I'm trying to get events from a SharePoint site and insert them into FullCalendar using the method described by Josh McCarty here.
An empty calendar shows up successfully using the snippets in Josh's article, however there seems to be an error when the Events List in SharePoint is being queried. I'm doing a slight modification from his example, because my List is in the host web site, not the app, so I have to use the webURL parameter.
When I look in the Chrome Dev console I see "Request format is unrecognized" as an error.
This technique uses SPServices, and a Caml query to fetch events, but I suspect something is wrong with my Caml.
My code (below) is almost verbatim from the example, but gives me an error at or around the line that I marked with debugger; //REQUEST FORMAT IS UNRECOGNIZED occurs here.
// Add events to the calendar. This is where the "magic" happens!
events: function( start, end, callback ) {
// Create an array to hold the events.
var events = [];
// Set the date from which to pull events based on the first visible day in the current calendar view.
// For a month view, this will usually be several days into the previous month.
// We can use FullCalendar's built-in getView method along with the formatDate utility
// function to create a date string in the format that SharePoint requires.
// It must be in the format YYYY-MM-DDTHH:MM:SSZ. Due to time zone differences,
// we will omit everything after the day.
var startDate = $.fullCalendar.formatDate( $( '#calendar' ).fullCalendar( 'getView' ).start, "u" ).split( "T" )[0];
// Get the current view of the calendar (agendaWeek, agendaDay, month, etc.).
// Then set the camlView to the appropriate value to pass to the web service.
// This way we will only retrieve events needed by the current view
// (e.g. the agendaWeek view will only retrieve events during the current week rather
// than getting all events for the current month).
var calView = $( '#calendar' ).fullCalendar( 'getView' ).title;
var camlView = "";
switch( calView ) {
case "agendaWeek":
camlView = "<Week />";
break;
case "agendaDay":
camlView = "<Week />";
break;
default: // Default to month view
camlView = "<Month />";
}
// Set the camlFields, camlQuery, and camlOptions to the appropriate values to pass to the web service.
// You can add additional <ViewFields /> or adjust the CAML query if you have some custom columns that
// you want to filter by or display data from. The values below are the pretty much the minimum you'll
// want to start from to get it working.
var camlFields = "<ViewFields><FieldRef Name='Title' /><FieldRef Name='EventDate' /><FieldRef Name='EndDate' /><FieldRef Name='Location' /><FieldRef Name='Description' /><FieldRef Name='fRecurrence' /><FieldRef Name='RecurrenceData' /><FieldRef Name='RecurrenceID' /><FieldRef Name='fAllDayEvent' /></ViewFields>";
var camlQuery = "<Query><CalendarDate>" + startDate + "</CalendarDate><Where><DateRangesOverlap><FieldRef Name='EventDate' /><FieldRef Name='EndDate' /><FieldRef Name='RecurrenceID' /><Value Type='DateTime'>" + camlView + "</Value></DateRangesOverlap></Where><OrderBy><FieldRef Name='EventDate' /></OrderBy></Query>";
var camlOptions = "<QueryOptions><CalendarDate>" + startDate + "</CalendarDate><RecurrencePatternXMLVersion>v3</RecurrencePatternXMLVersion><ExpandRecurrence>TRUE</ExpandRecurrence></QueryOptions>";
var hostUrl = decodeURIComponent(getQueryStringParameter("SPHostUrl"));
debugger;
// Make the web service call to retrieve events.
$().SPServices({
operation: "GetListItems",
async: false,
webURL: hostUrl,
listName: paidTimeOffListName, // This is the GUID or display name of your calendar. If the calendar is on a different site, you can use the display name with the webURL option (see SPServices.CodePlex.com for more information).
CAMLViewFields: camlFields,
CAMLQuery: camlQuery,
CAMLQueryOptions: camlOptions,
completefunc: function( xData, Status )
{
debugger; //REQUEST FORMAT IS UNRECOGNIZED occurs here.
//.find( '[nodeName="z:row"]' ) unsupported past jQuery 1.7
$(xData.responseXML).SPFilterNode('z:row').each( function()
{
debugger;
// Check for all day events
var fADE = $( this ).attr( 'ows_fAllDayEvent' );
var thisADE = false;
var thisStart = $( this ).attr( 'ows_EventDate' );
var thisEnd = $( this ).attr( 'ows_EndDate' );
if ( typeof fADE !== "undefined" && fADE !== "0" )
{
thisADE = true;
}
// Get the list item ID and recurrence date if present. This will be used to generate the ID query string parameter to link to the event (or the specific instance of a recurring event). The ID query string parameter must be in the format "ID.0.yyyy-MM-ddTHH:mm:ssZ" for recurring events (where "ID" is the list item ID for the event). Event ID's are returned as just a number (for non-recurring events) or several numbers separated by ";#" in 2007 or "." in 2010 to indicate individual instances of recurring events. By splitting and joining the ID this way, thisID will be set to a valid query string parameter whether an event is recurring or not for both versions of SharePoint.
var thisID = $( this ).attr( 'ows_ID' ).split( ';#' ).join( '.' );
// FullCalendar documentation specifies that recurring events should all have the same id value when building the events array (the id is optional, but I'm including it for completeness). We can get the list item ID (which is the same for all instances of recurring events) without the recurrence information by simply splitting thisID.
var eventID = thisID.split( '.' )[0];
// Get the event title. This is displayed on the calendar along with the start time of the event.
var thisTitle = $( this ).attr( 'ows_Title' );
// Get the event description. I don't use it in this example, but you could use it for something, perhaps as a tooltip when hovering over the event.
var thisDesc = $( this ).attr( 'ows_Description' );
// Add the event information to the events array so FullCalendar can display it.
events.push({
title: thisTitle,
id: eventID,
start: thisStart,
end: thisEnd,
allDay: thisADE,
// Adjust this URL to link to the display form for your calendar events. You can include a Source parameter to allow users to easily return to the FullCalendar page.
url: '/Lists/Calendar/DispForm.aspx?ID=' + thisID + '&Source=' + window.location,
description: thisDesc
});
});
callback( events );
}
});
}
I believe I've figured out what the problem is, although unfortunately, it seems to be unfixable.
SPServices won't work cross-domain (as far as I can tell), and since I'm trying to make this call from inside of a SharePoint app to a list not stored in the app, it's considered a cross domain request.
One workaround would be to use something else to get the info, such as REST.
EDIT: Info in the article here seems to contradict my belief:
SPServices supports anonymous access to read from lists, and it works cross-site and cross-domain by simply specifying the webURL in the Web Service operations where it makes sense to do so. Of course, your authentication model has to allow cross-domain access.
So perhaps it is possible.
Related
I have recurrence item in calendar, let's say it will repeat 5 times from start date 5/23/2018
In Calendar view, it will repeat the item from 5/23 to 5/28.
However in all item view(list view), it will only show once, it won't repeat the item 5 times.
Is there a way, to make list view replicate the calendar view?
Thanks
You can switch the view to "Current Events." In that view, recurrent events are displayed individually.
It can't be done using OOTB.
The best you get with OOTB is to create a view using view type 'Standard View, with Expanded Recurring Events.":
This will repeat the recurring item in the view
However this will only show event from today onwards(will not show past events)
The other way will be to code manually
It can be done using spservice and camlquery
var camlQuery = "<Query><Where><DateRangesOverlap><FieldRef Name='EventDate' /><FieldRef Name='EndDate' /><FieldRef Name='RecurrenceID' /><Value Type='DateTime'><Year /></Value></DateRangesOverlap></Where><OrderBy><FieldRef Name='EventDate' /></OrderBy></Query>";
var camlOptions = "<QueryOptions><RecurrencePatternXMLVersion>v3</RecurrencePatternXMLVersion><ExpandRecurrence>TRUE</ExpandRecurrence></QueryOptions>";
// Make the web service call to retrieve events.
$().SPServices({
operation: "GetListItems",
async: false,
listName: 'Test Calendar',
CAMLQuery: camlQuery,
CAMLQueryOptions: camlOptions,
completefunc: function(xData, Status) {
$(xData.responseXML).find("z\\:row, row").each(function() {
console.log($(this).attr('ows_title'))
});
}
});
So my goal is to allow the user to click the item on the list and it automatically changes a value on the item. Mostly I want a easy to allow the user to mark the item as being read. This list will be updated every week, so it will allow the user to know what is new easier. I have already set up the views so the users can only see their own data on the personal view.
I have looked online and I'm not having great luck. This is what I have so far.
<script src="https://code.jquery.com/jquery-1.11.2.min.js" type="text/javascript"></script><script type="text/javascript">
$(document).ready(function() {
$(".ms-listviewtable > tbody > tr").click(function(){ //only works before ajax runs
setTimeout( function() {
//alert("Hello! I am an alert box!!");
var ctx = SP.ClientContext.get_current("https://...");
var items = SP.ListOperation.Selection.getSelectedItems(ctx);
var clientContext = new SP.ClientContext();
var oList = clientContext.get_web().get_lists().getByTitle('Ticket');
this.oListItem = oList.getItemById(items[0].id);
this.oListItem.set_item('read','yes');
this.oListItem.update();
clientContext.executeQueryAsync(Function.createDelegate(this, this.onQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));
}, 200); //had to be put in place to give time for the list to be selected
});
});
These are the issues I have.
the first time I click a item it tells be SP.ListOperation.Selection.getSelectedItems is null. However if I click the same item it will give me the ID number. Then if I click it a third time it will tell me its null again.
It does not change the vaule, but it does give me the right ID number after I click it two times.
EDIT,
see comments below
Using this code:
$("table.ms-listviewtable").on("mouseup", "tr.ms-itmhover", function() {
//your code
});
I'm trying to delete selected doc from viewPanel1. The view is categorized ( can be > 1 category ) and is listing documents from 2 different datasources, let say: Cdoc and Pdoc. These docs. are linked by a common field.
my scenario: If the users select a Cdoc => the delete action will take place to the respective Cdoc but also for the all Pdoc being in the same category. If the user selects a Pdoc => delete just the Pdoc. Also, I would like to add some confirmation text with some information ( Value fields ) from the selected documents.
I tried the following
var viewPanel=getComponent("viewPanel1");
var docIDArray=viewPanel.getSelectedIds();
for(i=0;i < docIDArray.length;i++){
var docId=docIDArray[i];
var doc=database.getDocumentByID(docId);
var formName = (doc == null)? null : doc.getItemValueString("Form");
if( formName =="fmPersContact" ){
.....
} // in this case, it works OK.
else if ( formName =="fmCompanie" ){ // here if I selected > 1 Cdoc, it deletes just one Cdoc + the respective PDocs.
var doc:NotesDocument = null;
doc=database.getDocumentByID(docId);
var ky:java.util.Vector = new java.util.Vector();
ky.add(doc.getItemValueString("txt_NumeCompanie"));
... // delete method
}
Could you tell me what I did wrong and what am I missing in the above code? thanks for your time!
The first thing you want to do is confirm. Unlike Lotusscript, you cannot use a function in the middle of a script to open the confirm dialog and get the answer. To do this, I recommend using the confirm simple action before going into an execute script simple action.
<xp:button
value="delete"
id="button1"
>
<xp:eventHandler event="onclick" submit="true" refreshMode="complete">
<xp:this.action>
<xp:actionGroup>
<xp:confirm message="Are you certain?"></xp:confirm>
<xp:executeScript
script="#{javascript:doSomething();}"
>
</xp:executeScript>
</xp:actionGroup>
</xp:this.action></xp:eventHandler></xp:button>
EDIT
In the past, I have also used built my own dialogs with the extension library, filled the text in with SSJS and then called the doWhatever() or close() from the dialog itself. This is not the best solution as it requires an update from the server to get the string. The best solution would be, as Paul Withers says, to use CSJS to perform the confirmation. I have yet to do this though.
/EDIT
For your delete function, I recommend getting the document you want to delete, then tell whether it is a P- or C- doc, either by the form name or whatever mechanism you use, and then either delete the single document or by getting a documentcollection from the view by using getAllDocumentsBykey(), then iterating through them all, deleting them one by one.
var ky:java.util.Vector = new java.util.Vector();
ky.add("MainCat");
ky.add("subCat");
ky.add("subCat2");
var vw:NotesView = database.getView("vw_myView");
var docs:NotesDocumentCollection = vw.getAllDocumentsByKey(ky);
//... delete stuff...
//dont forget to recycle
Post Question Edit
I recommend the following to get the form name:
var getSelectedDoc = function(){
var vwPnl = getComponent("viewpanel");
var ids = vwPnl.getSelectedIds();
var id = null;
var doc:NotesDocument = null;
if(ids.length > 0){ //could use for loop var i = 0; i < ids.length;i++
id = ids[0]; //could pack all ids into java.util.ArrayList and return that list to work on further
//be warned that if the user selects a parent doc and those automatically deleted by it that you need a mechanism to check if the document was already deleted!
}
if(id != null){
doc = database.getDocumentByID(id);
}
return doc;
}
var doc = getSelectedDoc();
var formName = (doc == null)? null : doc.getItemValueString("form");
if(PDOC_FORM_NAME.equalsIgnoreCase(formName)){
deleteFunctionComplete(doc);
} else if (CDOC_FORM_NAME.equalsIgnoreCase(formName)){
deleteFunctionTwo(doc);
} else {
// uh-oh
}
this also allows you to have the document in case you want to delete it right away.
Edits for comments
If Cdoc should delete more than one document, then yes. You should be using the getAllDocumentsBykey keeping in mind that the view needs to be built for it. By that I mean if you have a view with one single category, there is no issue, just plug in the string and you are fine. If you have a view with three categories, you cannot feed a vector into the getalldocs function with only two values, it must be all three. So, you want to delete all for "mycomp" with the underlying pdocs "Greg" "Sally "Bob", just use alldocsbykey("mycomp"), if the view looks like:
mycomp
---Greg
---Sally
---Bob
but if the view looks like
Poland
---mycomp
------Greg
------Sally
------Bob
then the a vector with poland and mycomp must be used. "poland" does not get the correct documents. --just an fyi and pitfall that is sometimes had.
Edit after further question clarification
I prefer this loop style to remove docs
var doc_temp:NotesDocument = null;
var doc_toDelete:NotesDocument = null;
var coll_docs:NotesDocumentCollection = ...; //get document collection
var doc_nextDoc = coll_docs.getFirstDocument();
while(doc_nextDoc != null){
doc_temp = doc_nextDoc; //set document to delete
doc_nextDoc = coll_docs.getNextDocument(doc_nextDoc); // set next document before deletion
try{
doc_temp.remove(true);//lots of errors can happen here, such as ACL settings
} catch(e) {
//handle, or just break
} finally{
if(doc_temp != null) try{doc_temp.recycle()} catch(e){}// try to recycle, could also cause errors
doc_temp = null;// for the sense of completeness
}
}
Even further edit based on the question edits
of course you are only deleting one Pdoc, the way you have that set up, you are only ever returning one document. You could expand the getSelectedDoc() to put all selected documents into an java.util.ArrayList or something, and then use that arraylist to delete more than one at a time, but that could be dangerous depending on what you do because NotesDocuments are not serialisable. In that case, I recommend using the same code that you use for getSelected doc, use a for loop to get the document IDs, get the document, if the document is not null, then delete.
apropos getAllDocumentsByKey(with a vector)
The way this is currently set up, no Vector is necessary.
If you have a view with a category and sub category and you want to get all the documents in that sub category, then you must use a vector to get at it. If you include a simple string or a vector with only one value, then the documents in the sub category will not be returned. The vector can be thought of as "cat1", "subcat", "furtherSubCat"
Furthermore, there is no check here to see if the string returned from the document is empty. This should be done. There is also no check to see if the DocumentCollection is empty. This should also be done. My expectation is that there is an issue retrieving the collection based on above mentioned reasons.
I am writing a app that creates a custom events list.
I'd like to enable content approval (also referred to Moderation on the back end) upon creation of the list.
Here's what my list-creation code looks like.
function createList(listToCreate)
{
// Create a SharePoint list with the name that the user specifies.
var hostUrl = decodeURIComponent(getQueryStringParameter("SPHostUrl"));
var hostContext = new SP.AppContextSite(currentContext, hostUrl);
var hostweb = hostContext.get_web();
var listCreationInfo = new SP.ListCreationInformation();
//title the list
listCreationInfo.set_title(listToCreate);
//set the base type of the list
listCreationInfo.set_templateType(SP.ListTemplateType.events);
var lists = hostweb.get_lists();
//use the creation info to create the list
var newList = lists.add(listCreationInfo);
var fieldCollection = newList.get_fields();
//add extra fields (columns) to the list & any other info needed.
fieldCollection.addFieldAsXml('<Field Type="User" DisplayName="Requester" Name="Requester" />', true, SP.AddFieldOptions.AddToDefaultContentType);
fieldCollection.addFieldAsXml('<Field Type="User" DisplayName="Manager" Name="Manager" />', true, SP.AddFieldOptions.AddToDefaultContentType);
fieldCollection.addFieldAsXml('<Field Type="Boolean" DisplayName="Approved" Name="Approved" />', true, SP.AddFieldOptions.AddToDefaultContentType);
//Attempting to enable moderation. This doesn't seem to have any effect.
newList.set_enableModeration(true);
currentContext.load(fieldCollection);
currentContext.load(newList);
currentContext.executeQueryAsync(onListCreationSuccess, onListCreationFail);
}
function onListCreationSuccess() {
alert("We've created a list since one didn't exist yet. Look in the site that hosts this app for the list." );
}
function onListCreationFail(sender, args) {
//alert("We didn't create the list. Here's why: " + args.get_message());
}
Unfortunately it seems that .set_enableModeration(true); has no effect. I don't get any errors, but when I look at the settings for the list I created using this code I see this:
So Content Approval clearly was not enabled via the method I'm using.
In order to set Content Approval using SP.List.enableModeration Property, the SP.List.update() Method have to be called as demonstrated below:
list.set_enableModeration(true);
list.update();
My code looks like below. I am feching the json records from the server side using structs action. it is returning the records fine and i could able to see the table with data. pagination links created fine. when i click next and datasource is called on each click of any link on the pagination. if i click on colum header also, the datasource is being called.
my questions are:
1)When datasource is being called. because i am seeing sometime called and some times not. like when i got from 1page to 2page, datasource is called fine. when i go back to previous pages by clicking 'prev' link, datasource is being called. but after that if i click again on next to go to 2nd page, datasource is not being called. when exactly datasource is called and how many times it will called. is it for every link in the pagination calls datasource?
2)If my datasource returns 100 recods of data and my records per page is set to 25, then do i see the 4 pages. I am confused here with server side pagination and datasource calls.
datasource is not called for each page link and next or prev link clicks? if not, when datasource is called? please explain me. I know how many total records are there in the begining and my requirement is showing 25 records per page when ever user clicks on page number or next or prev links. i have the capability to bring corresponding 25 records based on the page number from server side.
3)how to capture the 'next' and 'prev' clicks on the pagination.
my requirement is to dynamically fetch the json data using datasource from the server whenever user click on page number links or next or prev.
Please help me out with this. I am new to YUI. I have to User YUI 2 only since we are already using it.
<div id="dynamicdata"></div>
<script type="text/javascript">
YAHOO.example.DynamicData = function() {
var myColumnDefs = [ // sortable:true enables sorting
{key:"PIN", label:"PIN", sortable:true},
{key:"CODE", label:"CODE", sortable:true}
];
// Customize request sent to server to be able to set total # of records
var generateRequest = function(oState, oSelf) {
// Get states or use defaults
oState = oState || { pagination: null, sortedBy: null };
var sort = (oState.sortedBy) ? oState.sortedBy.key : "PIN";
var dir = (oState.sortedBy && oState.sortedBy.dir === YAHOO.widget.DataTable.CLASS_DESC) ? "desc" : "asc";
var startIndex = (oState.pagination) ? oState.pagination.recordOffset : 0;
var results = (oState.pagination) ? oState.pagination.rowsPerPage : 25;
var total = YAHOO.util.Dom.get("total").value *1;
// Validate input
if(!YAHOO.lang.isNumber(total) || total < 0 || total > 200) {
YAHOO.util.Dom.get("total").value = 0;
total = 0;
alert("Total must be between 0 and 200.");
}
// Build custom request
return "sort=" + sort +
"&dir=" + dir +
"&startIndex=" + startIndex +
"&results=" + (startIndex + results) +
"&total=" + total;
};
// DataTable configuration
var myConfigs = {
generateRequest: generateRequest,
initialRequest: generateRequest(), // Initial request for first page of data
dynamicData: true, // Enables dynamic server-driven data
sortedBy : {key:"PIN", dir:YAHOO.widget.DataTable.CLASS_ASC}, // Sets UI initial sort arrow
paginator: new YAHOO.widget.Paginator({ rowsPerPage:10 }) // Enables pagination
};
var myDataSource = new YAHOO.util.DataSource("<%=request.getContextPath()%>/results.do?startIndex="+localStartIndex+"&rowsPerPage="+rowsPerPage);
myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
myDataSource.responseSchema = {
resultsList: "data",
fields: [
{key:"SSN"},
{key:"PIN"}
]
}
var myDataTable = new YAHOO.widget.DataTable("dynamicdata", myColumnDefs, myDataSource, myConfigs);
// DataTable instance
var myDataTable = new YAHOO.widget.DataTable("dynamicdata", myColumnDefs, myDataSource, myConfigs);
// Update totalRecords on the fly with values from server
myDataTable.doBeforeLoadData = function(oRequest, oResponse, oPayload) {
oPayload.totalRecords = 200;
return oPayload;
};
return {
ds: myDataSource,
dt: myDataTable
};
}();
As far as I remember, the DataTable will always ask for fresh data whenever it changes pages or sorts by a different column. It doesn't cache previous requests nor does it keep track what it has asked. If you don't see requests arriving on the server side it might be because of caching, but not in DataTable or DataSource but by the browser itself, which is a matter of issuing the proper headers on the server to tell the browser not to cache.
If I am not mistaken, that this is supported by the behavior you describe. The first page is requested twice, once when you first draw the table, once again when you return from page 2. All other pages are never requested twice. Why? Because the first time around the URL formed is slighty different from the URL when you return to it. The browser cache only knows about URLs.