Thursday, October 24, 2013
Content deployment gotchas
Monday, September 16, 2013
Fixing missing Hidden and Sealed attributes in Site columns
After a recent upgrade to sharepoint 2010, we were no longer able to create new site templates. Creating a template would fail with an error:
Unexpected System.InvalidOperationException: Error copying temporary solution file to solutions gallery: _catalogs/solutions/TestProduction.wsp at Microsoft.SharePoint.SPSolutionExporter.ExportWebToGallery
Digging a little deeper into the ULS logs, I was able to find a better explaination of the error:
SharePoint Foundation General 9fjj Monitorable SPSolutionExporter: Microsoft.SharePoint.SPException: Feature definition with Id 10a563f6-0afb-4d38-9ddc-241c61694ac6 failed validation,
file 'TestProductionListInstances\ElementsFields.xml', line 39, character 167:
The 'Hidden' attribute is invalid - The value '' is invalid according to its datatype 'http://schemas.microsoft.com/sharepoint/:TRUEFALSE' - The Enumeration constraint failed. at Microsoft.SharePoint.Administration.SPSolutionPackage.SolutionFile.FeatureXmlValidationCallBack
Something was wrong with our columns. The "Hidden" parameter was blank instead of true or false. Looking into the database confirmed the error. The xml schema of the column definition had : Hidden="" instead of Hidden="FALSE". I also noticed that "Sealed" was also blank, and would cause the same error when trying to save the site as a template. The error describes the elementfields.xml file. This file is part of the site template WSP file. To view the file, go to the solutions gallery and download the Site template WSP that gets created. Change the extension to a CAB and open it. You will find the ElementFields.xml file and can use the line and character from the ULS log error to find the field that causes the problem. In my case, it was about 40 differnet fields.
These were errors on the site columns, many but not all of them being custom columns we created. Sealed and Hidden are not things that are easily changed through the UI, so I had to use Powershell to update the columns. I probably could have used SP Manager too, but since there were 40+ columns duplicated over a few different site collections, it was easier to use powershell.
Updating the Sealed column is fairly easiy since it is a property on a column that can be set directly. When trying to change the 'Hidden' property, I would get the following error:
PS C:\> $f = $web.Fields["Meeting Location"]
PS C:\> $f.Hidden = $false
Exception setting "Hidden": "Cannot change Hidden attribute for this field"
At line:1 char:4
+ $f. <<<< Hidden = $false
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : PropertyAssignmentException
So, if I can't change the field, how do I change it?. The answer is to change the xml schema. sharepoint will let you change the schema directly and skip around whatever check it has in place.
$field = $Web.fields[$i]
[XML]$schema = $field.schemaxml
$field.schemaxml = $schema.Innerxml
$field.update()
The next problem, how do you know what fields need to be changed? If you just check for $field.hidden -eq "" or something like that it will not work. It will tell you that the field is "FALSE" even when it is blank! So, we have to look at the xml:
[XML]$schema = $field.schemaxml
if ( $schema.InnerXML.Contains("Hidden=""""")){
do something}
All together the script looks like this:
$site = Get-SPSite "http://YourSiteCollectionURL"
$Web =$site.RootWeb
for($i = $Web.Fields.count -1; $i -ge 0; $i--)
{
$field = $Web.fields[$i]
[XML]$schema = $field.schemaxml
if ( $schema.InnerXML.Contains("Hidden=""""")){
$schema.field.Hidden = "FALSE"
$field.schemaxml = $schema.Innerxml
Write-Host $field "HIDDEN: " $schema.field.Hidden
$field.update()
}
if ( $schema.InnerXML.Contains("Sealed=""""")){
Write-Host $field "Sealed: " $schema.field.Sealed
$field.Sealed = "FALSE"
$field.update()
}
if ( $schema.InnerXML.Contains("Required=""""")){
Write-Host $field "Required: " $schema.field.Sealed
$field.Required = "FALSE"
$field.update()
}
}
This script will set all the hidden and sealed fields to FALSE for the site columns that have the field as blank. Another way to do this is to modifiy the database directly, but that isn't supported and you can easily break things.
I hope this can help someone else who runs into the same problem. I wasn't able to find a good solution on the web. MSFT support(wipro) did help, but it took them almost 2 months, and 3 different support techs to come up with a script that I then had to extensivly modify to get to actually solve the problem.
Tuesday, June 4, 2013
Sum Grouped Calculated Columns in SharePoint
The view had items grouped by user and a total number of hours for that user. This the hours was computed by a check in time and check out time. Sharepoint does not provide a way to calculate a total for calculated fields.
Most solutions tell you to use a dataview webpart to do this. Our old solution worked that way, but it was a hassle to add new views and re-use the code in other projects.
My solution was to throw out the old code and re-build it using out of the box, easy to create views and some javascript/jQuery magic.
The following code will find items by group, and compute a total for that group.
The javascript has two variables:
COLUMNTITLE: The name of the column you would like to find the total of all the rows
TEXTHEADER: The text you would like to show in front of the total. This cannot be blank, and should be some unique text. It is also used to tell if the total has already been calculated and displayed.
This code will automatically display totals for groups that have been expanded. If the group has not been expanded, it cannot show the total because SharePoint loads the data using ajax. When the group is expanded, the total will be calculated and displayed.
This works on a SharePoint 2007 site upgraded to 2010, but still using the 2007 UI. I haven't tested this in other scenarios. The idea should be the same, some of the css tags might change. YMMV.
var TEXTHEADER = "Total Hours: ";// text to put in front of the total
var COLUMNTITLE = "TotalBillableTime";// which column of data do you want to sum up?
var COLUMNNUMBER =0; // placeholder variable.
//alert("found:"+FindColumnNumber(COLUMNTITLE));
$(function() {
// attach to clicks to load the data when the user clicks to expand any closed sections
COLUMNNUMBER = FindColumnNumber(COLUMNTITLE);
AttachClicksToComputeTotalTime();
// find the loaded blocks of data and populate the header
// this can take some time, so we're calling it after a delay.
// better method would be to attach to the ajax complete call of sharepoint if there is one.
setTimeout(function(){FindLoadedBlocks();},1000);
});
function FindLoadedBlocks(){
// search for all tbody's that have tbod in the id and have isLoaded = true
// compute the totals for these
$('tbody[id^="tbod"][isLoaded="true"]').each(function($this){
ComputeBlock(this.id);
});
}
function AttachClicksToComputeTotalTime(){
// find all the header links that the user clicks to expand the data.
// add an onClick event to compute and display the totals
$("td.ms-gb2 a").click(function($this){
FindBlock(this);
});
}
function FindBlock(Headerblock){
// given a header, find it's child tbody and wait until it is fully loaded to compute its data
var parentSectionID = Headerblock.parentNode.parentNode.parentNode.id;
var childSection = parentSectionID.replace("titl","tbod")+"_";
var kidSection = $("#"+childSection);
if($(kidSection).text().length<100 br=""> setTimeout(function(){FindBlock(Headerblock);},50);
return;
} else{
// it's done, let's load it.
ComputeBlock(childSection);
}
}
function ComputeBlock(blockName){
// for a block of data rows, compute the total and insert it into the header
var parentSectionID = blockName.replace("tbod","titl").replace("__","_");
// when groups are expanded by default, a second tobdy is created
// check if this tobody has data, if not, go to the next
if($('#'+blockName).html().length<100 br=""> var block = $('#'+blockName).next();
}else{
var block = $('#'+blockName);
}
var rowSum = 0;
childRows = $(block).find("tr").each(function(){
// find the 6th column
var x = $(this).find(":nth-child("+COLUMNNUMBER +")");
var rowVal = parseFloat($(x).text().trim());
if(!isNaN(rowVal)){
rowSum+= rowVal;
}
});
rowSum = rowSum.toFixed(2); // trim it to 2 decimal places
// find the parent section
var ParentSection =$("#"+parentSectionID+" tr td");
// check that the parent section doesnt' already have the totals
if($(ParentSection).text().indexOf(TEXTHEADER)>0){
}
else{
$(ParentSection).append(" "+TEXTHEADER +" "+rowSum+"");
}
}
function FindColumnNumber(ColumnTitle){
// find the column by name and figure out where it is.
var pos = 0;
var foundVal = 0;
$(".ms-vh2").each(function(){
pos++;
if($(this).text() == ColumnTitle){
foundVal = pos;
}
});
return foundVal;
}100>100>
each group section will look something like this: