Tuesday, November 5, 2013

Sandbox: Create subsites from XML

Introduction

Last blog entry I talked about creating publishing pages from an xml file in a sandboxed solution. This time I'm continuing in line with an article about creating subsites based on an xml file in the SiteCollectionsDocuments folder. The code is somewhat simpler since creating subsites in the sandbox is not very difficult, it actually works the same as always.

The most difficult part is to run through the XML recursively since you'll want to be able to create sub-sub-sites in your site collection from one input file.

In the xml-file you detail the url, title, description and the webtemplate name you want to use for a node. You could extend this with the future-webs' LCID.
For the webtemplate you can of course also use a custom solution webtemplate that is deployed to the solution gallery, or the name of a site saved as a template (it will then also end up in the solutions gallery).

The webpart consists of only a button (to run the code) and a label (to display errors). You might see an timeout error when running the code. Since creating websites is a time consuming task, I'm pretty certain the following exception occurs:
Have fun trying it out! I was just as lazy as the last time so the code now expects the xml file to be exactly the second file in the SiteCollectionDocuments folder :-)

Friday, October 25, 2013

Sandbox: Creating pages from XML file

Introduction

We are researching a move from SharePoint 2007 to SharePoint Online. Regarding moving the actual content (publishing pages) options seems to be limited especially with regard to migrating towards using managed metadata columns. So what we wanted is the ability to read from an XML file and create publishing pages through sandboxed code. The sandboxed code can be deployed to SharePoint online and the xml can be created by custom farm-level code on the SharePoint 2007 farm.

Solution

The blog article Publishing in sandboxed solutions - Create a page contains the main code behind the sandboxed visual webpart that I created. The webpart depends on a pages.xml file that is the only file in the SiteCollectionDocuments folder (hey, this is only a Proof of Concept!). See below for the contents of the XML file and the code for the webpart.

I used the Visual webpart template in VS2012, set to sandbox solution for developing the webpart. This should work similarly in VS2010 with the Power tools extension installed.

Here's what to do once you have the solution available:

1. Put pages.xml as the only file in the SiteCollectionDocuments folder of a publishing portal
2. Deploy sandboxed solution to site
3. Put CreatePages webpart on a webpart page
4. Push the button and see if a page is created correctly in the Pages library of the root web


Code for Creating Pages from XML file in Sandboxed webpart
XML file from creating pages in sandboxed webpart.

Post a comment if you would like to receive the whole VS project (but hopefully you can build it yourself from this blogpost!)

Wednesday, July 10, 2013

AB Testing in SharePoint using Variations: The SharePoint Solution (1)

The solution has the following pre-conditions:
- A site collection should exist with an X number of variation sites. Preferably the variations are named A, B, etc to clearly indicate the variations only exist for testing.
- There is a number of pages on which there is a call to action (e.g. a registration page or a newsletter sign up) and if a user performs the action they are redirected to a target page (e.g. a thank you for registering/signing up page).
- The controls used in the site have been made variation-aware (e.g. they work correctly when variations are turned on) by the developers of the site.

The solution consists of the following components:

  1. Variation Switcher
  2. Behaviour Recorder
  3. Application page for setup
  4. (minimal) Masterpage
  5. Statistical Results overviews
These components are explained below.
1. Variation Switcher: The Variation switcher component will randomly assign a variation to a new user that visists the site, for existing users the code will redirect it to the variation that was assigned to him/her on their first visit. If possible the code will direct the user to the same page but in the correct variation site, otherwise it redirects to the variation root for the correct variation. A visitor is unique or not depending on the existence of a browser cookie (so if the user deleted cookies, (s)he is a new unique visitor on the next visit).
2. Behaviour Recorder: This component checks (for each page load) if the page is a call to action or a target page and records the visit of a user. It also records some other user information link the user agent string etc. This information is stored in a list in the root web of the site collection.
3. Application page for setup: This page allows a site admin to setup call to action and target page pairs for which page visits need to be recorded. It also allows the site admin to view and update the lists of campaigns etc (via a link to the list or on the page itself, tbd)
4. Masterpage: A minimal masterpage that includes the controls above is deployed by the solution so a site admin can easily customize the branding of the site for AB testing.
5. Statistical Results overviews: The current results of the AB testing should be shown in visual webparts in order to clearly show the performance of the A and B versions of the site. It should show the statistical significance of the site, indicate the power of the test and conversion rates of the versions of the site for each target-call to action page pair. 

Once the variations exist, the target and call to action pages have been set and the controls are added to the masterpage of each variation then (based on the start and end date of the campaign) the AB testing has started.

Next blog will continue about the technical implementation, and have the downloadable solution to try it out.


AB Testing in SharePoint using Variations

After reading about UXPin and an article about AB Testing on smashing I decided to try to create a SharePoint solution that utilizes the variation mechanism to allow site admins to AB test certain aspects of their design. In this way changes in visual or functional design can be tested for the conversion rate (how many people successfully perform an action on your site).

Will keep you up-to-date on the progress of the solution!

Monday, June 24, 2013

Office 365: Recreate cross site publishing


This is a very to-the-point description of how I made a setup similar to the new Cross site Publishing features in SharePoint 2013 but in the current version of Office 365 that does not yet have those standard features.

The goal is to pull in content that is tagged with a certain Term from one site collection in another, based on a property of the current user's profile (such as Country = NL). This allows from complete de-coupling of the authoring and the publishing sites, which means you can publish a piece of content once and then have it appear on different devices with custom branding in the desired presentation style.

Currently the standard SharePoint 2013 to do this are not yet enabled in Office365. That is why I created this javascript in order to create the same functionality but with client-side scripting and the REST API's.

General:
1. Create Managed metadata term set (in this case HRS > Country ).
2. Add managed property to User Profiles in SharePoint Administration > Profiles. Type is text and the property is connected to the Country Termset, same as your site column.
3. Set your own user profile property value to a Term that you will create content for at step 9.
4. Create two site collections: Author and Reader.

Authoring site collection:
4. In the Author site collection setup variations in the normal way: Add a few variation labels and locales.
5. Create Site Column attached to managed metadata term set (single valued). In this case HRS_Country.
6. Create content type based on Publishing Page. Add Site Column HRS Country.
7. Create a page layout (download the webpart page layout from the masterpage gallery, rename it and upload it again). Attach it to the content type you created during upload.
8. Make the page layout available in the variation subsites.
9. Create some content in the source variation. When the variation timer job has run, update the page's tagging and content in the target variations.
10. Turn off automatic propagation of changes in the variation settings, because they're annoying as hell.
11. Wait for a crawl of the content.

In the Reader site collection:
12. Create custom masterpage.
13. Add script references to custom masterpage.
14. Set masterpage of the site to your custom masterpage.
15. Add javascript/jquery in a script editor webpart on the homepage of the 'Reader' site.

The javascript will wait until the correct JS libraries have been loaded. After this it gets the current user's profile properties and then runs a query on the search API with the current user's Country value.
It will return all items that are tagged with the same country as the current user's profile. It will get three fields Url, PublishingImage and Title.

In the Itemsfound method the html will be built that places the values of those three columns of the found items in a UL (ala the content query webpart but across site collections.

Great Success! You've just rebuilt the new Cross site Publishing functionality and Content by Search webparts but with the REST API's.


This means it will also work in Office365 right now (which should receive the cross site publishing feature later this year).

Javascript for 'Cross-site Publishing'

<script>
// initialize profile properties
var g_CurrentUserProfileProperties = "";
var g_CurrentUserName = "";
var g_CurrentUserCountry = "";
var g_CurrentUserLanguage = "";
var g_CurrentUserAccountName = "";
var g_CurrentUserWorkEmail = "";
</script>

<!-- div that will be appended with the found items -->
<div id="divContentContainer"></div>

<!-- script that will do the deed -->
<script type="text/javascript">
    $(document).ready(function ($) {
 var e = ExecuteOrDelayUntilScriptLoaded(DisplaySearchResults, "sp.js");      
    });

function DisplaySearchResults()
{
CallloadCurrentUserDataWhenScriptIsloaded();
}


function CallloadCurrentUserDataWhenScriptIsloaded()
{
SP.SOD.executeOrDelayUntilScriptLoaded(loadCurrentUserData, 'SP.UserProfiles.js');
}

function loadCurrentUserData()
{
//Get Current Context
var clientContext = new SP.ClientContext.get_current();

//Get Instance of People Manager Class
var peopleManager = new SP.UserProfiles.PeopleManager(clientContext);

//Get properties of the current user
userProfileProperties = peopleManager.getMyProperties();

clientContext.load(userProfileProperties);

//Execute the Query.
clientContext.executeQueryAsync(onloadCurrentUserDataSuccess, onloadCurrentUserDataFail);

}
 
function onloadCurrentUserDataSuccess() {  

// Get ProfileProperties object
g_CurrentUserProfileProperties = userProfileProperties.get_userProfileProperties();

// Get specific properties
g_CurrentUserName = userProfileProperties.get_displayName();
g_CurrentUserCountry = g_CurrentUserProfileProperties['Country'];
g_CurrentUserLanguage = g_CurrentUserProfileProperties['Language'];
g_CurrentUserAccountName = g_CurrentUserProfileProperties['AccountName'];
g_CurrentUserWorkEmail = g_CurrentUserProfileProperties['WorkEmail'];

console.log(g_CurrentUserName);
console.log(g_CurrentUserCountry);
console.log(g_CurrentUserAccountName);
console.log(g_CurrentUserWorkEmail);

if (g_CurrentUserCountry != null && g_CurrentUserCountry != undefined)
{
var basePath = "https://jwiersem.sharepoint.com/sites/hrs-content/_api/";
var aFullQuery = "search/query?Querytext='HRS_Country=" + g_CurrentUserCountry + "'&selectproperties='Url,Title,PublishingImage'";

$.ajax({
url: basePath + aFullQuery,
type: "GET",
headers: { "Accept": "application/json;odata=verbose" },
success: function (data) {
ItemsFound(data);
},
error: function (data) {
//output error HERE
alert('Error api call 1: ' + data.statusText);
}
});
}
}

function onloadCurrentUserDataFail(sender, args) {
alert("Error: " + args.get_message());
}

function ItemsFound(data)
{
//script to build UI HERE
//get the details for each item
var listData = data.d.query.PrimaryQueryResult.RelevantResults.Table.Rows.results;

if (listData != null && listData != undefined)
{
var itemCount = listData.length;
var processedCount = 0;
var ul = $("<ul>");
for (i = 0; i < listData.length; i++) {
var aCurrentItem = listData[i].Cells.results;
var aCurrentItemURI = GetValueFromArray(aCurrentItem, "Url");
var aCurrentItemTitle = GetValueFromArray(aCurrentItem, "Title");
var aCurrentItemImageHTML = GetValueFromArray(aCurrentItem, "PublishingImage");
aCurrentItemImageHTML = unescape(aCurrentItemImageHTML);

// Show only the pages with the same language as the user's preferences
var aIsCurrentUserLanguageItem = IsCurrentUserLanguageItemOrGlobal(aCurrentItemURI, g_CurrentUserLanguage);

if (aIsCurrentUserLanguageItem)
{
var htmlStr = "";
var aLink = "<a href='" + aCurrentItemURI + "'>" + aCurrentItemImageHTML + "</a>";
var aTitleSpan = "<span>" + aCurrentItemTitle + "</span>";
var aFullDiv = "<div>" + aLink + aTitleSpan + "</div>";

htmlStr += "<li>" + aFullDiv + "</li>";

ul.append($(htmlStr));
}
} // end for all items in listData
ul.append("</ul>");
$("#divContentContainer").append(ul);
}
}

function IsCurrentUserLanguageItemOrGlobal(aCurrentItemURI, aCurrentUserLanguage)
{
var Yesitis = ((aCurrentItemURI.toLowerCase().indexOf(aCurrentUserLanguage) >= 0) || (aCurrentItemURI.toLowerCase().indexOf("global") >= 0));

return Yesitis;
}

function GetValueFromArray(aArray, aKey)
{
var aReturnValue = "";
var aCounter = 0;

if (aArray != null && aArray != undefined && aArray.length > 0)
{
if (aKey != null && aKey != undefined)
{
$.each(aArray, function(key, value)
{
var bKey = aArray[key]["Key"];
if (bKey == aKey)
{
aReturnValue = aArray[key]["Value"];
aCounter++;
}
});
}
}

if (aCounter > 1)
{
console.log('Error in GetValueFromArray: Multiple entries foudn in array with the same key');
}

if (aReturnValue == undefined || aReturnValue == null || aReturnValue == "")
{
console.log('Error in GetValueFromArray: Key not found in array');
}

return aReturnValue;
}
</script>

Sunday, May 26, 2013

Wolfram Alpha Widget App for Office 365/SharePoint 2013

Description of how to create a SharePoint 2013 App

Introduction
SharePoint 2013 uses the new app model to enable all developers to easily create re-uable components that can't negatively affect farm stability, because they do not rely on code deployments on the server itself.
The apps can only use a small subset of the options the SharePoint object model provides, similar to Sandboxed Solutions in SharePoint 2010 (still available in 2013 btw).
The main focus of development with the App model lies in HTML+CSS+Client side code with SharePoint's own Client-side Object Model and/or jQuery.
It is difficult to see just yet what the App model adds to SharePoint development, and what actually the added benefits compared to Sandboxed Solutions are (that already wasnt a very successful new methodology in SP2010).
Solution in Visual Studio 2012
But let's not complain until 2013 has gained more ground and it becomes more clear what the practicality is of the new development model. For now, we can only try it out and see the (im)possibilities of a SharePoint app.

To do just that, and because I became interested again in Wolfram Alpha, I decided to make a SharePoint App that can be used to dynamically pull in some of their user generated widgets. 
Yes indeed, an App that does nothing but show other 'apps' :-)

Wolfram is known in the Computer Science and Mathematics scene for o.a. Mathematica, a very advanced tool for doing and learning about mathematics, and also the search engine that uses some kind of inferencing magic to give you 'smart results' instead of just comparing the search query with the index. The user generated widgets they provide on their site use that search/inefrencing engine in order to create small widgets that provide a specific function.

On the right is an image of the VS2012 solution, most noteworthy is the WolframSharePointApp.js file, that contains all the client side code that makes the app function, the default.aspx that contains the html and script references for the app, and the list instance WolframWidgets. This list holds the information required to fetch the Wolfram Widgets, and will be added to a drop down list so a user can select a single Widget.

See the app below after selecting the Derivative Solver. Also available for selection are the human growth chart, a currency convert, a time zone converter, a calculator and then some. As you can see, with very limited amount of code and by using pre-existing widgets freely available you can quickly create a fun, versatile and perhaps even useful App for SharePoint 2013/Office 365.

The App in Office 365 with the Derivative Solver selected.
Of course the App is not perfect yet (javascript is a bitch!), when it is I'll expand this post with the code and a link to the final product.