対象ユーザーを使って、ビューのアクセス制御っぽいことを実現する

小ネタ、というかメモとして書いておきます。

ご存じのように、SharePointではビューに固有のアクセス権をつけることはできません。
が、ちょっとした工夫でアクセス制御っぽい機能を実現することができます(「ぽい」です、あくまで)。

SharePointのwebパーツには、「対象ユーザー」を設定することができますよね。
リストのビューもwebパーツではあるので、対象ユーザーを設定して特定のユーザーにしか見せないようにできます。

【設定手順】
※以下、画面は2010ですが、2013でも同様にできることを確認しています。

①アクセス制限をかけたいビューを表示した状態で、「サイトの操作」→「ページの編集」を選択します。
②リストビューwebパーツのドロップダウンから、「webパーツの編集」を選択します。
20150114-1
③webパーツの設定画面の「詳細設定」セクションの「対象ユーザー」に、ビューにアクセスさせたいSharePointグループを指定し、「OK」をクリックします。
20150114-2
④リボンで「編集の終了」を押して保存します。
以上です!

これで、指定したグループに登録されていないユーザーは、ページにはアクセスできるもののビューが表示されません。
20150114-3
リボンにも「参照」タブしか表示されず、いい感じです。

厳密にいえば、隠してるだけでほんとのセキュリティではないのですが、こんなやり方もあるよ、ということで。

SharePoint RESTサービスとKnockout.jsで遊んでみた

いぜん、この辺のポスト
SharePoint2013 & Knockout.js で遊んでみた(その3)
でKnockout.jsを使ってSharePointリストのCRUDを試みましたが、その時心残りだったのが、jQuery Library for SharePoint Web Servicesというライブラリを使っていたこと、アイテム更新時に差分だけの更新ができなかったことでした。

で、かねてよりRESTサービスを使って何とかできないかなぁ・・・と考えていたのですが、先日こちらの記事を読みまして、
SharePoint REST サービスを使用したアイテムの CRUD 方法 – Japan SharePoint Support Team Blog – Site Home – TechNet Blogs
ここに記載のサンプルコードを元にトライしてみることにしました。(SharePoint2013のオンプレ環境です)

画面イメージはこんな感じです。
20150109

事前準備として、サイト内に「testList」という名前のカスタムリストを作成し、「Body」という一行テキスト列を追加しておきます。

ではソースコードです。
以下を、ページのスクリプトエディタ内にぺちょっと貼っていただけばOKです。

<script type="text/javascript" src="http://code.jquery.com/jquery-1.11.1.min.js"></script>
<script type="text/javascript" src="http://ajax.aspnetcdn.com/ajax/knockout/knockout-2.2.1.js"></script>
<script langauge="JavaScript">
var weburl = "http://[your host]/[your site]";
var listTitle = "testList";
var myDigest = null;

function ItemsModel() {
  var self = this;
  self.Items = ko.observableArray([]);
  GetItems();

  function GetItems() {
      $.ajax({
      url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items",
      type: "GET",
      headers: {
                "accept": "application/json;odata=verbose",
                "Content-Type": "application/json;odata=verbose",
                "x-requestforceauthentication": true
              },
      success: function(data){
                if (data.d.results) {
                  self.Items(data.d.results);
                }
              },
      error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
    });
  }

  function runWithFormDigest(fn){
    if (myDigest == null){
      $.ajax({
        url: weburl + "/_api/contextinfo",
        type: "POST",
        contentType: "application/x-www-url-encoded",
        dataType: "json",
        headers: {
                  "accept": "application/json;odata=verbose",
                },
        contentLength: 0,
        beforeSend: function (xhr) { xhr.withCredentials = true; },
        success: function (data) {
                  if (data.d) {
                    myDigest = data.d.GetContextWebInformation.FormDigestValue;
                    fn(); 
                  }
                }
      });
    } else {
      fn();
    }
  }

  self.AddRow = function() {
    self.Items.push({
      ID: "New",
      Title: "",
      Body: "",
    });
  };

  self.DelItem = function(data){
    var id = data.ID;
    if (id !== "New") {
      runWithFormDigest(function(){
        $.support.cors = true;
        $.ajax({
          url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items(" + id + ")",
          type: "POST",
          headers: {
                    "X-HTTP-Method":"DELETE",
                    "accept": "application/json;odata=verbose",
                    "Content-Type": "application/json;odata=verbose",
                    "x-requestforceauthentication": true,
                    "X-RequestDigest": myDigest,
                    "IF-MATCH": "*"
                  },
          success: function(xhr){ alert("completed.")},
          error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
        });
      });
    };
    self.Items.remove(data);
  };

  self.SaveItem = function(data){
    var id = data.ID;
    var title = data.Title;
    var body = data.Body;
    if(id!= "New"){
      runWithFormDigest(function(){
        $.support.cors = true;
        $.ajax({
          url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items(" + id + ")",
          type: "POST",
          data: JSON.stringify({ '__metadata': { 'type': 'SP.Data.TestListListItem' }, 'Title': title, 'Body': body }),
          headers: {
                    "X-HTTP-Method":"MERGE",
                    "accept": "application/json;odata=verbose",
                    "Content-Type": "application/json;odata=verbose",
                    "x-requestforceauthentication": true,
                    "X-RequestDigest": myDigest,
                    "IF-MATCH": "*"
                  },
          success: function(xhr){ GetItems(); alert("completed.");},
          error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
        });
      });
    } else {
      runWithFormDigest(function(){
        $.support.cors = true;
        $.ajax({
          url: weburl + "/_api/Web/Lists/GetByTitle('" + listTitle + "')/Items",
          type: "POST",
          data: JSON.stringify({ '__metadata': { 'type': 'SP.Data.TestListListItem' }, 'Title': title, 'Body': body }),
          headers: {
                    "accept": "application/json;odata=verbose",
                    "Content-Type": "application/json;odata=verbose",
                    "x-requestforceauthentication": true,
                    "X-RequestDigest": myDigest
                  },
          success: function(xhr){ GetItems(); alert("completed.")},
          error: function (xhr) { alert(xhr.status + ": " + xhr.statusText) }
        });
      });
    }
  }
}

$(document).ready(function () {
  ko.applyBindings(new ItemsModel());
});
</script> 
<div id="mainContent">
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Title</th>
        <th>Body</th>
        <th></th>
        <th></th>
      </tr>
    </thead>
    <tbody data-bind='foreach: Items'>
      <tr>
        <td data-bind="text:ID"></td>
        <td><input data-bind='value: Title' /></td>
        <td><input data-bind='value: Body' /></td>
        <td><input type='button' value='Save' data-bind='click: $root.SaveItem' /></td>
        <td><input type='button' value='Delete' data-bind='click: $root.DelItem'/><td>
      </tr>
    </tbody>
  </table>
  <input type="button" value="Add New" data-bind='click: AddRow' />
</div>

なお、上で引用したSharePoint Support Teamさんのブログではアイテムの追加と更新のとき、「type」に「SP.ListItem」を渡していましたが、私の環境ではうまくいきませんでした。
なんでだろう・・・と悩みつつ、こちらを読むと
REST を使用したリスト アイテムの操作

この操作を実行するには、リストの ListItemEntityTypeFullName プロパティを知っていて、それを HTTP 要求本文の type の値として渡す必要があります。

とあったので、上記ソースコードのような書き方になっています。
私のソースコードでうまくいかない場合は、Support Teamさんのブログを参照してください。

※ListItemEntityTypeFullName プロパティは、ブラウザのアドレスバーに以下を入力してEnterすると取得できます。

http://[your host]/[your site]/_api/lists/getbytitle('testList')?$select=ListItemEntityTypeFullName

RESTサービスだと値をJSON形式で取れるので、SPServicesを使っていたときよりだいぶシンプルに書けたのではないかと思います。

2014年のまとめ

気が付けば2015年になってしまいました。あけましておめでとうございます。
さて、WordPressから2014年のまとめレポートが届いてたので、ちょっとだけご紹介。

WordPress.com 統計チームは、2014年のあなたのブログの年間まとめレポートを用意しました。

概要はこちらです。

シドニーにあるオペラハウスのコンサートホールには2,700人が収容できます。2014年にこのブログは約9,800回表示されました。オペラハウスのコンサート4回分になります。

このペースだと、来月くらいには1万アクセス到達でしょうか?
(とかなんとか言ってたら、すでに1万アクセス突破してました・・・追記)

ちなみに、昨年一番人気の記事は

サイドリンクバーで「最近の変更」を非表示にする

でした。

ひきつづきゆるーく頑張りたいと思いますので、今年もよろしくお願いいたします。