5/18/2010The best practice is to deploy a site branding through a web solution package. We had a third party do a branding for us, then needed to update it before they delivered the source code for the WSP to us. So I found myself needing to update a master page for multiple site collections. The master page was deployed via the WSP. If I updated it by uploading a new version of the master page to the Master Page Gallery in each site collection, I’d have to do that upload 20 times since we had 20 site collections. But if I updated the file in its Feature folder in the 12 Hive, I’d only have to do the update once, and it would take effect everywhere assuming that the master pages that were out there were not customized from the site definition. Could I use PowerShell to quickly report on the customization state of all instances of my master page? Of course! function global:Get-SPWebApplication{ Get-SPFarm |% {$_.Services} | where {'$_.TYPEName -eq "Windows SharePoint Services Web Application"'} |% {$_.WebApplications} |% {Write-Output $_} } function global:get-AllSiteCols($webAppName){ $WA = Get-SPWebApplication |where {$_.Name -eq $webAppName} return $WA.Sites } function global:report-masterPageStates($masterFilename) { #example of $masterFilename: “mycustom.master’ $sites = get-AllSiteCols $webAppName $sites | foreach { $site = $_ $rootweb = $site.Rootweb $MPG = $rootweb.Lists["Master Page Gallery"] $masterItem = $MPG.Items | where {$_.Name -eq $masterFilename} if ($masteritem.File.CustomizedPageStatus -eq "Customized") {$fontcolor = "Red"} else {$fontcolor = "Green"} Write-host -foreground $fontcolor $site.Url":"$masterItem.Name ": Customization Status:" $masteritem.File.CustomizedPageStatus $rootweb.Dispose() $site.Dispose() } } --Michael 5/15/2010At my current client, we are setting up a MOSS 2007 web application that provides collaboration sites for their communities of practice. We have an initial site hierarchy of 155 sites across 20 site collections. How do you set that all up? Although we set up the 20 site collections manually (most got their own content databases, but that’s not the reason we did them manually. With only 20 items, in the time it would have taken to set up a script, it could also just be done, and it was good training for a new SharePoint support person. For the sites however, we wanted to have each site based on the Publishing site template, have a set of preconfigured lists, use a branding feature we had installed, and have a welcome page that used a particular layout and had a particular set of web parts. Because it was a publishing site, we couldn’t save it as a template. However, we could save lists as templates, so that’s what we did. - Save a list as a template:
$list.SaveAsTemplate($filename, $templatename, $list.Description, $true); #true if you want to #include content, $false if you don’t. - Once we have several templates saved in the List Template Gallery, we need to download them and then upload them into the other site collections. So let’s download all templates to a local folder:
function global:download-allListTemplates($dir,$siteCollectionURL) { cd $dir $site = get-SPSite $siteCollectionURL $rootweb = $site.RootWeb $LTG = $rootweb.Lists["List Template Gallery"] $LTG.Items | foreach { $_.Name $bytes = $_.File.OpenBinary() $bytes | set-content $_.Name -Encoding byte } $rootweb.Dispose() $site.Dispose() dir $dir } - Now we can upload templates to the list template gallery. The reason I do a .Replace(space, no-space) on the filename of the list stemplate is to remove any spaces from the filenames. Also I am assuming that $templateLocalDir only contains list templates (.stp in MOSS 2007)
function global:upload-listTemplatesTo($SiteColl,$templateLocalDir) { $rootweb = $siteColl.RootWeb $LTGrootFolder = $rootweb.GetFolder("List Template Gallery") Get-ChildItem $templateLocalDir | foreach { $stream = [IO.File]::OpenRead($_.FullName) if ($SiteColl.ServerRelativeUrl -eq "/"){ $desturl = "/_catalogs/lt/"+$_.Name.Replace(" ","") } else { $desturl = $SiteColl.ServerRelativeUrl+"/_catalogs/lt/"+$_.Name.Replace(" ","") } Write-host Loading $_.Fullname to $desturl $resultingfile = $LTGrootFolder.files.Add($desturl,$stream,$true) $stream.close() Write-Host $_.Name uploaded to $resultingfile.Url in $siteColl.Url } $rootweb.Dispose() } You can loop over the set of site collections in your web app and do this for all of them. To check your work, you can report on how many templates are in the List Template Gallery in each site collection. function global:get-AllSiteCols($webAppName){ $WA = get-spwebApplication |where {$_.Name -eq $webAppName} return $WA.Sites } function global:report-TemplateCounts($webAppName){ get-AllSiteCols $webAppName | foreach { $rootweb = $_.RootWeb $ltg = $rootWeb.Lists["List Template Gallery"] write-host $rootweb.Url $ltg.ItemCount $rootweb.Dispose() } } Once the templates are in the list template gallery, then you can create lists from all of them at once like this: function global:Create-AllCustomLists($web){ $site = $web.Site write-host In $web.Url $templates = $site.GetCustomListTemplates($web) Create-FromTemplates $web $templates $site.Dispose() } function global:Create-FromTemplates($web,$templates){ $templates |foreach { write-host Instantiating $_.Name if ($web.Lists[$listname] -eq $null) { $guid = $web.Lists.Add($listname, $_.Description, $_) $resultingList = $web.Lists.GetList($guid,$false) $resultingList.OnQuickLaunch = $True $resultingList.Update(); } else { Write-Host $listname "already exists." } } } Now you can create preconfigured lists in a site very quickly. List templates won’t preserve the relationship to site columns though. Lists that were created from templates will have list level column definitions for the columns that were originally site columns, and site column updates won’t roll down to these lists. Next time – more or less – I will cover setting up a welcome page based on a custom layout and adding web parts to pages, all via POSH. --Michael More of my posts about using PowerShell with SharePoint 5/14/2010I realize I’ve been woefully behind in my blogging about the SharePoint work I’ve done with PowerShell. Last year, I was working on a MOSS 2007 intranet website for a political action committee. At a high level, here are some of the things I did. - Use the choices in a choice field as the basis for a loop. $fieldname is the name of a column that is a Choices column.
$choicefield = $list.Fields[$fieldname] $choicefield.Choices | foreach { #do something } - Hide a navigation node (in a site with the Publishing feature turned on.
$pages = $web.Lists["Pages"] #the following performs fine because the Pages list is small, actually VERY short, just 4 #items. $pages.Items |where {$_.Title -eq $publishingPageTitle} |foreach { $pageitem = [Microsoft.SharePoint.Publishing.PublishingPage]::GetPublishingPage($_); $pageItem.IncludeInCurrentNavigation = $false} #Note that you may have to call $pageitem.Update() as well as checkin and/or publish #the item. - Create a site template from a non-publishing site
- Actually, in this case, it was a publishing site where I used one POSH script to deactivate the publishing feature and save the site (SPWeb) as a template. I then had another POSH script that created a new site (SPWeb) from the template, activated the Publishing feature, and fixing the navigation things that had gotten lost when the publishing feature was deactivated. This let me use templates even though the site was a publishing site.
- Create a site from a site template
- $site.AllWebs.Add($name,$name,"",1033,"mycustom.stp",$false,$false)
- Note that in this case, site name and relative URL are the same
- Create a bunch of data. Creating list items from spreadsheets is really easy. Import-csv is a very helpful cmdlet!
- Create folder list items as well as regular list items. In this case, I am creating the folders in the root of the list.
$FolderConst = 1 $folder = $list.Items.Add($list.RootFolder.ServerRelativeUrl,$folderConst) $folder["Title"]=$foldername $folder.Update() One of the advantages of POSH is that it lets you – without the overhead of building a compiled app – use Data Form Web Parts (DFWP) in more ways. For example, you can use POSH to use the SharePoint API to update the properties of the DFWP, including changing list IDs or filter expressions. I had a web part For my next blog post, I will (hopefully) talk about all the POSH scripting I have been doing for my current client, a Fortune 500 engineering firm, to create 155 sites across 20 site collections in a MOSS 2007 web application. 11/6/2009Here’s a little POSH script I wrote today, generalized a bit. The web part in question shows a list that has a column named “Title”, and one named “Percentage”, where it grouped them by a column called MarketType. It started out sorting ascending by title, and afterwards sorts descending by percentage. function global:Sort-WPBiggestPercentOnTop($SPViewUsedByWP) { $oldqu = $SPViewUsedByWP.Query if $oldqu -ne "<GroupBy Collapse=""FALSE"" GroupLimit=""100""><FieldRef Name=""Market_x0020_Type"" Ascending=""FALSE"" /></GroupBy><OrderBy><FieldRef Name=""Title"" /></OrderBy>" { write-error "View not as expected"} $newqu = $oldqu.Replace("<FieldRef Name=""Title"" />","<FieldRef Name=""Percentage"" Ascending=""FALSE"" />" $SPViewUsedByWP.Query = $newqu $SPViewUsedByWP.Update() write-host "Need to reset the state of the web part toolbar, it gets changed when the view gets edited." } 3/2/2009If you haven't started listening to the SharePoint Pod Show, you should! It's at www.sharepointpodshow.com, and I've listened two a few episodes so far - in particular the interviews with Andrew Connell and Jeremy Thake (Episode 13). Thake even mentions my PowerShell Building Blocks (PSBB's) for SharePoint at 40 minutes, 53s to 42 minutes 45 seconds. PSBB is one of my CodePlex projects - you can find it at www.codeplex.com/PSBB . He mentions one that displays the SharePoint version number. I don't think that's actually in the PSBB collection - yet. I need to add a number of scripts to it, and work with Neil Iverson to include his. I the mean time, here is how to get the SharePoint version number. function global:Get-SPFarm{ return [Microsoft.SharePoint.Administration.SPFarm]::Local } $farm = Get-SPFarm $farm.BuildVersion The result is: Major Minor Build Revision ----- ----- ----- -------- 12 0 0 6318 --- Michael 1/29/2009Here's a bunch of brief things that I think are too short to merit their own post. 1) CAML syntax that ought to error but doesn't. Double, Nested Query tags return all records. Strange thing. When building a CAML query, I accidentally malformed my query syntax: Instead of <Query><Where><and> etc, I had <Query><Query><Where><And> etc.. I had an extra pair of Query tags within the outer Query tags. I would expect that to be invalid (not conforming to the CAML Query Schema for example) but it's not. Instead of throwing a error, it treats the where clause like it's not there and returns all records. 2) Anonymous Access permissions can't be set of you have the list's Advanced Settings set so users can only read their own items. This makes sense, but the connection between the disabled checkboxes on the anonymous access permissions page and the Advanced Settings is not obvious. I discovered this when I needed to allow anonymous users to be able to add items to a list (such as contact requests for a user group). To do this, you have to give anonymous users the Add items and View items permissions (you can't give Add without View). 3) There is a BDC behavior when refreshing a Business Data column that looks a lot like a bug. See http://social.msdn.microsoft.com/Forums/en-US/sharepointbdc/thread/fc10e5e4-a250-4859-bb1e-6b87f7b01352/ 4) Performance Point is being rolled into MOSS. The Business Intelligence part of the MOSS pie gets even stronger! From the SharePoint Team Blog: Microsoft Business Intelligence strategy update and SharePoint 5) SPDisposeCheck is released! Finally! From the SharePoint Team Blog: SPDisposeCheck Released! 6) My Dad can still teach me a thing or two. In Outlook, do you ever find that you have an outdated address stuck in the list of addresses that Outlook suggests as you type an address? Well, I have a number of old addresses that I wanted to get rid of but couldn't figure out how. Right-clicking didn't do anything, and once I tried looking in the registry to find where it stored the list, but I think it was stored in a binary format so I couldn't find it. It turns out that the Delete key works when you are in that list, and if you use the up and down arrow keys to select the bad address, you can remove it with just a tap of the Delete key. It never would have occurred to me that the Delete key would work in that context. There's a lesson about affordances in there.
7) No longer presenting at SharePoint Connections this March. Last September (2008), I had the honor of being selected to present three sessions at the March 2009 SharePoint Connections conference. I was told today that due to budget cuts and lower registrations, sessions were being cut. Mine included. I would have been presenting: - Site Provisioning Solutions
- Information Architecture for a MOSS intranet
- PowerShell for MOSS Developers and Administrators
I hope to have the opportunity to present these (or the other presentation in my repertoire, "An Introduction to MOSS Administration") at a future conference. In the mean time, please go attend as many SharePoint conference as possible so this doesn't happen again! 8) PowerShell is so darn handy! Today I was doing some code enhancements and noticed that I was limiting a number of textboxes to 10 characters. I needed to see if the corresponding list field was also limited to 10 or not. I wanted to see what the max length on the text fields in my list were, but the length is not shown on the list settings page and I didn't want to have to click into every text column. Tada! A bit of PowerShell to the rescue. .\moss.ps1 $web = Get-SPWeb $url $list = $web.Lists["MyCustomList"] $list.Fields | Select-Object -Property Title,Type,MaxLength,Hidden | where {-not $_.Hidden}| where {$_.Type -eq "Text"} | Select-Object Title, Type, MaxLength | sort -property MaxLength moss.ps1 defined the Get-SPWeb function. $url is the URL to my site. The output is a nice three column list of column name, datatype (all Text in this example), and the max length. That's all for now! --Michael 7/8/2008I've just started the next version of a workflow solution I am building for a client, and took the time to automate my build and deployment process a bit more. To this end, I wrote the following PowerShell functions that you can use (as-is, no warranties, etc, etc, blah, blah, blah.) - Clear-Alerts-From-SPWeb -URL <URL>
- Removes all alerts from a site. Useful for example if you restore a backup of a production site collection to your dev environment, and you don't want real end users getting alerts from your dev site.
- Clear-SPList -ListName <name of list> -URL <URL of parent web>
- I have a list that I fill with sample data when I do my unit testing, and when I am ready to do another round of testing, this gives me a very quick way to delete all items from the list. I suppose that makes it a bit dangerous.
- Delete-ContentType -ContentTypeName <name> -ListName <name of list> -URL <URL of parent web>
- Deletes a content type from a list. Since my deployment package includes three content types, my deployment process deletes the columns added by the content types, deletes the content types from the list, deactivates and removes the feature that defined them, then deploys and activates the new build of the same.
- Delete-SPField -FieldName <field name> -ListName <name of list> -URL <URL of parent web>
- Deletes a column from a list. Since adding a content type with new fields adds those fields to the list, but the fields don't disappear when you remove the content type, I use this function to remove them.
- Add-ContentType-To-List -ContentTypeName <name> -ListName <name of list> -URL <URL of parent web>
All of these rely on the Get-SPWeb function from Zach Rosenfield. --Michael 5/2/2008 $wspfile = "mysolution.wsp" $sollist = stsadm -o enumsolutions $xmldoc = [xml]$sollist $xmlnode = [System.Xml.XmlNode]$xmldoc #$xmlnode.SelectSingleNode("Solutions/Solution[File='$wspfile']") $resultElement = $xmlnode.SelectSingleNode("Solutions/Solution[File='$wspfile']/LastOperationResult") $result = $resultElement.get_InnerXml() "Deployment Result: " + $result if ("DeploymentSucceeded" -ne $result) {exit 1} This script will extract the deployment result string from the XML returned by stsadm for the wsp of interest. The last line is useful if you are using this in a build script. If you are using this interactively, you probably want to change the then clause to something other than an exit statement. Michael 2/28/2008 Here are some examples of working with MOSS lists via PowerShell. All of these assume that Get-SPWeb() is defined. You can get that from http://sharepoint.microsoft.com/blogs/zach/Lists/Posts/Post.aspx?List=90bbfd11%2Dc9a5%2D45cf%2Da77e%2D19559aae81ae&ID=7. -
Adding a Calendar list to an SPWeb function global:Add-Calendar($url,$title){ $destWeb = get-SPweb -url $url $caltemplate = $destWeb.ListTemplates["Calendar"] $listtitle = $title+" Events" $destWeb.Lists.Add($listtitle,"Departmental calendar of events.",$caltemplate) Dispose-SPWeb($destWeb) } -
Adding an Announcements list to an SPWeb function global:Add-News($url,$title){ $destWeb = get-SPweb -url $url $template = $destWeb.ListTemplates["Announcements"] $destWeb.Lists.Add($title+" News","News and announcements.",$template) Dispose-SPWeb($destWeb) } -
Adding a Links list to an SPWeb function global:Add-LinksList($url){ $destWeb = get-SPweb($url) $template = $destWeb.ListTemplates["Links"] $destWeb.Lists.Add("Related Sites","Links to relevant other sites.",$template) Dispose-SPWeb($destWeb) } -
Adding all three function global:Add-NewsCalAndLinks($url,$title){ Add-News -url $url -title $title Add-Calendar -url $url -title $title Add-LinksList($url) } -
Printing out a list of the Lists $web = Get-SPWeb($url) $web.Lists | ForEach-Object -process {$_.Title} Dispose-SPWeb($web) -
Printing out a list of the ListTemplates $web = Get-SPWeb($url) $web.ListTemplates | ForEach-Object -process {$_.Name} Dispose-SPWeb($web) --Michael 1/16/2008 Way back when the first Web browser, Mosaic, was just being born at NCSA at the University of Illinois, I was in college as well, and was running Linux. I was into Unix Shell scripting – csh, ksh, zsh, etc. Once I graduated, I switched to Windows and with the exception of a brief period of time working with Broadvision (yuck!) it's been all Microsoft all the time. Professionally, I've always been a Microsoft developer and IT consultant. However, my experience with Unix shell scripting taught me the value of a rich shell language and for a long time the Dos/Windows command prompt seemed like a step down. Then PowerShell happened, and it was a whole new world. I'd read about PowerShell, and subscribed to the PowerShell blog, but didn't really have much justification to use it. I used it once to create some test data files, and then just recently found a great use for it when deployment testing a SharePoint Application Pages application. My application used a .WSP solution file and several features to define and create about a dozen SharePoint lists in one site. During the feature deployment, most of those lists get prepopulated with data. A subsequent retract and delete of the solution though does not delete the lists. Also, in one list that I am using for configuration, I have the list item values set for the production environment and these settings are wrong for my development environment. PowerShell to the rescue. First, I got Zach Rosenfield's PowerShell script that provides access to SPWebs and SPSites (http://sharepoint.microsoft.com/blogs/zach/Lists/Posts/Post.aspx?ID=7), ran it, and then I did the following: $rootweb = Get-SPWeb "http://moss.litwareinc.com" for ($i=0; $i -lt $rootweb.Lists.Count; $i++) { $siteTitle = $rootweb.Lists[$i].Title; if($siteTitle -like "Site Pro*") { $siteTitle; $rootweb.Lists[$i].Delete(); } } $rootweb.Dispose(); Note that here all the lists that I wanted to delete began with a common prefix. Also, since I am deleting each list while looping through the collection of lists, the loop ends prematurely because the count of lists keeps decreasing. So I have to run the script a few times before it gets all the lists. But hitting up arrow and enter a few times took a lot less brain power and clicking than manually deleting each list. Eventually I unrolled the loop and just had it execute a handful of $rootweb.Lists["<list name>"].Delete(); commands (one for each list I needed to remove). For the SharePoint list that held my application settings, I had two settings that included the name of the staging environment (mosstest.litwareinc.com), while my development environment was called moss.litwareinc.com. Since this list and its data was instantiated fresh with each deployment, I could reference individual list items by ID, which would be the same after almost every build. $rootweb = Get-SPWeb "http://moss.litwareinc.com/" $listitem = $rootweb.Lists["Site Provisioning App Settings"].Items.GetItemByID(11) $listitem["Value"] = $listitem["Value"].ToString().Replace("test","") $listitem.Update() $listitem = $rootweb.Lists["Site Provisioning App Settings"].Items.GetItemByID(21) $listitem["Value"] = $listitem["Value"].ToString().Replace("test","") $listitem.Update() I included these scripts in my build process so that on every build of my SharePoint application, I compile all code, retract and delete the existing Solution and its features (deactivating them first if needed), delete the relevant lists, build the new WSP, install, deploy, and activate the solution and site level features and update one of the resulting lists. The only downside is that it takes about 3 minutes for a complete build. This is inside a VM that uses 1GB of Ram, so it's not the highest performance environment. That's not the end of the world, but if you want to test a small code change, it's annoying. I'm tempted to add build configurations where you have one configuration that does this whole process and one that just updates the DLLs. --Michael |
|
|
|
|