Querying an SPFieldMultiLineText field for an exact line match with LINQ

Today, I had to come up with a way to return a value from one column in a SharePoint list based on finding an exact line match of a user’s query terms in an SPFieldMultiLineText field in the same list. To clarify, here’s an example of how the data looked like in the SharePoint list (this is just sample data):

Term Synonyms
SharePoint SharePoint 2010
SP2010
SharePoint 2007
Microsoft Office SharePoint Server
MOSS
Windows SharePoint Services
SharePoint Foundation
SharePoint Server
FAST Search FAST ESP
FAST Search for Internet Sites
FAST Search for SharePoint
FSIS

In the UI provided, if a user looked for the term ‘Microsoft Office SharePoint Server’, I needed to return back the ‘SharePoint’ term (first item, first column in the table).

There were three options I could think of to accomplish this:

  1. CAML query.I ruled this out fairly quickly. Unfortunately, I had to do an exact match on the term that the user entered. So, for instance, if the user entered the term ‘Windows SharePoint Services’, then the first term (SharePoint) would be returned. However, if the user only entered the term ‘Windows’, the ‘SharePoint’ item shouldn’t be returned because it was not an exact match. With CAML, there was really no way for me to specify an exact match per line. I couldn’t use <Eq> because that would necessitate the entire Synonyms field to match what the user entered. I also couldn’t use <Contains> as that would result in false positives. None of the other comparison operators seemed appropriate either.
  2. For-each loop. Of course, i could have iterated through the items one by one and examine the Synonyms field. For each item in the list, I’d have to examine the Synonyms line-by-line to find the exact match. This was originally what I planned on doing, however, it felt inelegant and potentially inefficient, especially because this list is going to contain many items.
  3. LINQ. I didn’t know exactly how I could use LINQ to tackle this but I always felt like I could. So this is the path I attempted. Obviously, it worked or I wouldn’t be writing this blog post :).

Below is the code I wrote to query the field for an exact line match:

 

 1: public string GetTerm(string userSearchTerm)
 2: {
 3:     using (SPSite site = new SPSite("http://mysite"))
 4:     {
 5:         using (SPWeb web = site.OpenWeb("myweb"))
 6:         {
 7:             SPListItemCollection items = web.Lists["mylist"].Items;
 8:  
 9:             string[] delimiters = new string[] { "\r\n" };
 10:  
 11:             var term = (from e in items.OfType<SPListItem>()
 12:                         where ((string)e["Term"]).Equals(userSearchTerm, StringComparison.InvariantCultureIgnoreCase) 
 13:                         || ((string)e["Synonyms"]).ToLower().Split(delimiters, StringSplitOptions.RemoveEmptyEntries).Contains(userSearchTerm.ToLower())
 14:                         select e).FirstOrDefault();
 15:  
 16:             if (term != null)
 17:                 return (string)term["Term"];
 18:             else
 19:                 return null;
 20:         }
 21:     }
 22: }

The code isn’t terribly difficult but a few lines probably warrant some explanation. The first thing to understand is how the values are stored in an SPFieldMultiLineText field. Each line is in the field is split using a carriage return/line-feed combination (\r\n). So if I wanted the value of the second item in the list, the value would be represented as FAST ESP\r\nFAST Search for Internet Sites\r\nFAST Search for SharePoint\r\nFSIS. Line 9 sets up the pattern that I’m going to be looking for to treat each line as a separate item.

Lines 11-14 is the LINQ code. In line 11, I first need to convert the items object into a generic, IEnumerable<T> representation of the collection. Even though the SPListItemCollection class implements IEnumerable and LINQ is supposed to work with IEnumerable objects, LINQ didn’t seem to work directly with the SPListItemCollection. It’s almost like I needed to give LINQ a hint what type of items the collection held.

Line 12 checks for an exact match of the user’s query terms to the term stored in the SharePoint list itself. For example, if the user searches for FAST Search, then the second item in the list better be returned.

Line 13 is where I handle the Synonyms multiline field. The first thing I do is convert the entire string in the field to lowercase, then I split the value of that into an array using the ‘\r\n’ combination as the delimiter. I then just need to call the Contains() method to see if the term the user entered is in the array and return the SharePoint list item if it is or null if it isn’t (Line 14 – call to FirstOrDefault()).

That’s pretty much it. Not hard stuff but I think a pretty useful technique.

SharePoint 2010 Download as Zip File Custom Ribbon Action

So I was bored and decided I wanted to learn a little more about the new SharePoint 2010 UI Framework. There are some really cool things you can do with the new framework and I wanted to put some of them to use but at the same time create something that’s useful for other people. I remember a while back someone asking me to write code for MOSS 2007 that would allow users to download files down to their computer as a zip file because they didn’t like having to download each file one by one. For some reason or another, I never got around to it – I think I told the person that they could download multiple files from SharePoint using Windows Explorer and, although it wasn’t exactly what they were looking for, it at least got the job done. But I decided recently that this might not be a bad enhancement to add to the UI in 2010 so that’s what I set out to build today.

The first thing to look at is my Visual Studio 2010 project. I will discuss all the important files that are part of this project. Below is a screenshot of my project:

SolutionExplorer

The first thing I did was to create a new SharePoint 2010 Empty SharePoint Project called DeviantPoint.DownloadZip. Then, I added a reference to ICSharpCode.SharpZipLib.dll which is a part of SharpZipLib(a .NET library that is well-used for working with Zip files). I then created a helper class, ZipBuilder, used to actually create the zip file. I created this helper class with the intent that I could reuse this elsewhere, not just for this project.

Basically, when an instance of this class is constructed, you need to pass in a stream that will be used to write the contents of the zip file to. This could be any type stream (FileStream, MemoryStream, etc). There are a couple of helper methods in this class that allow you to add files and folders and method that “finalizes” the zip file. This Finalize() method must always be called to ensure that the zip file is written out correctly. This class also implements the IDisposable pattern since it is handling streams.

This is the code for the ZipBuilder class:

ZipBuilder.cs

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.IO;
   6: using ICSharpCode.SharpZipLib.Zip;
   7: using ICSharpCode.SharpZipLib.Core;
   8:  
   9: namespace DeviantPoint.DownloadZip
  10: {
  11:     public class ZipFileBuilder : IDisposable
  12:     {
  13:         private bool disposed = false;
  14:  
  15:         ZipOutputStream zipStream = null;
  16:         protected ZipOutputStream ZipStream
  17:         {
  18:             get { return zipStream; }
  19:  
  20:         }
  21:  
  22:         ZipEntryFactory factory = null;
  23:         private ZipEntryFactory Factory
  24:         {
  25:             get { return factory; }
  26:         }
  27:  
  28:  
  29:         public ZipFileBuilder(Stream outStream)
  30:         {
  31:             zipStream = new ZipOutputStream(outStream);
  32:             zipStream.SetLevel(9); //best compression
  33:  
  34:             factory = new ZipEntryFactory(DateTime.Now);
  35:         }
  36:  
  37:         public void Add(string fileName, Stream fileStream)
  38:         {
  39:             //create a new zip entry            
  40:             ZipEntry entry = factory.MakeFileEntry(fileName);
  41:             entry.DateTime = DateTime.Now;
  42:             ZipStream.PutNextEntry(entry);
  43:  
  44:             byte[] buffer = new byte[65536];
  45:  
  46:             int sourceBytes;
  47:             do
  48:             {
  49:                 sourceBytes = fileStream.Read(buffer, 0, buffer.Length);
  50:                 ZipStream.Write(buffer, 0, sourceBytes);
  51:             }
  52:             while (sourceBytes > 0);
  53:  
  54:  
  55:         }
  56:  
  57:         public void AddDirectory(string directoryName)
  58:         {
  59:             ZipEntry entry = factory.MakeDirectoryEntry(directoryName);
  60:             ZipStream.PutNextEntry(entry);
  61:         }
  62:  
  63:         public void Finish()
  64:         {
  65:             if (!ZipStream.IsFinished)
  66:             {
  67:                 ZipStream.Finish();
  68:             }
  69:         }
  70:  
  71:         public void Close()
  72:         {
  73:             Dispose(true);
  74:             GC.SuppressFinalize(this);
  75:         }
  76:  
  77:         public void Dispose()
  78:         {
  79:             this.Close();
  80:         }
  81:  
  82:         protected virtual void Dispose(bool disposing)
  83:         {
  84:             if (!disposed)
  85:             {
  86:                 if (disposing)
  87:                 {
  88:                     if (ZipStream != null)
  89:                         ZipStream.Dispose();
  90:                 }
  91:             }
  92:  
  93:             disposed = true;
  94:         }
  95:     }
  96: }

The next thing I wrote was SPExtensions.cs, a class for adding extension methods to some of the Microsoft.SharePoint objects. This class basically just adds a few simple helper methods to the SPListItem class and the SPList class. For the SPListItem class, I just added a method to determine if the SPListItem instance is actually a folder and for the SPList class, I added a method to determine if the list is actually a document library.

The code for SPExtensions is below:

SPExtensions.cs

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Runtime.CompilerServices;
   6: using Microsoft.SharePoint;
   7:  
   8: namespace DeviantPoint.DownloadZip
   9: {
  10:     public static class SPExtensions
  11:     {
  12:         public static bool IsFolder(this SPListItem item)
  13:         {
  14:             return (item.Folder != null);
  15:         }
  16:  
  17:         public static bool IsDocumentLibrary(this SPList list)
  18:         {
  19:             return (list.BaseType == SPBaseType.DocumentLibrary);
  20:         }
  21:     }
  22: }

The next thing I did was to add a SharePoint Mapped Folder to my project mapping to the Layouts directory located in the SharePoint root. When you add a mapped folder, Visual Studio will automatically create a sub-folder in that mapped folder with the same name as your project. This is a good thing as you don’t want to be mixing up all your project files with all of the files that come out of the box from SharePoint.

After I had my Layouts mapped folder and subfolder created, I added a SharePoint 2010 Application Page item to the sub-folder called DownloadZip.aspx. The purpose of this application page is to actually handle the request from the client to build the zip file and send it back down to the client. Having an application page handle this is the same technique that is used with the ‘Download a Copy’ action button you see in the SharePoint 2010 ribbon. Basically, a POST request from a client is sent to my DownloadZip.aspx page and this page takes care of packaging up a zip file and sending it down to the client’s browser. This page expects two parameters:

  • sourceUrl –the full url of the document library (and folder, if inside of a subfolder) where the request is being made from
  • itemIDs – a semi-colon delimited list of the SPListItem IDs that should be included as part of the zip file. Note that folders also have ids so if a folder is selected, that folder’s id would also be sent.

The code-behind for this application page basically takes the list of item ids and for each item id, goes and grabs the corresponding file from the document library in SharePoint and, using the ZipBuilder class, packages it up as a zip file. If one of the items that was selected is actually a folder, it will create that folder in the zip file as well and put all the items that are in that SharePoint folder into the corresponding zip file folder. It will also traverse through all the sub-folders in the hierarchy.

Below is the code-behind for the DownloadZip.aspx application page (there is nothing I added to the Download.aspx file itself):

DownloadZip.aspx.cs

   1: using System;
   2: using System.IO;
   3: using System.Web;
   4: using Microsoft.SharePoint;
   5: using Microsoft.SharePoint.WebControls;
   6:  
   7: namespace DeviantPoint.DownloadZip.Layouts.DeviantPoint.DownloadZip
   8: {
   9:     public partial class DownloadZip : LayoutsPageBase
  10:     {
  11:         protected void Page_Load(object sender, EventArgs e)
  12:         {            
  13:             string fullDocLibSourceUrl = Request.Params["sourceUrl"];
  14:             if (string.IsNullOrEmpty(fullDocLibSourceUrl)) return;
  15:  
  16:             string docLibUrl = fullDocLibSourceUrl.Replace(SPContext.Current.Site.Url, "");
  17:  
  18:             SPList list = SPContext.Current.Web.GetList(docLibUrl);
  19:             if (!list.IsDocumentLibrary()) return;
  20:  
  21:             string pItemIds = Request.Params["itemIDs"];
  22:             if (string.IsNullOrEmpty(pItemIds)) return;
  23:  
  24:             SPDocumentLibrary library = (SPDocumentLibrary)list;
  25:  
  26:             string[] sItemIds = pItemIds.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
  27:             int[] itemsIDs = new int[sItemIds.Length];
  28:             for (int i = 0; i < sItemIds.Length; i++)
  29:             {
  30:                 itemsIDs[i] = Convert.ToInt32(sItemIds[i]);
  31:             }
  32:  
  33:             if (itemsIDs.Length > 0)
  34:             {
  35:                 using (MemoryStream ms = new MemoryStream())
  36:                 {
  37:                     using (ZipFileBuilder builder = new ZipFileBuilder(ms))
  38:                     {
  39:                         foreach (int id in itemsIDs)
  40:                         {
  41:                             SPListItem item = library.GetItemById(id);
  42:                             if (item.IsFolder())
  43:                                 AddFolder(builder, item.Folder, string.Empty);
  44:                             else
  45:                                 AddFile(builder, item.File, string.Empty);
  46:                         }
  47:  
  48:                         builder.Finish();
  49:                         WriteStreamToResponse(ms);
  50:                     }
  51:                 }
  52:             }
  53:  
  54:         }
  55:  
  56:         private static void AddFile(ZipFileBuilder builder, SPFile file, string folder)
  57:         {
  58:             using (Stream fileStream = file.OpenBinaryStream())
  59:             {
  60:                 builder.Add(folder + "\\" + file.Name, fileStream);
  61:                 fileStream.Close();
  62:             }
  63:         }
  64:  
  65:         private void AddFolder(ZipFileBuilder builder, SPFolder folder, string parentFolder)
  66:         {
  67:             string folderPath = parentFolder == string.Empty ? folder.Name : parentFolder + "\\" +folder.Name;
  68:             builder.AddDirectory(folderPath);
  69:  
  70:             foreach (SPFile file in folder.Files)
  71:             {
  72:                 AddFile(builder, file, folderPath);
  73:             }
  74:  
  75:             foreach (SPFolder subFolder in folder.SubFolders)
  76:             {
  77:                 AddFolder(builder, subFolder, folderPath);
  78:             }
  79:         }
  80:  
  81:         private void WriteStreamToResponse(MemoryStream ms)
  82:         {
  83:             if (ms.Length > 0)
  84:             {
  85:                 string filename = DateTime.Now.ToFileTime().ToString() + ".zip";
  86:                 Response.Clear();
  87:                 Response.ClearHeaders();
  88:                 Response.ClearContent();
  89:                 Response.AddHeader("Content-Length", ms.Length.ToString());
  90:                 Response.AddHeader("Content-Disposition", "attachment; filename=" + filename);
  91:                 Response.ContentType = "application/octet-stream";
  92:  
  93:                 byte[] buffer = new byte[65536];
  94:                 ms.Position = 0;
  95:                 int num;
  96:                 do
  97:                 {
  98:                     num = ms.Read(buffer, 0, buffer.Length);
  99:                     Response.OutputStream.Write(buffer, 0, num);
 100:                 }
 101:  
 102:                 while (num > 0);
 103:  
 104:                 Response.Flush();
 105:             }
 106:         }
 107:     }
 108: }

After creating the application page, I added a SharePoint 2010 Empty Element item called DownloadZip to my project. This is nothing more than an Elements.xml file that takes care of adding my custom action to ribbon (CustomAction.Location=”CommandUI.Ribbon”). By default, for document libraries, this is what the ribbon looks like:

image

I wanted to add my action inside of the area in the Documents tab, inside of the Copies group so to do this, for the CommandUIDefinition, I set the Location attribute to "Ribbon.Documents.Copies.Controls._children”. I also wanted it to appear right after the Download a Copy action so for the Button element’s Sequence attribute, I set the value to 15 (the Download a Copy button has a sequence of 10 and the Send To button has a sequence of 20 so I needed to set the sequence of my button to something in between). To understand where everything is placed and what the sequences are by default, you need to look at the file C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\GLOBAL\XML\CMDUI.xml. I also specified the icons I wanted to use for my action (these icons are also part of my project, located in a sub-directory of the Images SharePoint mapped folder) and I also set the TemplateAlias to “o1” so that my icon shows up large like Download a Copy does. I also define the actual command handler in this Elements.xml file by adding a CommandUIHandler element. The CommandAction attribute is used to specify what exactly the button is supposed to do and the EnabledScript attribute is used to determine whether or not the button/command is enabled. These two attributes’ values both point to javascript functions I define in a separate file (discussed later). Because I’m using a separate javascript file, I also have to add another CustomAction element in the Elements file that points to the location of my javascript file. This is the result:

Enabled

image

Disabled

image

Below is the full Elements.xml file:

Elements.xml

   1: <?xml version="1.0" encoding="utf-8"?>
   2: <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
   3:   <CustomAction Id="DeviantPoint.DownloadZip" Location="CommandUI.Ribbon">
   4:     <CommandUIExtension>
   5:       <CommandUIDefinitions>
   6:         <CommandUIDefinition Location="Ribbon.Documents.Copies.Controls._children">
   7:           <Button Id="Ribbon.Documents.Copies.DownloadZip"
   8:                   Command="DownloadZip"
   9:                   Sequence="15" 
  10:                   Image16by16="/_layouts/images/DeviantPoint.DownloadZip/zip_16x16.png" 
  11:                   Image32by32="/_layouts/images/DeviantPoint.DownloadZip/zip_32x32.png"
  12:                   Description="Download zip" LabelText="Download as Zip"
  13:                   TemplateAlias="o1"/>
  14:         </CommandUIDefinition>
  15:       </CommandUIDefinitions>
  16:       <CommandUIHandlers>
  17:         <CommandUIHandler
  18:           Command="DownloadZip"
  19:           CommandAction="javascript:downloadZip();"
  20:           EnabledScript="javascript:enable();"/>
  21:       </CommandUIHandlers>
  22:     </CommandUIExtension>
  23:   </CustomAction>
  24:   <CustomAction Id="Ribbon.Library.Actions.Scripts"
  25:                 Location="ScriptLink"
  26:                 ScriptSrc="/_layouts/DeviantPoint.DownloadZip/CustomActions.js" />
  27: </Elements>

Finally, I created the CustomActions.js file. This file is used to define the actions/behavior of my new ribbon button. The enable() function is used to determine whether or not my button is enabled. If there is at least one item selected, then my button is enabled. The downloadZip() function just starts off the download process. Actually, I could have probably written the javascript so I didn’t even need this function or calls to SP.ClientContext.executeQueryAsync() but I was just trying to get something done quickly and actually writing it this way gave me another place to show-off another one of the UI features, the Status. If the call to SP.ClientContext.executeQueryAsync() fails, then the onQueryFailed delegate is executed. The onQueryFailed() function uses the SP.UI.Status to display the error message, shown here:

download_failed

The function onQuerySucceeded() is where the majority of the action happens. I use the SP.ListOperation.Selection object to get a list of the selected items. I then create a request to my DownloadZip.aspx application page and send that page the list of selected item ids as well as the current url (the url of the page the user is on). Like I said earlier, that application page takes care of packaging everything up as a zip and streaming it down to the browser.

Below is the code for CustomActions.js:

CustomActions.js

   1: function enable() {
   2:     var items = SP.ListOperation.Selection.getSelectedItems();
   3:     var itemCount = CountDictionary(items);
   4:     return (itemCount > 0);
   5:  
   6: }
   7:  
   8: function downloadZip() {
   9:  
  10:     var context = SP.ClientContext.get_current();
  11:     this.site = context.get_site();
  12:     this.web = context.get_web();
  13:     context.load(this.site);
  14:     context.load(this.web);
  15:     context.executeQueryAsync(
  16:         Function.createDelegate(this, this.onQuerySucceeded),
  17:         Function.createDelegate(this, this.onQueryFailed)
  18:     );    
  19: }
  20:  
  21: function onQuerySucceeded() {
  22:  
  23:     var items = SP.ListOperation.Selection.getSelectedItems();
  24:     var itemCount = CountDictionary(items);
  25:  
  26:     if (itemCount == 0) return;
  27:  
  28:     var ids = "";
  29:     for (var i = 0; i < itemCount; i++) {
  30:         ids += items[i].id + ";";
  31:     }
  32:  
  33:     //send a request to the zip aspx page.
  34:     var form = document.createElement("form");
  35:     form.setAttribute("method", "post");
  36:     form.setAttribute("action", this.site.get_url() + this.web.get_serverRelativeUrl() + "/_layouts/deviantpoint.downloadzip/downloadzip.aspx");
  37:  
  38:     var hfSourceUrl = document.createElement("input");
  39:     hfSourceUrl.setAttribute("type", "hidden");
  40:     hfSourceUrl.setAttribute("name", "sourceUrl");
  41:     hfSourceUrl.setAttribute("value", location.href);
  42:     form.appendChild(hfSourceUrl);
  43:  
  44:     var hfItemIds = document.createElement("input")
  45:     hfItemIds.setAttribute("type", "hidden");
  46:     hfItemIds.setAttribute("name", "itemIDs");
  47:     hfItemIds.setAttribute("value", ids);
  48:     form.appendChild(hfItemIds);
  49:  
  50:     document.body.appendChild(form);
  51:     form.submit();
  52: }
  53:  
  54: function onQueryFailed(sender, args) {
  55:     this.statusID = SP.UI.Status.addStatus("Download as Zip:", 
  56:         "Downloading Failed: " + args.get_message() + " <a href='#' onclick='javascript:closeStatus();return false;'>Close</a>.", true);
  57:     SP.UI.Status.setStatusPriColor(this.statusID, "red");
  58: }
  59:  
  60: function closeStatus() {
  61:     SP.UI.Status.removeStatus(this.statusID);
  62: }

So how does this actually all look when the user is using it? Below is the hierarchy of an example document library I have:

image

  • Documents
    • Folder A (Folder)
      • Subfolder in Folder A (Folder)
        • Sub Sub Folder (Folder)
          • Versioning Flow (Visio diagram)
        • Business Brief_SoW (Word document)
        • SoW_Phase1 (Word document)
      • Request Email (Text file)
      • Users and Roles (Excel file)
    • Issues (Excel file)
    • Product_Planning (Excel file)

The user has selected some documents and a sub-folder so my custom ribbon button is enabled:

image

The user clicks on this button and this feature executes and after it’s complete the user is prompted with this (note, the filename is a timestamp):

image

The user saves it down locally to his computer and sees all the files are there:

image

That’s it, code is done. Time to drink some beer (I was actually drinking some beer as I wrote this so forgive any mistakes :)).

WSP file (Site collection feature): DeviantPoint.DownloadZip.wsp (84.84 kb)

Visual Studio 2010 Project file: DeviantPoint.DownloadZip.zip (317.01 kb)

ALM with Team Foundation Server 2010 and SharePoint 2010 (Part 2)

In the first part of this series, I showed you how to create a new Team Project Collection in TFS and the new SharePoint site collection and SSRS reports folder created for the site collection. In this part, I will show you how to create a TFS Team Project in the Team Project Collection, add team members to the project, manage permissions on the SharePoint site and review the SSRS report security settings.

Part 2A: Creating a team project.

1. Open Visual Studio 2010 and click on Connect to Team Foundation Server.

 image

2.  Select the TFS server and select the Team Project Collection that we created in Part 1 and click the Connect button.

image

3. Create a new Team Project in the Team Project Collection. In the Team Explorer pane, right-click on the Team Project Collection and select New Team Project.

image 

4. In the wizard that opens, specify the name and description of the new project.

image

5. Specify the Process Template to use for the project. Out of the box, there are two process templates available: MSF for Agile Software Development v5.0 and MSF for CMMI Process Improvement v5.0. For this project, I’m going to use the MSF for Agile Software Development v5.0 template. To understand the differences between the two templates, refer to the documentation located here: http://msdn.microsoft.com/en-us/vstudio/aa718795.aspx. Note, you can also download additional process templates or create your own process template using the Process Editor (part of the TFS Power Tools). This is available at the above link.

image 

6. Allow the New Project wizard to create the SharePoint site that will be used as the portal for this project. The site will be created under the site collection that was created for the Team Project Collection.

image

7. The wizard will create a new source control folder in TFS.

image

8. Review the new Team Project settings.

image

9. Click the Finish button.

image

10. Close the wizard after the project is created.

image

Part 2B: Managing Team Members and Permissions

1. In Team Explorer, find the new project we just created and right-click on Team Members. You can give users access to the team by clicking on the Add Team Member  option. You can also create sub-teams by selecting the New Subteam option. For this project,  we will create the following subteams: Business Analysts, Developers, Project Managers, and Testers.

image

2. Add the appropriate members to each sub-team by right-clicking on each sub-team name and selecting the Add Team Member option from the menu. These members will all be added to the Team Project’s Contribute security group. To modify or reassign them to a different security group, right-click on the team project name and select Team Project Settings –> Group Membership.

image

3. In order to make sure the team members can access the project portal, you need to go into the SharePoint site and give the users you added to the team permissions to the site. In the Team Explorer window, right-click on the project name and select Show Project Portal. This will open the SharePoint site that was created for this project.

image

14. Go to Site Actions –> Site Permissions to grant access to this site for the project team members. You can create groups, custom permission levels, etc just like permission management in any other SharePoint site. In this case, I’m going to just grant each team member Contribute permissions for this site.

image 

Part 2C: Reviewing SSRS Security

1. In the SharePoint site, click the Reports link in the Quick Launch menu on the left. You will be taken to the SSRS reports folder specific to the new project. There different folders for the different reports. The reports available are based on the process template you selected when creating the Team Project. By default, for the Agile-based process template, there are reports for Bugs, Builds, Project Management, and Tests.

image

2. You can also manage the security of a report folder or report. Hover over a folder or report and select Security from the drop-down menu. Select Edit Item Security from the toolbar. You will be asked to confirm that you actually want to break permission-inheritance for the object.

image

image

3. By either clicking on the New Role Assignment toolbar item or clicking Edit next to an existing Group or User, you can change with Role Assignment for the group or user. The main roles are: Browser (read-access), Publisher (create reports), and Content Manager (manages the report server content for the project).

image

image