Monday, 17 December 2012

Fun With PowerShell and SharePoint 2010

I recently had a project to create a repository for staff records, requiring a new site that required hundreds of SharePoint groups and individualy permissioned wiki pages with custom content and provisioned web parts. The idea of doing this manually seemed crazy and, as it was a one-off task, I decided to tackle it in PowerShell.
This post is broken into two parts, one for each script. The first deals with the creation and population of the SharePoint groups. And the second creates and customises the permissions and content for the wiki pages. I also provision a ListViewWebPart with custom view, and add a ContentEditorWebPart with content.

Creating and Populating SharePoint Groups

Once I had the group and member names (and verified them), this task was actually surprisingly simple. I simply created two arrays as shown below.
You can provide one or more users per group using a semi-colon as the delimiter.
Next we add the users to the site. This is important. I originally tried provisioning them directly to each group as it was created but this had the curious side effect of adding the users as group owners and not adding them to the group itself.
We then create an indexer for the users array and run a foreach loop against the groups array.
And that's it. Update the web and dispose.

Create Wiki pages with Custom Content

This script took quite a bit more work. SP2010's wiki pages have a few interesteing quirks that separate them from their Publishing or Web Part Page cousins. Firstly, they have no visible web part zone. They're treated more like documents and the content field accepts HTML as content.
Let's start by loading the SharePoint assembly and getting our required objects.

Customise List Permissions

I then needed to break permissions inheritance on the list and remove the site visitors role assignment to prevent access to the pages.
If you've ever worked with SharePoint permissions in code you know just how convoluted they can be. Is use the awesome PowerGui to interrogate the site objects and then drill down to find the required values. It's agreat learning tool and removes hours of guesswork and frustration.

Create the Pages

Now I was ready to create my pages. I used the same groups array I used earlier but you could just as easily create a new array and an indexer to pair it up against your group names. In my case, the the group names already represented suitable page names.
Note the use of the rootfolder path and SPTemplateFileType. I also used the new page's Item property to get a reference to my pages for later.

Create Item-Level Permissions

I then needed to break inheritance for each page and modify it's permissions by adding the required access group, while also reducing the Owners and Members groups access to Read in order to prevent tampering.
I pass $false to the first param because I don't want to copy role assignments. This was also important from a looping perspective to avoid having all the new groups added to subsequent pages. And finally remove the current user (me) by ID just for clarity.

Add Web Parts to Wiki Pages

As stated earlier, this is slightly different to adding web parts to other page types. Wiki pages have a hidden web part zone called WPZ which efeectively lives inside the pages content field. In addition, you're required to add the HTML container markup for the webparts to this field!
This had me chasing my tail for hours because I couldn't work out why the web part maintenance view for my pages showed the web parts but no content was being rendered to my page.
As with adding any web part, we first need to get the LimitedWebPartManager for the page.
Add a ListViewWebPart with Custom View
Once of the tricker requirements was providing a custom view for each page instance that was filtered by the page/group name so users would only see items relevant to them.
While adding a listview web part is quite straightforward, trying to specify or modify a custom view is far less so. As soon as you add a LVWP to a page, a hidden view instance is created on the source list. I found precious few references online as to how to modify this list instance.
The approach I took was to customise the original view's Query property and update it each time. This was fine for my purposes because I then simply deleted the view from the list after implementation.
Also note the format of the GUID for wiki page webparts. For easons unknown this is different the standard GUID format we've come to expect for anything SharePoint. The replace method below shows how to convert it to the correct format.
But of course nothing ever goes as planned. It turned out that the views needed tweaking AFTER page creation. I could have deleted all the pages and started again but I was curious to see if there wasn't a way to somehow get a reference to this elusive hidden view and update it directly.
I again used PowerGui to drill down into the webparts properties to grab what I needed. After getting a reference to the web part I wnated by its Title property, I simply grabbed it's ViewGuid and passed this to my list to get the hidden instance before updating the Query and the view.
Add CEWP with Content
One final requirements was to hide the unrequired Recently Updated list from the Wiki page's QuickLaunch. I'd initially hoped I could embed the required CSS to the page's content field but SharePoint prepends the style with a new class that prevented this.
A little investigation revealed that this web part has a Content property of type XmlElement. So you first need to define a root element and then add your markup to its InnerText node.

Add Content to Wiki Pages

The final step was to add some instructional text to the pages. For this we can simply pass a raw HTML string to the page's WikiField. Th eonly problem I had here was with the quirky syntax used by PowerShell's here-string. The beauty of here-strings is that you don't need to escape them. Just be sure to use the $($myparam) format for any PS vars or they will be interpretted literally.
You can also see the markup required for the web part containers and the less than obvious way to pass in the required GUIDs.
We can finally close our loop and update the page, before updating and disposing of the web. And we're done!
What's that you say? You found a typo in the page content? Oh dear... :{
Never fear. Just as we update the WikiField's value, we can also grab it. Just get a reference to it, then re-provide your new content string and set it again. You can also append new content in the same manner, while leaving the existing content alone.

Wrapping Up

When you put it all together you can see that it all makes perfect sense. Well, mostly. But there were many little catches and caveats along the way that made this exercise take far longer than it should have. A distinct lack of documentation being the usual issue.
That being said, including development time, I knocked out 147 custom pages in less then 3 hours. Esitmate how long it would have taken to do all this through the UI. ;)

Friday, 16 November 2012

SharePoint Page View Counter

In my last post [SharePoint Hit Counter] we looked at a solution to add an ad hoc hit counter to selected lists. While this solution could be tweaked to work with any kind of list, I wanted to look at a more global solution that could track unique visits and store them in a more centralised location.
This solution uses a web control which is referenced in the site's masterpage. When the masterpage is hit, the URL, logged in user and today's date are written to a centralised list. The control itself just renders the number of visits to the current page. The code behind perofrms a check to see if our Sats list exists and creates it if it doesn't.
If your site uses two master pages - one for Site and one for System pages - then you can apply it to one or both. In may case, I only referenced the control in my custom master. The reason for this is that I only wanted to target Publishing Pages. For starters, the URLs cleaner and I already had a separate solution in place for these. Feel free to modify the solution to suit your needs.

The Masterpage

If you already have a master page feature then you can just make the required changes and add the control where you like.
This requires two additions to be made. A tagprefix at the top of your page which registers our tagprefix name and associates it to our assembly code. I used the same namespace as my previous list stats solution for consistency.
And then a reference to your control where you would like to see it appear. I chose to place mine to the right of the breadcrumbs but it would go just as well in the footer.

The List

In this case I chose to add the list creation code into my control class. There are many (possibly better) ways to do this, such as via feature receiver or list template. The benefit to this method is that if the list is ever re-named or deleted (by accident or design), it will be recreated automatically on the next page view.

The Web Control

The web control itself is quite simpe and has no properties or controltemplate for redering. Add the appropriate references and using statements to your project, and a brief description of what it does.
Then can then begin defining our control class. The first thing we need to do in this case is to make sure our list exists and, if not, create it. The following is a useful means of getting a list by name, which allows you to pass in the SPListCOllection of your choice.
We then override the OnLoad method and begin by getting our current context items. Then open our elevated priviliges block, open a new SPWeb object and grab the values we want to capture for the Stats list.
Then we check to see if our list exists and create it otherwise. I also decided to create a custom view and make it the default. I chose to group by Title (URL) and display Page name, Date of view and the User.
And if it does exist, we query it and update the list only if the current user has not hit the page today.
Now all that's left is to override the control's render method and display the view count on the page. The query simply gets all items from the Stats list that match the current URL.

Conclusion

The final result is simple but effective. as in my previous post, you could take this a step further by creating a stats viewer web part. But in my case the custom list view which I added a Total for to the Date field, provided everything I needed.
As usual, all feedback on how you might improve this solution is welcomed.

Thursday, 15 November 2012

SharePoint Hit Counter

If you've ever enabled and viewed the Usage Analysis reports for SharePoint, you'll already know that they're...well, lacking. I was recently requested to provide hit counts for annoucements on a coporate Intranet and rose to the challenge, of which their were many. :)
Now while there are many ways to skin this cat, my requirements were to provide an ad hoc solution that can could be enabled at the list level by anyone with the Manage List permission level.
I have described a different approach [SharePoint Page View Counter] for Publishing Pages that uses a dedicated list, gathers more information, and comprises a custom webcontrol that is then referenced in the site's custom master page.
The following describes a MOSS 2007 solution comprising of two web parts - a Hit Counter and a Hit Viewer - as well as a site column definition, and is deployed as a site level feature. This could be easily modified for 2010.
The end result will look something like this.
Fig.1 - Hit Viewer web part
Fig.2 - Hit Counter web part
I use WSPBuilder for convenience when creating MOSS solutions, so some of these steps will vary.
Start by adding a web part feature to your project called CounterWP. Then add a second web part called HitViewerWP.
Open up your elements.xml file and add the following field definition for the site column.
There are several advantages to deploying the site column. Firstly, it removes any guess work or room for error on behalf of the content author who is adding it to the target list. Secondly, we're able to hide the field from any of the list form views; something you can't really do via the UI.

HitCounter Web Part

Now let's create a class file for the Counter web part. I'm inheriting from System.Web...WebPart class just to reduce the using footprint. First thing I want to do is create my global booleans for an ID key/pair value in the current URL and the presence of the custom Counter field.
The rest of the code is commented pretty heavily but I'll break it up anyway. As we only want the code to run on DispForm pages with an valid ID present, I'm going to check for the ID first in the OnInit event. No point continuing otherwise.
If the ID is there we can then grab the current context in the OnLoad event. Because we're elevating privileges, we want to get our contexts before trying any update methods.
We don't want the counter field to increment when the page is in Edit mode, or the field doesn't exist, so we'll check for both.
Now we can create our elevated privileges block and open a new SPSite instance to re-grab our needed objects. I do an additional (redundant) check for the Counter field because that's how I roll. You could/should just as easily use a try/cach block.
Then check for a null value on Counter and set it to zero. This is important, because if we add the field to a list with potentially hundreds of existing items, we don't want to have to set a value for all of them. New items get the default value of zero, as defined in the field definition.
Notice that I check the field value's object for null. Also important. Checking for counterStr as a null or empty string does not yield the correct result. Don't ask me why.
We then increment the current value by 1 to count the current hit.
Now we're ready to perform our updates. As we're wanting to update the database from a GET request, we need to set allowunsafeupdates to true on the SPWeb.
We're also going to do an additional check for Content Approval on the list object and approve the item at the same time, otherwise we'll leave the updated item in a Pending state.
Here's the rest of our OnLoad code.
Now let's render the results to screen to provide feedback for the user. This is optional but I provide it here for completeness. I used the RenderContents method for future proofing.
And that's it for our CounterWP. Build your project and let's move on to our HitViewerWP.

HitViewer Web Part

Again, I inherit from System.Web.UI and define a couple of globals for error checking and to get/set the user-selected query for the report viewer.
I chose to check and set a few web part properties during initialising. Again, this is optional.
Now add the CreateChildControls method which will call our render code. As I'm rendering multiple controls this method is preferable to the RenderContents method in this scenario.
Our LoadLists function firstly gets the list collection for the current SPWeb and adds the user controls for the view selector dropdown list.
I then chose to add a script block with a little jQuery goodness to collapse the lists on page load, and an onclick event to toggle the items display. If you don't already have a jQuery reference in your masterpage, you will need to add a reference to the library here as well.
").AppendLine(); ]]>
Then begin rendering the container and grab only those lists whose field collection contains our Counter field. No point querying lists that don't and causing performance issues.
Now let's try to grab the list titles and define the queries for the view dropdown list. I provided two options. The default query includes a filter that grabs only those items modified in the last 30 days. The second doesn't discriminate. It will grab all items.
In both cases I've restricted the queries to the first ten items matching the criteria. I also specify only the ViewFields required to render my items. This is again for performance.
I use SPQuery over other methods (such as SPSiteDataQuery and CrossListQueryInfo) because I already have my filtered list collection, and because it offers me more control.
Now we can define the render method for our list items, catch any exceptions, and add our literal control to the page.
Finish off by adding our exception handling function and we're done!

In Closing...

Because we're updating the list item, any list views that order by modified date will be affected. There are pros and cons to this. The pro, is that your list will now be effectively sorting by popularity (most recently viewed items.
If this isn't what you want, or you have the need to keep some items 'sticky', then you have a number of options. Sort the view by Created date, create a new Yes/No column to keep important items at the top...the choice is yours. In my case I did both.
As a final touch you could use a little jQuery to check for a 'yes' value in your 'sticky' field and append a ! to the item's title to make it clear why some items are at the top.
Enjoy! And as usual, I look forward to any feedback.

Thursday, 21 July 2011

SharePoint Dataviews - Items Per Row?

While DVWPs (Dataview Web Parts) undoubtedly rock, styling them has always been a nightmare of wading through line after line of inexplicable XSLT. To make matters worse, MOSS currently only supports v1.0 and uses the DDWRT namespace.

This means you can't rely on many of the online tutorials and examples you might come across when it comes to styling this beast.

Thankfully, SharePoint Designer provides many useful UIs to assist in this area but IMO they still fall short when it comes to something as simple as stating HOW MANY ITEMS PER ROW you might like to display. Which, of course, brings us finally to the point of this post. :)

The plan is to go from this:

NameTitleDescr
Item1First ItemDull
Item2Second ItemDrab
Item3Third ItemBoring
Item4Fourth ItemKill me now

To something like this:

First Item

Wow!

Second Item

Great stuff!

Third Item

Terrific!

Fourth Item

Wizard!

In my search for a solution to this problem I came across a few examples but sadly none of them was able to provide a straightforward example of how to achieve this. To be fair to Microsoft, there is a two column layout which is what provided me with the base code. But it is terribly inflexible and doesn't allow for simple variations. So, pencils at the ready. Here we go...

Let's Get Started

First things first, you want to either convert an existing List View or add a new Dataview from the Insert menu. Add or remove data fields and header rows as you like; apply pagination, limits, sort order...just don't group by anything. That would add an additional complication to what we're trying to achieve.

For the purpose of this exercise accept the default table template as well. i.e. Don't change the layout.

In Source view, look for the opening tag. Then scroll down until until you get to the first template definition.

<xsl:template match="/"...

Breaking Things Down

For the sake of this exercise I will be styling an image library view but the steps will apply to any type of list. The first thing we're going to do is reduce the meat of this template down to its bare essentials like so:

<table id="myID">
    <xsl:call-template name="dvt_1"/>
</table>

The reason for the ID should be obvious. Apart from providing an easy method for applying custom styles, it also facilitates targeting the table with jQuery or any other language. That's our first template down. Now let's move down to the next two for the body. You may not need to make any changes to these but I provide the code below just in case.

<xsl:template name="dvt_1">
    <xsl:variable name="dvt_StyleName">Table</xsl:variable>
    <xsl:variable name="Rows" select="/dsQueryResponse/Rows/Row"/>
    <xsl:call-template name="dvt_1.body">
        <xsl:with-param name="Rows" select="$Rows"/>
    </xsl:call-template>
</xsl:template>
<xsl:template name="dvt_1.body">
    <xsl:param name="Rows"/>
    <xsl:for-each select="$Rows">
        <xsl:call-template name="dvt_1.rowview"/>
    </xsl:for-each>
</xsl:template>

Using Custom Templates and Variables

Now because this is an image library I've created an additional template to define the thumbnails. While this won't be relevant for other lists I include it as an example of how you can easily create your own custom variables and see how they can be applied. Skip this if you already know.


<xsl:template name="getThumb">
    <xsl:param name="str"/>
    <!-- Default file type for MOSS preview pics -->
    <xsl:variable name="ext">.jpg</xsl:variable>
    <xsl:choose>
        <xsl:when test="contains($str, $ext)">
            <xsl:value-of select="substring-before($str, $ext)"/>
            <!-- Default suffix for MOSS preview pics -->
            <xsl:text>_jpg.jpg</xsl:text>
            <xsl:call-template name="getThumb">
              <xsl:with-param name="str" select="substring-after($str, $ext)"></xsl:with-param>
            </xsl:call-template>
        </xsl:when>
        <xsl:otherwise>
            <xsl:value-of select="$str"></xsl:value-of>
        </xsl:otherwise>
    </xsl:choose>
</xsl:template>

Now For the Fun Part

Now we need to define the contents of our rows. While this looks needlessly complicated (it is!) I've included comments to help break it down. If it still doesn't make sense, don't worry about it. It works. Trust me, ok? ;)

You can either modify the existing template, which will already contain your preferred data fields, or delete everything and start from scratch. You can, of course, omit the imageThumb variable and just start with the opening table cell.

<xsl:template name="dvt_1.rowview">
    <!-- Create a variable to hold our thumbnail template. -->
    <xsl:variable name="imageThumb">
        <!-- Call our template -->
        <xsl:call-template name="getThumb">
            <xsl:with-param name="str" select="@NameOrTitle"></xsl:with-param>
        </xsl:call-template>
    </xsl:variable>
    <!-- START HERE!!
            Now we define our row. You can define this content any way you want
            within the confines of the table cell. Just use the appropriate
            field names and layout for your list. -->
    <td>
        <!-- Title field -->
        <h4><xsl:value-of select="@Title"/></h4>
        <!-- Create a hyperlink to the item display form and wrap it
                around our thumbnail. -->
        <a href="{@FileRef}" 
              title="{@Title}: {@Description}" 
              name="{@Title}">
            <img src="/{@FileDirRef}/_t/{$imageThumb}" alt="{@Description}" />
        </a>
    </td>
    <!-- This is where the magic happens. We use the mod function to define how
            many items we'd like per row. We then test for our position (item
            count)in the data array. If it's not a multiple of our number then
            we ignore it and continue rendering our item template. If it is,
            we close the row and start a new one. -->
    <xsl:if test="position() mod 4 = 0" ddwrt:cf_ignore="1">
        <xsl:text disable-output-escaping="yes"></tr></xsl:text>
        <xsl:if test="position() != last()" ddwrt:cf_ignore="1">
            <xsl:text disable-output-escaping="yes"><tr></xsl:text>
        </xsl:if>
    </xsl:if>
    <!-- Don't forget to apply your item count here too! -->
    <xsl:if test="position() = last()" ddwrt:cf_ignore="1">
        <xsl:if test="position() mod 4 != 0" ddwrt:cf_ignore="1">
            <!-- This never seems to gets called but is somehow required anyway! -->
           <span></span>
        </xsl:if>
    </xsl:if>
</xsl:template>

And that's it! Hope you found this useful.

Tuesday, 20 October 2009

SharePoint Advanced Search Properties Don't Work - the skinny on Created By, Modified By, Author and more

If you've ever done much work with the Advanced Search web part it won't take you long until you discover that most of the default property searches simply don't work. As of writing this post there were dozens of very long forum threads describing the problem with no comprehensive fix in sight by Microsoft or anyone else. Perhaps until now...?

What's not Broken?

Despite the claims of many, the Size and (Created/Modified)Date properties are not completely broken. They simply require the correct format for input. It's also worth noting that the explicit value is required when using property searches. No wildcards or partials!
  • Size - takes a value in bytes, so that 1000000 = 1Mb.
  • Dates - require a xx/xx/xxx or xx/xx/xxxx format. The order of the days/months will depend on your regional settings. If you're still getting no results, then check the metadata mappings and XSLT references below.
Although uncovering the correct format was pain, it was nothing compared with what followed.

What is Broken and Why?

As many have discovered, the Created By, Last Modified By, Created Date, Last Modified Date and Author properties are all affected to some degree.
The root cause for all this is down to improper mapping of crawled properties to their managed properties within Search Administration and keeps going all the way through to the XSLT for the Advanced Search and Search Core Results web parts.
The reasons behind these poor relationships become obvious shortly after you begin looking for a solution. To be frank, it also becomes obvious why most people gave up trying!

Thanks to...

Much thanks goes Anne Stenberg and her 6-part series entitled - Mystery Solved - Crawled Properties in SharePoint.
In this series Anne patently and painstakingly goes through every last property in each defined category, and providing a description for many. I'm not entirely sure where she came by all this information but it proved invaluable when it came to identifying and testing the result of many changes to come in my metadata property mapping.

Please explain!

Using a combination of Anne's tables, the U2U CAML Query Builder feature, the ever useful SharePoint Manager, and the XSLT within the search web parts - it quickly becomes obvious that it's going to take more than a packet of off-the-shelf headache tablets to get through this.
Without going into too much detail - ignorance being bliss - let's take a look at something as simple as Author.
  • We have a visible Author column whose internal name is _Author.
  • A hidden Created By column whose internal name is Author.
  • And a managed property called Author that seems to want to hedge its bets by trying to cover all these bases as well as a few more.
But that that's just the beginning - Created By and Modified By searches will invariably return zero results and also have their fair share of possible mappings and hidden values. What the heck is Write anyway?? Apparently just another value for Modified Date...but more on that later. I'm sure anyone's who's that interested can do their own research. I won't bore everyone alse any further.

What's the fix already!?

OK, OK. Keep your propeller hat on.
After days of stuffing around, tweaking mappings, modifying web part properties and performing a full crawl each time(!) I have finally found - I think - a solution. At least, a number of searches - using Author, Created By and Last Modified By properties with the AND operator - all returned correct results.
It's also worth noting that this solution is not Office-centric and will work with any document type.

First, the Metadata

This assumes a good knowledge of Central Administration. If you require detailed steps they can be fond elsewhere.
You can use all or some of the settings shown below but the only ones that really matter are the Mappings themselves. After you've added the crawled properties, be sure to click each one and check the "Include values for this property in the search index" checkbox, otherwise it won't get added to the index! In all cases I went with the default "Inlcude values from all crawled properties mapped" option.
Also note that there are often TWO properties with exactly the same name - e.g. Office:4(Text). Picking the right one is essential and I have provided the Property Set IDs below where this is relevant.
And, remember, what follows is in no way Gospel - it's just what worked for me.
NB: Don't forget to run a Full Crawl after making these changes.
Property Name
Type
May be deleted
Use in scopes
Mappings
AuthorTextNoYes_Author(Text), ows__Author(Text)
CreatedDate and TimeYesNoOffice:12(Date and Time), Basic:15(Date and Time)
CreatedByTextYesNoOffice:4(Text), ows_Created_x0020_By(Text
LastModifiedTimeDate and TimeNoYesBasic:14(Date and Time), Basic:16(Date and Time), ows_Modified(Date and Time)
ModifiedByTextYesYesOffice:8(Text)
  • Office:12(Date and Time) - f29f85e0-4ff9-1068-ab91-08002b27b3d9
  • Basic:15(Date and Time) - b725f130-47ef-101a-a5f1-02608c9eebac
  • Office:4(Text) - f29f85e0-4ff9-1068-ab91-08002b27b3d9
  • Office:8(Text) - f29f85e0-4ff9-1068-ab91-08002b27b3d9

Advanced Search XSLT

The following go in PropertyDefs. There are many default values here, I'm just providing the full block. You'll then need to add the same 'Name' references to each ResultType in the order you prefer.
<propertydef name="Author" datatype="text" displayname="Author">
<propertydef name="Size" datatype="integer" displayname="Size">
<propertydef name="Keywords" datatype="text" displayname="Keywords">
<propertydef name="CreatedBy" datatype="text" displayname="Created By">
<propertydef name="Created" datatype="datetime" displayname="Created Date">
<propertydef name="ModifiedBy" datatype="text" displayname="Last Modified By">
<propertydef name="LastModifiedTime" datatype="datetime" displayname="Last Modified Date">

Search Core Results XSLT

Unless you're trying to provide custom results using some of the values described above you won't need to make any changes here. Quite frankly it's a little daunting but great things can be done - such as displaying Size, Author and a custom link to open the containing folder for each result. I'll probably leave this for another post as it's a topic in itself.

In conclusion...

So, hopefully, if you've done everything right and performed a full crawl, you should now be able to search using one or all of the properties we've discussed here.
One thing you may find still doesn't work is the "Does not equal" operator. You might also that it's not described anywhere in the web part code but is managed by a separate core JavaScript file. I'm just not willing to look into this right now - or the reasons why "Contains" and "Doesn't contain" aren't available for partial search term querying. If anyone else has any ideas - performance notwithstanding - feel free to drop me a line.
I look forward to any further insight and feedback others might have and hope that all my hard work isn't undone with the next upgrade!

Thursday, 16 July 2009

Deleting unused SharePoint Content Types

It happens to every site collection admin at some stage. For whatever reason you're required to replace a defunct content type in your document library with a new one. Adding t he new one is easy. But then you try to delete the old one and receive the terrifically informative "Content Type is still in use" error.

You've tried everything:

  • Updated any files you could find with the new content type.
  • Checked every file twice to make sure you didn't miss any.
  • Managed Checked Out files to locate all those tricky 'hidden' docs that haven't been checked in yet and only exist in some funky temp/draft state! (You'll either need to take ownership of these as site collection admin, or email a list to the original authors to get them to check them in or delete then. I can't believe there is no option to delete these en masse!)
  • Run custom CAML queries to make REALLY sure you didn't miss any (using the fabulous U2U SahrePoint CAML Query feature - www.u2u.be/res/Software.aspx.
  • Written a console app just to make ABSOLUTELY sure you didn't miss any.
  • Emptied the recycling bin - both of them!

But still the error persists.

Just when I thought I'd exhausted all options it occurred to me that perhaps versions were the culprit. Looking at the version history for a few suspect documents confirmed that they were.

Running the following SQL query will find them all.

SELECT *
FROM AllUserData
WHERE (tp_DirName LIKE '%sitename/Shared Documents%')
AND ((tp_ContentType = 'myDodgyCT'))
ORDER BY tp_DirName

This should be self-explanatory but tp_dirName is just using a relative path from the domain to the library. And ContentType is the explicit name of the content type to search on.

From there I just exported the results to Excel, filtered out duplicates and was left with a workable list of documents. Simply publish a final version of each (if required), then go to the Version History and delete all previous versions.

You could do all this in one step by turning off versioning for the library and then turning it on again. But this would delete versions for ALL documents. A good trick to remember when you're site quota is reached.

Good luck!

Thursday, 18 June 2009

Import Publishing Sub Site as Site Collection

Any farm administrator will eventually feel the need to restructure when they watch in horrid fascination as their once insignficant sub sites grow to monster sites. Current MOSS limitations such as the 15GB limit for the stsadm import/export operations mean that unless you're using a terrifically expesnive 3rd party migration app you need to act fast before things get out of hand

The obvious solution would be to export the site(s) in question and then reimport them to an empty site collection. Yes, that would seem obvious, but it's not quite as straightforward as you might assume. What really compounds the matter is if you happen to have enabled the publishing feature on your source site.

After many days of disappointment and incoherent error messages I finally returned to Gary Lapointe's brilliant blog - SharePoint Automation. Gary has developed dozens of custom stsadm commands in order to fill the cavernous and aching void left by the default offerings. One thread in particular, Subsite to Site Collection, deals with this pesky issue and also includes his own hair-pulling frustration with the out-of-the-box limitations to achieve this. BTW, the solutions he provides are not strictly supported - but they work where nothing else will and are very easy to use.

In my case I simply deleted every failed test and started from scracth using the gl-convertsubsitetositecollection operation. All I had to do was provide the source site and destination URL and the command did the rest.

stsadm -o gl-convertsubsitetositecollection -sourceurl http://mydomain.com/source -targeturl http://mydomain/destination -nofilecompression -owneremail me@home.com -ownerlogin "mydomain\me" -nositetemplate 

This exports the source site, creates the destination site collection and site (based on source site template), and activates any features. The source site was a Team Site with publsihing and several custom features enabled. The only thing I did was create the managed path but this command even has a flag for that!

The only thing it doesn't appear to do (yet) is allow you to specify a new content database. I've already put my request in for this and hope to see it soon. If this helps someone, please thank Gary - not me! :)