FullCalendar.js - "there was an error while fetching events" - javascript

I'm using FullCalendar.js to display Google Calendar events from multiple sources. It's been working OK up until today. For some reason FullCalendar started popping up the "there was an error while fetching events" error message and all the events are obviously gone. Here is a jsfiddle.
http://jsfiddle.net/mlk4343/1wko0z1j/1/
$(document).ready(function() {
$('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'month,agendaWeek,agendaDay'
},
contentHeight: 600,
eventMouseover: function(calEvent, jsEvent) {
var tooltip = '<div class="tooltipevent">' + calEvent.title + '</div>';
$("body").append(tooltip);
$(this).mouseover(function(e) {
$(this).css('z-index', 10000);
$('.tooltipevent').fadeIn('500');
$('.tooltipevent').fadeTo('10', 1.9);
}).mousemove(function(e) {
$('.tooltipevent').css('top', e.pageY + 10);
$('.tooltipevent').css('left', e.pageX + 20);
});
},
eventMouseout: function(calEvent, jsEvent) {
$(this).css('z-index', 8);
$('.tooltipevent').remove();
},
eventSources: [
{
// Adele H
url: 'https://www.google.com/calendar/feeds/sonomaschools.org_u030vtntt1tp7gjn8cnqrr9nsk%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'yellow', // a non-ajax option
textColor: 'black' // a non-ajax option
},
{
// Altimira
url: 'https://www.google.com/calendar/feeds/sonomaschools.org_e6j3ejc40g02v4sdo0n3cakgag%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'red', // a non-ajax option
textColor: 'white' // a non-ajax option
},
{
// Charter
url: 'https://www.google.com/calendar/feeds/sonomacharterschool.org_0p2f56g5tg8pgugi1okiih2fkg%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'LightSalmon', // a non-ajax option
textColor: 'black' // a non-ajax option
},
{// Dunbar
url: 'https://www.google.com/calendar/feeds/sonomaschools.org_4tmsks5b9s70k6armb6jkvo9p0%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'green', // a non-ajax option
textColor: 'white' // a non-ajax option
},
{// El Verano
url: 'https://www.google.com/calendar/feeds/pv2hfl7brn6dj8ia3mqksp9fl0%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'LightBlue', // a non-ajax option
textColor: 'black' // a non-ajax option
},
{ // Flowery
url: 'https://www.google.com/calendar/feeds/sonomaschools.org_v0a2nmtu4jrca90lui62tccbd4%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'blue', // a non-ajax option
textColor: 'white' // a non-ajax option
},
{ // Prestwood
url:'https://www.google.com/calendar/feeds/sonomaschools.org_25rjgf4pu3vsa5i7r7itnqkigs%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'purple', // a non-ajax option
textColor: 'white' // a non-ajax option
},
{ // Sassarini
url: 'https://www.google.com/calendar/feeds/sonomaschools.org_18a25r5mrc084gn4ekegadpfm8%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'Aqua ', // a non-ajax option
textColor: 'black' // a non-ajax option
},
{ // SVHS
url: 'https://www.google.com/calendar/feeds/sonomaschools.org_h450occacktra5errgbhsrv3k4%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'Chartreuse', // a non-ajax option
textColor: 'black' // a non-ajax option
},
{ // SVUSD
url: 'https://www.google.com/calendar/feeds/sonomaschools.org_2i1596pg2fokba99kvatqn45bk%40group.calendar.google.com/public/basic',
type: 'POST',
error: function() {
alert('there was an error while fetching events!');
},
color: 'MediumVioletRed', // a non-ajax option
textColor: 'white' // a non-ajax option
},
]
});
});
The events show OK on Google Calendar.

I tried the other solutions, which got me close to a fix but not entirely there. The results were fetching the entire set of calendar events and not a set number in a certain date-range.
What I discovered was that the names of the Parameters have also changed in the new API.
See: https://developers.google.com/google-apps/calendar/v3/reference/events/list
My fix involves adding the new APIv3 parameters to the data variable. Also the date-format for timeMin and timeMax are RFC3339/ATOM and not ISO 8601 (which Moment.js outputs by default) so I have added a format string to produce RFC3339 formatted dates.
Use the APIv3 URL format in your HTML/PHP file:
https://www.googleapis.com/calendar/v3/calendars/CALENDAR-ID/events?key=API-KEY
Update your gcal.js to the following code. This is based on the fixes posted by user4263042 and Stephen (Thanks guys!)
(function(factory) {
if (typeof define === 'function' && define.amd) {
define([ 'jquery' ], factory);
}
else {
factory(jQuery);
}
})(function($) {
var fc = $.fullCalendar;
var applyAll = fc.applyAll;
fc.sourceNormalizers.push(function(sourceOptions) {
if (sourceOptions.dataType == 'gcal' ||
sourceOptions.dataType === undefined &&
(sourceOptions.url || '').match(/^(http|https):\/\/www.googleapis.com\/calendar\/v3\/calendars/)) {
sourceOptions.dataType = 'gcal';
if (sourceOptions.editable === undefined) {
sourceOptions.editable = false;
}
}
});
fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) {
if (sourceOptions.dataType == 'gcal') {
return transformOptions(sourceOptions, start, end, timezone);
}
});
function transformOptions(sourceOptions, start, end, timezone) {
var success = sourceOptions.success;
var data = $.extend({}, sourceOptions.data || {}, {
'singleEvents' : true,
'maxResults': 250,
'timeMin': start.format('YYYY-MM-DD[T]HH:mm:ssZ'),
'timeMax': end.format('YYYY-MM-DD[T]HH:mm:ssZ'),
});
return $.extend({}, sourceOptions, {
url: sourceOptions.url + '&callback=?',
dataType: 'jsonp',
data: data,
success: function(data) {
var events = [];
if (data.items) {
$.each(data.items, function(i, entry) {
events.push({
id: entry.id,
title: entry.summary,
start: entry.start.dateTime || entry.start.date,
end: entry.end.dateTime || entry.end.date,
url: entry.htmlLink,
location: entry.location,
description: entry.description || '',
});
});
}
var args = [events].concat(Array.prototype.slice.call(arguments, 1));
var res = applyAll(success, this, args);
if ($.isArray(res)) {
return res;
}
return events;
}
});
}
// legacy
fc.gcalFeed = function(url, sourceOptions) {
return $.extend({}, sourceOptions, { url: url, dataType: 'gcal' });
};
});

Here's the fix everyone:
https://github.com/jonnyhweiss/fullcalendar/commit/520022a4da81ded61f3a1cc7020df4df54726fbc?diff=split
It requires editing of gcal.js and gcal.html to get the demo's working; from those demos you should be able to fix your own broken calendars, hopefully ; )
Please note:
Requires Full-Calendar 2.2.0
I quickly discovered it will not work on Full Calendar 1.x.x, or if it will, I'm not code savvy enough to figure it out. Full Calendar 2.2.0 adds moment.js as a dependent JS link, which is not a part of Full Calendar 1.x.x, so copy and pasting what is available on the link above into your Full Calendar 1.x.x files will not work.
Happy coding and fixing your Google Calendars!

I believe I have the solution.
After a little digging I found a this page, but written as is, the code failed to work correctly. BUT after a little modification, see below, I now have things in working order again.
To use the new piece of code one needs to change the source URL for ones calendar to the form:
https://www.googleapis.com/calendar/v3/calendars/CALENDAR-ID/events?key=API-KEY
Insert your own calendar id and public API key into the URL as indicated. Your API-KEY can be obtained by setting up a project inside your Google Developers Console and then creating a public access API browser key.
Here is the actual code one needs to use in place of ones in the current gcal.js file.
(function(factory) {
if (typeof define === 'function' && define.amd) {
define([ 'jquery' ], factory);
} else {
factory(jQuery);
}
})
(function($) {
var fc = $.fullCalendar;
var applyAll = fc.applyAll;
fc.sourceNormalizers.push(function(sourceOptions) {
if (sourceOptions.dataType == 'gcalv3'
|| (sourceOptions.dataType === undefined
&& (sourceOptions.url || '').match(/^(http|https):\/\/www.googleapis.com\/calendar\/v3\/calendars\//))) {
sourceOptions.dataType = 'gcalv3';
if (sourceOptions.editable === undefined) {
sourceOptions.editable = false;
}
}
});
fc.sourceFetchers.push(function(sourceOptions, start, end, timezone) {
if (sourceOptions.dataType == 'gcalv3') {
return transformOptionsV3(sourceOptions, start, end, timezone);
}
});
function transformOptionsV3(sourceOptions, start, end, timezone) {
var success = sourceOptions.success;
var data = $.extend({}, sourceOptions.data || {}, {
singleevents: true,
'max-results': 9999
});
return $.extend({}, sourceOptions, {
url: sourceOptions.url,
dataType: 'json',
data: data,
startParam: 'start-min',
endParam: 'start-max',
success: function(data) {
var events = [];
if (data.items) {
$.each(data.items, function(i, entry) {
events.push({
id: entry.id,
title: entry.summary || '', // must allow default to blank, if it's not set it doesn't exist in the json and will error here
start: entry.start.dateTime || entry.start.date,
end: entry.end.dateTime || entry.start.date, // because end.date may be the next day, cause a '2-all-day' event, we use start.date here.
url: entry.htmlLink,
location: entry.location || '', // must allow default to blank, if it's not set it doesn't exist in the json and will error here
description: entry.description || '' // must allow default to blank, if it's not set it doesn't exist in the json and will error here
});
});
}
var args = [events].concat(Array.prototype.slice.call(arguments, 1));
var res = applyAll(success, this, args);
if ($.isArray(res)) {
return res;
}
return events;
}
});
}
});

To fix comment out the Google Holiday feed if you are using it. That fixed it for us. Evidently they are having feed issues. That is the only feed from Google I use, so other Google feeds may be impacted also.

Version 2 of the API was deprecated today.

Related

How to call events method after new event source in FullCalendar.js?

I am trying to create a client side filter for events, I have gone with the same approach of addEventSource. I use the events method to conditionally render the events. I just wanna know how to call the events method or even redefine it?
This is the initial code I am using to render the the calendar
$('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'month,basicWeek,basicDay'
},
displayEventTime: false,
navLinks: true, // can click day/week names to navigate views
editable: true,
eventLimit: true, // allow "more" link when too many events
events: (start, end, timezone, callback) => {
$.ajax({
url: 'get-events',
dataType: 'json',
data: {
// our hypothetical feed requires UNIX timestamps
start: start.unix(),
end: end.unix()
},
success: function (res) {
console.log(res)
var events = [];
res.map((value, index) => {
if (value.cadence != null) {
$("#allDropdown").append(`<a id="file-${value.dq_datafile_id}" onclick="selectThis(this)" href="#about">${value.data_file_name}</a>`)
}
if (value.cadence == "WEEKLY") {
if (value.dqfeed__file_status == "RECEIVED") {
const data_file_name = value.data_file_name;
let repeatDay = dow_handler.hasOwnProperty(data_file_name) ? dow_handler[data_file_name] : undefined
events.push({
title: `${value.data_file_name}`,
start: '2020-04-13',
dow: [repeatDay, 1],
color: '#00ff00'
});
}
});
});
});
This is the code I am using to fetch new events or filtered events
const applyCalendarFilter = () => {
var filter = {
type: 'get',
url: 'filter-events',
}
$("#calendar").fullCalendar('addEventSource', filter);
}
The error I get is Uncaught TypeError: Cannot read property 'hasTime' of undefined because the JSON returned doesn't have a start_date or end_date
So I found removeEvents that will clear all the events from the calendar and renderEvents that takes an option argument events and this will render the rest of the events.
So here's the code that did this.
const applyCalendarFilter = () => {
var filter = {
type: 'get',
url: 'filter-events',
}
$("#calendar").fullCalendar('removeEvents');
$.ajax({
url: 'filter-events',
type: 'get',
success: (res) => {
let events = [];
res.map((value, index) => {
if (value.cadence == "WEEKLY"){
events.push({
'title': value.title,
'start_date': value.start_date,
'end_date': value.end_date
}
})
$("#calendar").fullCalendar('renderEvents', events);
}, error: (res) => {
alert('cant get the data');
}
)};

Fullcalendar - can't get events to show asp.net

I'm trying to implement fullcalender but I can't get the exemple data to show. I have been following this guide.
I get no errors but the events doesn't show in the calendar. I have tried different solutions but I can't get it to work, also my experience with json is very limited and i would appreciate some help.
public class CalendarController : BaseController
{
public ActionResult ShowCalendar()
{
return View();
}
public ActionResult GetMeetings(double start, double end)
{
using (var db = new ApplicationDbContext())
{
var fromDate = ConvertFromUnixTimestamp(start);
var toDate = ConvertFromUnixTimestamp(end);
var eventList = GetEvents();
var rows = eventList.ToArray();
return Json(rows, JsonRequestBehavior.AllowGet);
}
}
private static DateTime ConvertFromUnixTimestamp(double timestamp)
{
var origin = new DateTime(1970, 1, 1, 0, 0, 0, 0);
return origin.AddSeconds(timestamp);
}
private List<Events> GetEvents()
{
List<Events> eventList = new List<Events>();
Events newEvent = new Events
{
id = "1",
title = "Event 1",
start = DateTime.Now.AddDays(1).ToString("s"),
end = DateTime.Now.AddDays(1).ToString("s"),
allDay = false
};
eventList.Add(newEvent);
newEvent = new Events
{
id = "1",
title = "Event 3",
start = DateTime.Now.AddDays(2).ToString("s"),
end = DateTime.Now.AddDays(3).ToString("s"),
allDay = false
};
eventList.Add(newEvent);
return eventList;
}
<head>
#section scripts{
<link rel='stylesheet' href='fullcalendar/fullcalendar.css' />
<script src='lib/jquery.min.js'></script>
<script src='lib/moment.min.js'></script>
<script src='fullcalendar/fullcalendar.js'></script>
<script type="text/javascript">
$(document).ready(function () {
$('#calendar').fullCalendar({
theme: true,
header: {
left: 'prev, next, today',
center: 'title',
right: 'month, agendaWeek, agendaDay'
},
buttonText: {
prev: '<',
next: '>'
},
defaultView: 'month',
editable: false,
weekMode: 'liquid',
//allDaySlot: false,
selectTable: true,
//slotMinutes: 15,
events: function (start, end, timezone, callback) {
$.ajax({
url: "/calendar/getmeetings/",
type: 'GET',
dataType: 'json',
success: function (start, end, timezone, callback) {
alert('success');
},
error: function (start, end, timezone, callback) {
alert('there was an error while fetching events!');
},
data: {
// our hypothetical feed requires UNIX timestamps
start: start.unix(),
end: end.unix()
}
});
}
});
});
</script>
}
</head>
<br />
<div class="jumbotron">
<h1>Event Calendar</h1>
<div id="calendar"></div>
</div>
You are not following the tutorial particularly closely, I note, and are using a different methodology to fetch the events.
Within that, you simply forgot to pass the event list back to fullCalendar. You need to execute the callback function which fullCalendar provided to you, and supply your events to it.
Your definition of "success" and "error"'s callback parameters are nonsense, btw - you need to check the jQuery $.ajax documentation (http://api.jquery.com/jquery.ajax/) to see what is provided:
success: function (response) { //response will contain the JSON data from the server
callback(response); //pass the data to fullCalendar. You can pass "response" directly, assuming the response directly contains a JSON array of events
},
error: function(jqXHR) {
alert(jqXHR.responseText);
}
See https://fullcalendar.io/docs/event_data/events_function/ for more details.
You are not passing the events in a right way try passing the events my way its simple.If in case u still didn't get the calendar u need to check the console for some exception. Before passing the events u need the push the data to events.
event_array.push({
userid: v.UserId,
start: moment(v.LoginTime),
//end: moment(v.LogoutTime)
//start: moment(v.start),
end: v.LogoutTime != null ? moment(v.LogoutTime) : null
//color: v.themecolor,
//allday: v.isfullday
});
and then u need to call that in calender id for example
function GenerateCalender(event_array) {
debugger;
//$('#calender').fullCalendar('destroy');
$('#calender').fullCalendar({
events: event_array
});
}
This is my full code-
var event_array = [];
var event_array = [];
var selectedEvent = null;
FetchEventAndRenderCalendar();
function FetchEventAndRenderCalendar() {
events = [];
$.ajax({
url: "/Home/GetEvents",
data: "",
type: "GET",
dataType: "json",
async: false,
cache: false,
success: function (data) {
alert("success");
$.each(data, function (i, v) {
event_array.push({
userid: v.UserId,
start: moment(v.LoginTime),
//end: moment(v.LogoutTime)
//start: moment(v.start),
end: v.LogoutTime != null ? moment(v.LogoutTime) : null
//color: v.themecolor,
//allday: v.isfullday
});
})
GenerateCalender(event_array);
},
error: function (error) {
alert('failed');
}
})
}
function GenerateCalender(event_array) {
debugger;
//$('#calender').fullCalendar('destroy');
$('#calender').fullCalendar({
events: event_array
});
}

Select2 - infinite scroll not loading next page with remote data

I am using Select2 4.0.1, I have used ajax to populate the result based on users input, but whenever I search for anything select2 lists first page result, but consecutive pages were not loading, also request is made for 2nd page on scroll. seems to be I am missing something.
$multiselect = $(element).select2({
closeOnSelect: false,
multiple: true,
placeholder: 'Assign a new tag',
tags: true,
tokenSeparators: [","],
ajax: {
url: '/search_url',
dataType: 'json',
type: 'GET',
delay: 250,
data: function(params) {
return {
search: params.term,
page: params.page
};
},
processResults: function(data, params) {
var more, new_data;
params.page = params.page || 1;
more = {
more: (params.page * 20) < data.total_count
};
new_data = [];
data.items.forEach(function(i, item) {
new_data.push({
id: i.name,
text: i.name
});
});
return {
pagination: more,
results: new_data
};
},
cache: true
}
})
Any help is much appreciated.Thnx:)
This is the code I got working last week. I am using a different transport on my end, but that shouldn't make a difference. I was having the same issue as you regarding the lack of paging working while scrolling. My issue ended up being that I didn't have the proper {'pagination':{'more':true}} format in my processResults function. The only thing I can see that may work for you is to "fix" the page count in the data function vs. the processResults function.
When you scroll to the bottom of your list, do you see the "Loading more results..." label? Have you attempted to hard code the more value to true while debugging?
this.$(".select2").select2({
'ajax': {
'transport': function (params, success, failure) {
var page = (params.data && params.data.page) || 1;
app.do('entity:list:search',{'types':['locations'],'branch':branch,'limit':100,'page':page,'term':params.data.term})
.done(function(locations) {
success({'results':locations,'more':(locations.length>=100)});
});
}
, 'delay': 250
, 'data':function (params) {
var query = {
'term': params.term
, 'page': params.page || 1
};
return query;
}
, 'processResults': function (data) {
return {
'results': data.results
, 'pagination': {
'more': data.more
}
};
}
}
, 'templateResult': that.formatResult
, 'templateSelection': that.formatSelection
, 'escapeMarkup': function(m) { return m; }
});

FullCalendar: events not rendering initially from function call (AJAX)

I've configured my FullCalendar to pull its events from an AJAX request, but they don't render on the calendar when the page is first loaded.
$(document).ready(function() {
sh1client = new Array();
sh2client = new Array();
$('#sh1_cal').fullCalendar({
height: 1000,
minTime:'9:00am',
maxTime:'5:00pm',
editable: false,
events: function(start, end, callback) {
$.ajax({
type: 'GET',
url: 'http://localhost:8080/getEvents',
dataType: 'json',
success: function(reply) {
console.log(reply.first);
callback(reply.first);
}
});
}
});
$("#sh1_cal").fullCalendar('addEventSource', sh1client); // these are the clientside arrays
});
And on the server,
app.get('/getEvents', function(req, res){
console.log('Server: passing events...');
var arrays = {first: sh1, second: sh2}
var pack = JSON.stringify(arrays)
res.writeHead(200, {'Access-Control-Allow-Origin' : '*', 'Content-Type': 'application/json'});
res.end(pack);
});
Is there any reason these events wouldn't initially load? Everything seems to be being passed through alright, but it's like the callback isn't working or something.
EDIT: Here is another approach I tried
events: {
url: 'http://localhost:8080/getEvents',
type: 'GET',
error: function() {
alert('there was an error while fetching events!');
},
success: function(reply) {
console.log(reply);
//callback(reply.first);
},
color: 'yellow', // a non-ajax option
textColor: 'black' // a non-ajax option
}
EDIT: JavaScript console shows this as being POSTed to the page as soon as it loads (this is the first object in an array:
[Object]
allDay: "false"
end: "1392129000"
id: "phil#google.com"
room: "Sh1"
start: "1392127200"
title: "Phil - Google"
__proto__: Object
length: 1
__proto__: Array[0]
Instead of using your own ajax call, have you tried using fullcalendars?
http://arshaw.com/fullcalendar/docs/event_data/events_json_feed/
Fullcalendar defaults the dataType as JSON and caching to false.
Combined some of your code with code from doc:
$('#calendar').fullCalendar({
events: {
url: 'http://localhost:8080/getEvents',
type: 'GET',
error: function() {
alert('there was an error while fetching events!');
},
success: function(reply) {
console.log(reply.first);
callback(reply.first);
},
color: 'yellow', // a non-ajax option
textColor: 'black' // a non-ajax option
}
});
You can try just getting your JSON string cutting and pasting in and see if you can render without ajax call
events: [
{
end: 1392129000,
id: "phil#google.com",
room: "Sh1",
start: 1392127200,
title: "Phil - Google"
}]
You can also process the response:
$('#myCalendar').fullCalendar({
...
eventSources : [{
url: 'myUrl',
type: 'GET',
},
success: function(replyObject) {
var results = [];
var reply= replyObject.Results[0];
for(var index in reply) {
results.push(reply[index]);
}
return results;
}
}]
...

JSON + PHP + JQuery + Autocomplete problem

Only started today but I'm having massive problems trying to understand JSON/AJAX etc, I've gotten my code this far but am stumped on how to return the data being pulled by the AJAX request to the jQuery Auto complete function.
var autocomplete = new function() {
this.init = function() {
$('#insurance_destination').autocomplete({
source: lookup
});
}
function lookup() {
$.ajax({
url: "scripts/php/autocomplete.php",
data: {
query: this.term
},
dataType: "json",
cache: false,
success: function(data) {
for (key in data) {
return {
label: key,
value: data[key][0]
}
}
}
});
}
}
And example of the JSON string being returned by a PHP script
{
"Uganda": ["UGA", "UK4", "Worldwide excluding USA, Canada and the Carribbean"]
}
Normally, you don't have to do ajax query yourself:
$('#insurance_destination').autocomplete('url_here', {options_here});
That's assuming we're talking about standard jquery autocomplete plugin. Do I understand you correctly?
edit
Check api
http://docs.jquery.com/Plugins/Autocomplete/autocomplete#url_or_dataoptions
There are also some examples.
This is the code I've ended up with, it works in Chrome and Firefox but not in IE 6/7...
var autocomplete = new function (){
this.init = function() {
$('#insurance_destination').autocomplete({
source: function(request, response) {
debugger;
$.ajax({
url: "scripts/php/autocomplete.php",
dataType: "json",
data: {query:this.term},
success: function(data) {
response($.map(data.countries, function(item) {
return {
label: '<strong>'+item.name+'</strong>'+' '+item.description,
value: item.name,
code : item.region
}
}))
}
})
},
minLength: 2,
select: function(event, ui) {
$('#insurance_iso_code_hidden').val(ui.item.code);
},
open: function() {
},
close: function() {
}
});
}
}

Categories