I start with a note this time: Even though you were not interested in Project Server itself at all, I suggest you to read the post further, while most of the issues discussed below are not Project Server specific, they apply to SharePoint as well.
Recently I work mostly on a Project Server customization project. As I’ve learned on my former development projects, I try to automate so much repetitive tasks as possible (like automating the PWA provisioning), thus remains more time for the really interesting stuff. I plan to post my results on this blog to share the scripts and document the experiences for myself as well.
One of the very first tasks (and probably a never-ending one) was to create a customized Project Web Site (PWS) site template. New Enterprise Projects created in the Project Web Access (PWA) should have their PWS created based on the custom site template.
The process of customizing a PWS site template is described in this post, however, there are a few issues if we apply this approach alone, just to name a few of them:
– PWS master pages cannot be edited using SharePoint Designer by default. There is a workaround for this issue.
– If I create a custom master page for the PWA and would like a PWS to refer the same master page, I can set it for example using PowerShell. However, if I create a site template from this PWS, this configuration seems to be lost in the template, and template refers to the default seattle.master. I have similar experience with the site image / logo, I can set one, but this setting seems to be not saved in the template.
– The standard navigation structure of a project site (and all site template created based on it) contains project-specific navigation nodes, like Project Details that contains the Guid of the current project as a query string parameter. If you create a site template from this site, any project sites that will be created based on this template will contain this node twice: one of the is created based on the site template (wrong Guid, referring to the project the original site belongs to, thus wrong URL), and another one is created dynamically as the project web site gets provisioned.
The workflow of my web site template creation and customization process includes four main parts, and two of them – step 2 and step 4 – are automated by our script.
The first part of the process (including step 1 and step 2) is optional. If you have changed nothing in your web site prototype, you can immediately start with the manual manipulation of the extracted web site template content (step 3), otherwise, we have to get a fresh version of the template into our local system for the further customizations.
Step 1: Creation and customization a SharePoint web site, that serves as a prototype for the web site template.
A SharePoint web site is customized based on the requirements using the SharePoint web UI, SharePoint Designer (for PWA see this post), or via other tools, like PowerShell scripts (for example, JSLink settings). This is a “manual” task.
Step 2: Creation of the web site template based on the prototype, downloading and extracting the site template.
A site template is created (including content) based on the customized web site. If a former site template having the same name already exists, if will be deleted first.
The site template is downloaded to the local file system (former file having the same name is deleted first).
The content of the .wsp file (CAB format) is extracted into a local folder (folder having the same name is deleted first, if it exists).
Step 3: Customization of the extracted web site template artifacts.
The script is paused. In this step you have the chance to manual customization of the solution files, like ONet.xml.
Step 4: Compressing the customized files into a new site template, and uploading it to SharePoint.
After a key press the script runs further.
Files having the same name as our site template and extension of .cab or .wsp will be deleted. The content of the folder is compressed as .cab and the renamed to .wsp.
In the final step the original web site template is removed and the new version is installed.
Next, a few words about the CAB extraction and compression tools I chose for the automation. Minimal requirements were that the tool must have a command line interface and it should recognize the folder structure to be compressed automatically, without any helper files (like the DDF directive file in case of makecab).
After reading a few comparisons (like this and this one) about the alternative options, I first found IZArc and its command line add-on (including IZARCC for compression and IZARCE for extraction, see their user’s manual for details) to be the best choice. However after a short test I experienced issues with the depth of the folder path and file name length in case of IZARCE, so I fell back to extrac32 for the extraction.
Finally, the script itself:
$pwaUrl = "http://YourProjectServer/PWA/"
$pwsSiteTemplateSourceUrl = $pwaUrl + "YourPrototypeWeb"
$solutionName = "YourSiteTemplate"
$wspFileName = $solutionName + ".wsp"
$cabFileName = $solutionName + ".cab"
$siteTemplateTitle = $solutionName
$siteTemplateName = $solutionName
$siteTemplateDescription = "PWS Website Template"
$localRootPath = "D:\SiteTemplates\"
$wspExtractFolderName = $solutionName
$wspExtractFolder = $localRootPath + $wspExtractFolderName
$wspFilePath = $localRootPath + $wspFileName
$wspLocalPath = $localRootPath + $wspFileName
$wspUrl = $pwaUrl + "_catalogs/solutions/" + $wspFileName
$cabFilePath = $localRootPath + $cabFileName
function Using-Culture (
[System.Globalization.CultureInfo] $culture = (throw "USAGE: Using-Culture -Culture culture -Script {…}"),
[ScriptBlock]
$script = (throw "USAGE: Using-Culture -Culture culture -Script {…}"))
{
$OldCulture = [Threading.Thread]::CurrentThread.CurrentCulture
$OldUICulture = [Threading.Thread]::CurrentThread.CurrentUICulture
try {
[Threading.Thread]::CurrentThread.CurrentCulture = $culture
[Threading.Thread]::CurrentThread.CurrentUICulture = $culture
Invoke-Command $script
}
finally {
[Threading.Thread]::CurrentThread.CurrentCulture = $OldCulture
[Threading.Thread]::CurrentThread.CurrentUICulture = $OldUICulture
}
}
function Remove-SiteTemplate-IfExists($solutionName, $wspFileName, $pwaUrl)
{
$us = Get-SPUserSolution -Identity $solutionName -Site $pwaUrl -ErrorAction SilentlyContinue
if ($us -ne $Null)
{
Write-Host Former version of site template found on the server. It will be removed…
Uninstall-SPUserSolution -Identity $solutionName -Site $pwaUrl -Confirm:$False
Remove-SPUserSolution -Identity $wspFileName -Site $pwaUrl -Confirm:$False
}
}
function Remove-File-IfExists($path)
{
If (Test-Path $path)
{
If (Test-Path $path -PathType Container)
{
Write-Host Deleting folder: $path
Remove-Item $path -Force -Recurse
}
Else
{
Write-Host Deleting file: $path
Remove-Item $path -Force
}
}
}
Do { $downloadNewTemplate = Read-Host "Would you like to get a new local version of the site template to edit? (y/n)" }
Until ("y","n" -contains $downloadNewTemplate )
If ($downloadNewTemplate -eq "y")
{
Remove-SiteTemplate-IfExists $solutionName $wspFileName $pwaUrl
Using-Culture de-DE {
Write-Host Saving site as site template including content
$web = Get-SPWeb $pwsSiteTemplateSourceUrl
$web.SaveAsTemplate($siteTemplateName, $siteTemplateTitle, $siteTemplateDescription, 1)
}
Remove-File-IfExists $cabFilePath
Write-Host Downloading site template
$webClient = New-Object System.Net.WebClient
$webClient.UseDefaultCredentials = $True
$webClient.DownloadFile($wspUrl, $cabFilePath)
# clean up former version before downloading the new one
# be sure you do not lock the deletion, for example, by having one of the subfolders opened in File Explorer,
# or via any file opened in an application
Remove-File-IfExists $wspExtractFolder
Write-Host Extracting site template into folder $wspExtractFolder
# http://updates.boot-land.net/052/Tools/IZArc%20MANUAL.TXT
# limited file lenght / folder structure depth! :-(
#& "C:\Program Files (x86)\IZArc\IZARCE.exe" -d $cabFilePath $wspExtractFolder
#http://researchbin.blogspot.co.at/2012/05/making-and-extracting-cab-files-in.html
#expand $cabFilePath $wspExtractFolder -F:*.*
extrac32 /Y /E $cabFilePath /L $wspExtractFolder
}
Write-Host "Alter the extracted content of the site template, then press any key to upload the template…"
# wait any key press without any output to the console
# http://technet.microsoft.com/en-us/library/ff730938.aspx
$dummy = $host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
# clean up former version before creating the new one
# TODO rename it using a date time pattern instead of deletion!
Remove-File-IfExists $cabFilePath
Remove-File-IfExists $wspFilePath
# makecab: we cannot include multiple files directly. To do that, we have to create a directive file called a Diamond Directive File(DDF) and include instructions in it
# http://comptb.cects.com/automate-compression-tasks-cli/
& "C:\Program Files (x86)\IZArc\IZARCC.exe" -a -r -p $cabFilePath $wspExtractFolder
Rename-Item $cabFilePath $wspFileName
# remove former solution before uploading and activating the new one
Remove-SiteTemplate-IfExists $solutionName $wspFileName $pwaUrl
Write-Host Installing the new version of the site template
Add-SPUserSolution -LiteralPath $wspFilePath -Site $pwaUrl
$dummy = Install-SPUserSolution -Identity $solutionName -Site $pwaUrl
Note: If you are working with the English version of the PWA and have an English operating system on the server, you don’t need the Using-Culture function. To learn more about it see this post.
