From 0d443407a7fd240f2f3548186f24d457d6da7ec4 Mon Sep 17 00:00:00 2001 From: Lucie Galland Date: Wed, 13 Oct 2021 16:49:23 +0200 Subject: [PATCH] addtocalendar --- calendrier/static/css/add2calendar.css | 38 + calendrier/static/js/add2calendar.js | 683 ++++++++++++++++++ .../templates/calendrier/view_event.html | 49 +- gestion/templates/gestion/base.html | 2 + 4 files changed, 770 insertions(+), 2 deletions(-) create mode 100644 calendrier/static/css/add2calendar.css create mode 100644 calendrier/static/js/add2calendar.js 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 = ''; + + 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 = ''; + + return html; + }; + + // UNUSED + // QUITE DUPLICATE + this.getEventNotFoundListItemsHtml = function() { + var html = ''; + + html += '
  • '; + html += 'Not Found'; + html += '
  • '; + + return html; + }; + + this.getWidgetNode = function() { + var 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 @@ +