diff --git a/calendrier/static/css/add2calendar.css b/calendrier/static/css/add2calendar.css
new file mode 100644
index 0000000..bef7503
--- /dev/null
+++ b/calendrier/static/css/add2calendar.css
@@ -0,0 +1,38 @@
+.a2cldr {
+ height: 46px;
+ width: 246px;
+ position: relative;
+ color: #032c53;
+ font-family: sans-serif;
+}
+
+
+.a2cldr.active .a2cldr-list {
+ display: block;
+}
+.a2cldr-list {
+ position: relative;
+ z-index: 2;
+ padding: 0 12px;
+ display: none;
+ background-color: #000;
+ box-shadow: 0px 8px 20px 0px #bababa;
+}
+.a2cldr-item {
+ list-style: none;
+ padding: 14px 0;
+ border-bottom: 1px solid #f3f3f3;
+}
+.a2cldr-item a {
+ color: #032c53;
+ text-decoration: none;
+ width: 100%;
+ display: block;
+}
+.a2cldr-item a:hover {
+ color: #032c53;
+ text-decoration: underline;
+}
+.a2cldr-item:last-child {
+ border-bottom: 0;
+}
diff --git a/calendrier/static/js/add2calendar.js b/calendrier/static/js/add2calendar.js
new file mode 100644
index 0000000..4aecb6d
--- /dev/null
+++ b/calendrier/static/js/add2calendar.js
@@ -0,0 +1,683 @@
+/* global module */
+// prefix: a2cldr
+
+var Add2Calendar = function(eventData) {
+
+ /*================================================================ Util
+ */
+
+ /**
+ * Check if the element has this class name
+ *
+ * @param {[type]} ele
+ * @param {String} cls
+ * @return {Boolean}
+ */
+ this.hasClass = function(ele, cls) {
+ return (' ' + ele.className + ' ').indexOf(' ' + cls + ' ') > -1;
+ };
+
+ this.mergeObj = function(obj1, obj2) {
+ var result = {}
+
+ for (var attr1 in obj1) { result[attr1] = obj1[attr1]; }
+ for (var attr2 in obj2) { result[attr2] = obj2[attr2]; }
+
+ return result;
+ };
+
+ /**
+ * @see https://stackoverflow.com/questions/2998784/how-to-output-numbers-with-leading-zeros-in-javascript
+ * @param {Number} number
+ * @param {Number} size
+ */
+ this.pad = function(number, size) {
+ var num = number.toString();
+ while (num.length < size) num = "0" + num;
+ return num;
+ };
+
+ /**
+ * formatTime
+ *
+ * @todo change naming
+ * @param {Date} date
+ * @return {string} e.g. "20191223T110000Z", "20191223T230000Z"
+ */
+ this.formatTime = function(date) {
+ return date.toISOString().replace(/-|:|\.\d+/g, '');
+ };
+
+ /**
+ * [formatTime2 description]
+ * this method ignore timezone
+ *
+ * @see https://stackoverflow.com/questions/43670062/get-iso-string-from-a-date-without-time-zone
+ * @todo change naming
+ * @param {Date} date
+ * @return {string} e.g. "20191223", "20191224"
+ */
+ this.formatTime2 = function(date) {
+ return this.pad(date.getFullYear(), 4) + this.pad(date.getMonth() + 1, 2) + this.pad(date.getDate(), 2)
+ };
+
+ /**
+ * [isValidEventData description]
+ * UNUSED, UNCOMPLETE
+ * TODO
+ * - validate `eventData`
+ * - require only `start`
+ * - validate both single and multi
+ *
+ * @see http://stackoverflow.com/questions/5812220/how-to-validate-a-date
+ * @see http://stackoverflow.com/questions/1353684/detecting-an-invalid-date-date-instance-in-javascript
+ *
+ * @param {[type]} args [description]
+ * @return {Boolean} [description]
+ */
+ this.isValidEventData = function(eventData) {
+ if (this.isSingleEvent) {
+ // HACK
+ return true;
+
+ } else {
+ if (eventData.length > 0) {
+ // HACK
+ return true;
+ }
+ }
+
+ return false;
+ };
+
+ this.isObjectType = function(obj, type) {
+ return Object.prototype.toString.call(obj) === '[object ' + type + ']';
+ };
+
+ /**
+ * [isDateObject description]
+ * UNUSED
+ *
+ * @see http://stackoverflow.com/questions/643782/how-to-check-whether-an-object-is-a-date
+ *
+ * @param {[type]} obj [description]
+ * @return {Boolean} [description]
+ */
+ this.isDateObject = function(obj) {
+ return this.isObjectType(obj, 'Date');
+ };
+
+ this.isArray = function(obj) {
+ return this.isObjectType(obj, 'Array');
+ };
+
+ this.isFunc = function(obj) {
+ return this.isObjectType(obj, 'Function');
+ };
+
+ /**
+ * [serialize description]
+ * Object to query string (encode)
+ *
+ * @see http://stackoverflow.com/questions/1714786/querystring-encoding-of-a-javascript-object
+ *
+ * @param {[type]} obj [description]
+ * @return {[type]} [description]
+ */
+ this.serialize = function(obj) {
+ var str = [];
+ for (var p in obj) {
+ // eslint-disable-next-line
+ if (obj.hasOwnProperty(p)) {
+ str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
+ }
+ }
+
+ return str.join('&');
+ };
+
+ /**
+ * [replaceSpecialCharacterAndSpaceWithHyphen description]
+ *
+ * @see http://stackoverflow.com/questions/18936483/regex-for-replacing-all-special-characters-and-spaces-in-a-string-with-hyphens
+ *
+ * @param {[type]} str [description]
+ * @return {[type]} [description]
+ */
+ this.replaceSpecialCharacterAndSpaceWithHyphen = function(str) {
+ // eslint-disable-next-line
+ return str.replace(/([~!@#$%^&*()_+=`{}\[\]\|\\:;'<>,.\/? ])+/g, '-').replace(/^(-)+|(-)+$/g,'');
+ };
+
+ this.getLinkHtml = function(text, url, customClass, isEnableDownloadAttr, uniqueId) {
+ if (typeof isEnableDownloadAttr === 'undefined') { isEnableDownloadAttr = false; }
+ if (typeof uniqueId === 'undefined') { uniqueId = this.getCurrentUtcTimestamp(); }
+
+ var downloadAttr = '';
+
+ if (isEnableDownloadAttr) {
+ var fileName = 'add2Calendar-' + this.replaceSpecialCharacterAndSpaceWithHyphen(text).toLowerCase() + '-' + uniqueId;
+
+ downloadAttr = ' download="' + fileName + '" ';
+ }
+
+ return '' + text + ' ';
+ };
+
+ /**
+ * [getLiHtml description]
+ *
+ * @see http://stackoverflow.com/questions/417142/what-is-the-maximum-length-of-a-url-in-different-browsers
+ *
+ * @param {[type]} text [description]
+ * @param {[type]} url [description]
+ * @param {[type]} customClass [description]
+ * @param {Boolean} isEnableDownloadAttr [description]
+ * @return {[type]} [description]
+ */
+ this.getLiHtml = function(text, url, customClass, isEnableDownloadAttr, uniqueId) {
+ var result = '',
+ isValid = false;
+
+ // Validate
+ if (url) {
+ if (customClass === 'ical' || customClass === 'outlook') {
+ isValid = true;
+
+ } else {
+ var urlLength = url.length;
+
+ if (urlLength <= 20000) {
+ isValid = true;
+
+ } else {
+ console.log('Url longer than 2000');
+ }
+ }
+ }
+
+ if (isValid) {
+ var linkHtml = this.getLinkHtml(text, url, 'icon-' + customClass, isEnableDownloadAttr, uniqueId);
+ result = '
' + linkHtml + ' ';
+ }
+
+ return result;
+ };
+
+ this.getCurrentUtcTimestamp = function() {
+ return Date.now();
+ };
+
+ /*================================================================ Google
+ */
+
+ /**
+ * @todo take an arguments and return it instead of doing internal manipulation
+ */
+ this.updateGoogleUrl = function() {
+ if (this.isSingleEvent) {
+ var startDate = this.eventData.isAllDay
+ ? this.formatTime2(new Date(this.eventData.start))
+ : this.formatTime(new Date(this.eventData.start));
+ var endDate = this.eventData.isAllDay
+ ? this.formatTime2(new Date(this.eventData.end))
+ : this.formatTime(new Date(this.eventData.end));
+
+ var googleArgs = {
+ 'text' : (this.eventData.title || ''),
+ 'dates' : startDate + '/' + endDate,
+ 'location' : (this.eventData.location || ''),
+ 'details' : (this.eventData.description || ''),
+ 'ctz' : (this.eventData.timezone || ''),
+ 'locale' : (this.eventData.locale || ''),
+ 'sprop' : ''
+ };
+
+ this.googleUrl = 'https://www.google.com/calendar/render?action=TEMPLATE&' + this.serialize(googleArgs);
+ }
+
+ return this.googleUrl;
+ }
+
+ this.getGoogleUrl = function() {
+ return this.googleUrl;
+ };
+
+ this.getGoogleLiHtml = function() {
+ return this.getLiHtml('Google', this.googleUrl, 'google');
+ };
+
+ this.openGoogle = function() {
+ window.open(this.googleUrl);
+ };
+
+ /*================================================================ iCal / Outlook
+ */
+
+ /**
+ * @todo take an arguments and return it instead of doing internal manipulation
+ */
+ this.updateICalUrl = function() {
+ var url = typeof document !== 'undefined' ? document.URL : ''; // todo fix it
+ var startDate = ''
+ var endDate = ''
+
+ if (this.isSingleEvent) {
+ startDate = this.eventData.isAllDay
+ ? this.formatTime2(new Date(this.eventData.start))
+ : this.formatTime(new Date(this.eventData.start));
+ endDate = this.eventData.isAllDay
+ ? this.formatTime2(new Date(this.eventData.end))
+ : this.formatTime(new Date(this.eventData.end));
+
+ this.iCalUrl = encodeURI(
+ 'data:text/calendar;charset=utf8,' +
+ [
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0',
+ 'BEGIN:VEVENT',
+ 'URL:' + url,
+ 'DTSTART:' + startDate,
+ 'DTEND:' + endDate,
+ 'SUMMARY:' + (this.eventData.title || ''),
+ 'DESCRIPTION:' + (this.eventData.description || ''),
+ 'LOCATION:' + (this.eventData.location || ''),
+ 'END:VEVENT',
+ 'END:VCALENDAR'
+ ].join('\n')
+ );
+
+ } else {
+ var i = 0,
+ n = this.eventData.length;
+
+ var iCalData = [];
+ for (i = 0; i < n; i++) {
+ var data = this.eventData[i];
+ startDate = this.eventData.isAllDay
+ ? this.formatTime2(new Date(data.start))
+ : this.formatTime(new Date(data.start));
+ endDate = this.eventData.isAllDay
+ ? this.formatTime2(new Date(data.end))
+ : this.formatTime(new Date(data.end));
+
+ var tmp = [
+ 'BEGIN:VEVENT',
+ 'URL:' + url,
+ 'DTSTART:' + startDate,
+ 'DTEND:' + endDate,
+ 'SUMMARY:' + (data.title || ''),
+ 'DESCRIPTION:' + (data.description || ''),
+ 'LOCATION:' + (data.location || ''),
+ 'END:VEVENT',
+ ];
+
+ iCalData = iCalData.concat(tmp);
+ }
+
+ var iCalDataBegin = [
+ 'BEGIN:VCALENDAR',
+ 'VERSION:2.0'
+ ];
+ var iCalDataAfter = [
+ 'END:VCALENDAR'
+ ];
+ iCalData = iCalDataBegin.concat(iCalData, iCalDataAfter).join('\n');
+
+ this.iCalUrl = encodeURI('data:text/calendar;charset=utf8,' + iCalData);
+ }
+
+ // data is truncated when it contains a "#" character
+ // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
+ // https://en.wikipedia.org/wiki/Percent-encoding
+ this.iCalUrl = this.iCalUrl.replace(/#/g, '%23');
+
+ return this.iCalUrl;
+ };
+
+ this.getICalUrl = function() {
+ return this.iCalUrl;
+ };
+
+ this.getICalLiHtml = function() {
+ return this.getLiHtml('iCal', this.iCalUrl, 'ical', true);
+ };
+
+ this.openICal = function() {
+ window.open(this.iCalUrl);
+ };
+
+ // Same as getICalUrl
+ this.getOutlookUrl = function() {
+ return this.iCalUrl;
+ };
+
+ // Same as getICalLiHtml
+ this.getOutlookLiHtml = function() {
+ return this.getLiHtml('Outlook', this.iCalUrl, 'outlook', true);
+ };
+
+ // Same as openICal
+ this.openOutlook = function() {
+ window.open(this.iCalUrl);
+ };
+
+ /*================================================================ Outlook Online
+ */
+
+ /**
+ * @todo complete it
+ * @todo take an arguments and return it instead of doing internal manipulation
+ */
+ this.updateOutlookOnlineUrl = function() {
+ if (this.isSingleEvent) {
+ var startDate = new Date(this.eventData.start),
+ endDate = new Date(this.eventData.end);
+
+ var startDateTimezoneOffset = startDate.getTimezoneOffset();
+ startDate.setMinutes(startDate.getMinutes() - 2 * startDateTimezoneOffset); // HACK
+
+ var endDateTimezoneOffset = endDate.getTimezoneOffset();
+ endDate.setMinutes(endDate.getMinutes() - endDateTimezoneOffset); // HACK
+
+ startDate = this.formatTime(startDate).slice(0, -1);
+ endDate = this.formatTime(endDate).slice(0, -1);
+
+ var outlookOnlineArgs = {
+ 'summary' : (this.eventData.title || ''),
+ 'dtstart' : startDate,
+ 'dtend' : endDate,
+ 'location' : (this.eventData.location || ''),
+ 'description' : (this.eventData.description || '')
+ };
+
+ this.outlookOnlineUrl = 'http://calendar.live.com/calendar/calendar.aspx?rru=addevent&' + this.serialize(outlookOnlineArgs);
+ }
+
+ return this.outlookOnlineUrl;
+ };
+
+ this.getOutlookOnlineUrl = function() {
+ return this.outlookOnlineUrl;
+ };
+
+ this.getOutlookOnlineLiHtml = function() {
+ return this.getLiHtml('Outlook Online', this.outlookOnlineUrl, 'outlook-online');
+ };
+
+ this.openOutlookOnline = function() {
+ window.open(this.outlookOnlineUrl);
+ };
+
+ /*================================================================ Yahoo
+ */
+
+ /**
+ * @todo take an arguments and return it instead of doing internal manipulation
+ */
+ this.updateYahooUrl = function() {
+ if (this.isSingleEvent) {
+ var startDate = this.eventData.isAllDay
+ ? this.formatTime2(new Date(this.eventData.start))
+ : this.formatTime(new Date(this.eventData.start))
+
+ // FIXED: Yahoo! calendar bug
+ //
+ // Yahoo! did calculate timezone for `start`
+ // but they did not calculate timezone for `end`
+ var tmp = new Date(this.eventData.end);
+ var timezoneOffset = tmp.getTimezoneOffset();
+ tmp.setMinutes(tmp.getMinutes() - timezoneOffset);
+ var endDate = this.eventData.isAllDay
+ ? this.formatTime2(tmp)
+ : this.formatTime(tmp)
+
+ var yahooArgs = {
+ 'view' : 'd',
+ 'type' : '20',
+ 'title' : (this.eventData.title || ''),
+ 'st' : startDate,
+ 'et' : endDate,
+ // 'dur' : '',
+ 'in_loc' : (this.eventData.location || ''),
+ 'desc' : (this.eventData.description || '')
+ };
+
+ this.yahooUrl = 'https://calendar.yahoo.com/?v=60&' + this.serialize(yahooArgs);
+ }
+
+ return this.yahooUrl;
+ };
+
+ this.getYahooUrl = function() {
+ return this.yahooUrl;
+ };
+
+ this.getYahooLiHtml = function() {
+ return this.getLiHtml('Yahoo!', this.yahooUrl, 'yahoo');
+ };
+
+ this.openYahoo = function() {
+ window.open(this.yahooUrl);
+ };
+
+ /*================================================================ Widget
+ */
+
+ this.getEventListHtml = function() {
+ var html = '';
+
+ html += this.getEventListItemsHtml();
+ html += ' ';
+
+ return html;
+ };
+
+ this.getEventListItemsHtml = function() {
+ var html = '';
+
+ html += this.getGoogleLiHtml();
+ html += this.getICalLiHtml();
+ html += this.getOutlookLiHtml();
+ html += this.getOutlookOnlineLiHtml();
+ html += this.getYahooLiHtml();
+
+ return html;
+ };
+
+ // UNUSED
+ // QUITE DUPLICATE
+ this.getEventNotFoundListHtml = function() {
+ var html = '';
+
+ html += this.getEventNotFoundListItemsHtml();
+ html += ' ';
+
+ return html;
+ };
+
+ // UNUSED
+ // QUITE DUPLICATE
+ this.getEventNotFoundListItemsHtml = function() {
+ var html = '';
+
+ html += '';
+ html += 'Not Found ';
+ html += ' ';
+
+ return html;
+ };
+
+ this.getWidgetNode = function() {
+ var html = '';
+ html += this.getWidgetBtnText();
+ html += ' ';
+ html += this.getEventListHtml();
+
+ var result = document.createElement('div');
+ result.innerHTML = html;
+ result.className = this.textDomain;
+
+ return result;
+ };
+
+ this.getWidgetBtnText = function() {
+ var result = (this.option.buttonText)
+ ? this.option.buttonText
+ : this.add2calendarBtnTextMap[this.option.lang];
+
+ return result;
+ };
+
+ /*================================================================ API (Public)
+ */
+
+ this.createWidget = function(selector, cb) {
+ this.selector = selector;
+ this.eWidget = document.querySelector(selector);
+
+ // create and append into the DOM
+ var node = this.getWidgetNode();
+ this.eWidget.appendChild(node);
+
+ // bind an event
+ this.eButton = document.querySelector(selector + ' > .a2cldr > .a2cldr-btn');
+ this.bindClickEvent();
+
+ // callback
+ if (this.isFunc(cb)) {
+ cb();
+ }
+ };
+
+ this.bindClickEvent = function() {
+ var activeClassName = 'active';
+ var self = this;
+ var ele = this.eButton;
+
+ ele.onclick = function() {
+ var parent = ele.parentNode;
+ if (self.hasClass(parent, activeClassName)) {
+ parent.classList.remove(activeClassName);
+ } else {
+ parent.classList.add(activeClassName);
+ }
+ }
+ };
+
+ this.unBindClickEvent = function() {
+ if (this.eButton && this.eButton.onclick) {
+ this.eButton.onclick = null;
+ }
+ };
+
+ this.setOption = function(option) {
+ this.userOption = option;
+ this.option = this.mergeObj(this.defaultOption, this.userOption);
+ };
+
+ this.resetOption = function() {
+ this.option = this.defaultOption;
+ };
+
+ /*================================================================ API (Public) - In progress
+ */
+
+ this.update = function(eventData) {
+ this.init(eventData);
+ };
+
+ this.updateWidget = function(eventData, cb) {
+ this.update(eventData);
+
+ var ele = document.querySelector(this.selector + ' .a2cldr-list');
+ ele.innerHTML = this.getEventListItemsHtml();
+
+ if (this.isFunc(cb)) {
+ cb();
+ }
+ };
+
+ /*================================================================ Global var
+ */
+
+ this.textDomain = 'a2cldr';
+ this.add2calendarBtnTextMap = {
+ 'en': 'Add to Calendar',
+ 'th': 'เพิ่มเข้าปฏิทิน',
+ 'jp': 'カレンダーに追加',
+ 'kr': '캘린더에 추가',
+ 'ja': 'カレンダーに追加',
+ 'cn': '添加到日历',
+ 'de': 'In den Kalender',
+ 'es': 'Añadir al Calendario',
+ 'fr': 'Ajouter au calendrier',
+ 'ru': 'Добавить в календарь'
+ };
+
+ this.isSingleEvent;
+
+ // constructor parameter
+ this.eventData;
+
+ this.selector;
+ this.eWidget;
+
+ // option
+ this.defaultOption;
+ this.userOption;
+ this.option;
+
+ this.googleUrl;
+ this.iCalUrl; // iCal and Outlook
+ this.yahooUrl;
+ this.outlookOnlineUrl;
+
+ /*================================================================ Init & Others
+ */
+
+ this.updateAllCalendars = function() {
+ this.updateGoogleUrl();
+ this.updateICalUrl();
+ this.updateYahooUrl();
+
+ // disabled@01112016-1146 - cause it's not working
+ // this.updateOutlookOnlineUrl();
+ };
+
+ this.init = function(eventData) {
+ this.isSingleEvent = ! this.isArray(eventData);
+
+ if (! this.isValidEventData(eventData)) {
+ console.log('Event data format is not valid');
+
+ return false;
+ }
+
+ this.eventData = eventData;
+
+ this.selector = '';
+ this.eWidget = null; // widget element
+ this.eButton = null; // button element to click for opening the list
+
+ this.defaultOption = {
+ lang: 'fr',
+ buttonText: '',
+ };
+ this.option = this.defaultOption;
+
+ this.googleUrl = '';
+ this.iCalUrl = ''; // iCal and Outlook
+ this.yahooUrl = '';
+ this.outlookOnlineUrl = '';
+
+ this.updateAllCalendars();
+ };
+
+ this.init(eventData);
+};
+
+if (typeof module !== 'undefined' &&
+ module.exports != null) {
+ module.exports = Add2Calendar
+}
diff --git a/calendrier/templates/calendrier/view_event.html b/calendrier/templates/calendrier/view_event.html
index 590e308..d1b7bcc 100644
--- a/calendrier/templates/calendrier/view_event.html
+++ b/calendrier/templates/calendrier/view_event.html
@@ -3,11 +3,13 @@
{% load autotranslate %}
{% get_current_language as current_language %}
{% block titre %}{{ event.nom.capitalize }}{% endblock %}
-
+{% block extrahead %}
+{% endblock %}
{% block content %}
+
{% if not user.profile.is_chef and not user.profile.is_chef_event %}
{% if chef_only %}
{% trans "Cet événement est encore en construction ! Reviens plus tard." %}
@@ -30,6 +32,7 @@
{% if user.is_authenticated and event.desc_users %}
+
{% autotranslate current_language event.desc_users event.desc_users_en %}
@@ -82,7 +85,7 @@
-
{% trans "Répondre à l'événement" %}
+
{% trans "Répondre à l'événement" %}
{% endif %}
@@ -221,3 +224,45 @@
{% endblock %}
+{% block script %}
+
+
+{% endblock %}
diff --git a/gestion/templates/gestion/base.html b/gestion/templates/gestion/base.html
index 8faaa20..4107643 100644
--- a/gestion/templates/gestion/base.html
+++ b/gestion/templates/gestion/base.html
@@ -17,6 +17,7 @@
{% block extrahead %}{% endblock %}
+
@@ -163,6 +164,7 @@
+