Initial addition of the visual calendar (day view only).
This is the first step to include a visual calendar to Poulpe, only including a "day view" as of now (i.e. events hour-by-hour from one date to another). More views may be added at a later time. It is **NOT WORKING YET**! The CSS has been broken on this branch, and will have to be fixed before the calendar can work: * the CSS of the calendar needs to be adapted to the environment and design of Poulpe; * add actual links to enroll/un-enroll to an activity (cf. `Event` class in `calendar.js`); * other small tweaks :)? Finally, this view is likely to require the addition of start and end date change, so that an user can browse events over several days (cf. `setStartDate` and `setEndDate` methods of `Calendar` class in `calendar.js`). Note that this code should be better re-written (e.g. in Typescript, split between files, using more/better design patterns) at a later time. It should nonetheless be easy to fix it and use it right now (see above requirements for this).
This commit is contained in:
parent
d3e1943021
commit
d08a39307f
4 changed files with 1329 additions and 2 deletions
359
event/static/css/calendar.css
Normal file
359
event/static/css/calendar.css
Normal file
|
@ -0,0 +1,359 @@
|
|||
/* Calendar */
|
||||
|
||||
#cal-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
|
||||
font-family: "sans-serif";
|
||||
}
|
||||
|
||||
#cal-container,
|
||||
#cal-container * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Time slots */
|
||||
|
||||
#cal-container .cal-time-slot-container {
|
||||
display: grid;
|
||||
/* grid-template-columns: repeat(24, 1fr); */
|
||||
grid-template-rows: 30px auto;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot {
|
||||
border-right: 1px solid #EEE;
|
||||
background-color: #FAFAFA;
|
||||
}
|
||||
|
||||
/* #cal-container .cal-time-slot:hover {
|
||||
background-color: red;
|
||||
} */
|
||||
|
||||
#cal-container .cal-time-slot:nth-child(even) {
|
||||
background-color: #F4F4F4;
|
||||
}
|
||||
|
||||
/* #cal-container .cal-time-slot:nth-child(even):hover {
|
||||
background-color: #D9D9D9;
|
||||
} */
|
||||
|
||||
#cal-container .cal-time-slot:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot-hour {
|
||||
padding: 0 0 0 calc(100% - 0.9rem + 4px);
|
||||
background-color: #F9F9F9;/* #3D3D3D; */
|
||||
border-bottom: 1px solid #AAA;
|
||||
color: #333;
|
||||
font-size: 0.9rem;
|
||||
line-height: 30px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot-hour:nth-child(even) {
|
||||
background-color: #FCFCFC;/* #464646; */
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot-hour:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-time-slot.cal-new-day,
|
||||
#cal-container .cal-time-slot-hour.cal-new-day {
|
||||
border-left: 2px solid #AAA;
|
||||
}
|
||||
|
||||
/* Events */
|
||||
|
||||
#cal-container .cal-event-container {
|
||||
display: grid;
|
||||
/* grid-template-columns: repeat(24, 1fr); */
|
||||
grid-template-rows: repeat(12, auto);
|
||||
|
||||
position: absolute;
|
||||
top: 40px;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
#cal-container .cal-event {
|
||||
position: relative;
|
||||
margin: 2px 0;
|
||||
/* padding: 5px; */
|
||||
/* background-color: #EFEFEF; */
|
||||
border-radius: 3px;
|
||||
/* border: 1px solid #CCC; */
|
||||
border-width: 1px;
|
||||
border-style: solid;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
/* z-index: 500; */
|
||||
transition: 50ms ease-in;
|
||||
}
|
||||
|
||||
#cal-container .cal-event:hover {
|
||||
/* background-color: #FEDDDD; */
|
||||
}
|
||||
|
||||
#cal-container .cal-event > * {
|
||||
display: none;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-name,
|
||||
#cal-container .cal-event > .cal-event-location,
|
||||
#cal-container .cal-event > .cal-event-perm-count {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-location {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#cal-container .cal-event > .cal-event-perm-count {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-event:not(.cal-event-subscribed) > .cal-event-perm-count.cal-perms-missing {
|
||||
width: calc(100% - 10px);
|
||||
right: auto;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
background-color: #FFF;
|
||||
border: 2px solid #E44;
|
||||
color: #E44;
|
||||
font-weight: bold;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#cal-container .cal-event.cal-event-subscribed {
|
||||
border-width: 3px;
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
#cal-container .cal-event.cal-event-subscribed::after {
|
||||
content: "✔";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
padding: 1px;
|
||||
color: #fff;
|
||||
background-color: #000;
|
||||
border-top-right-radius: 3px;
|
||||
}
|
||||
|
||||
|
||||
/* Event details popup */
|
||||
|
||||
#cal-container .cal-event-details {
|
||||
position: absolute;
|
||||
min-height: 100px;
|
||||
min-width: 25%;
|
||||
max-width: 60%;
|
||||
padding: 20px;
|
||||
background-color: #333;
|
||||
color: #FFF;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 15px 50px rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details:after {
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-bottom-color: #333;
|
||||
border-width: 20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details.above-event:after {
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
border: solid transparent;
|
||||
content: " ";
|
||||
height: 0;
|
||||
width: 0;
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
border-top-color: #333;
|
||||
border-width: 20px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details * {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-close-button {
|
||||
width: 35px;
|
||||
height: 35px;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
font-size: 1.2rem;
|
||||
color: #BBB;
|
||||
transition: 100ms ease-out;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-close-button:hover {
|
||||
background-color: #484848;
|
||||
color: #EFEFEF;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details a,
|
||||
#cal-container .cal-event-details a:hover {
|
||||
color: #FFF;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name > h3 {
|
||||
margin: 0 0 25px 0;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
color: #FFF;
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name > h3::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 0 0 8px;
|
||||
vertical-align: middle;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAA90lEQVQoz2WRsS6DURiGn/yaaNqBySpGicFg6iJcAYuuNBaJpJEg6gLcADFIdWR0BZZKS4SBRTe3YFf/Y/j/c3LUe4Zz3vd78+U734uUJ7Ptg7k68NDpoIdyU9V3TzzwRdXt1HCq9pwR510W616oZ8Gwoe6KWFN1ScStogui3sRJ9uxYLd/n6hTuaCy3XHfNWuR6hM+OojBQdSXyJ0cZizwSsMo3kEc+ZCEjxZgxE8j4oBFZhRww8gaff4fEL3UuGfK4uG5L4VLVfvrNCrDJHfd0gSrXQB2AJvu0+Cm8HbXnbGw9seqwWH2z65Wv/8MKcfdVHaZx/wKtOg5kifzQhwAAAABJRU5ErkJggg==);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name:hover > h3 {
|
||||
margin-bottom: 5px;
|
||||
background-color: #484848;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name:hover > h3::after {
|
||||
content: "Cliquez pour afficher les détails.";
|
||||
display: block;
|
||||
width: auto;
|
||||
height: 15px;
|
||||
padding: 5px 0 0 0;
|
||||
font-size: 0.6rem;
|
||||
background-image: none;
|
||||
color: #DDD;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-name:hover + .cal-detail-close-button {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details td.cal-detail-label {
|
||||
padding: 0 10px 10px 0;
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details td.cal-detail-value {
|
||||
padding: 0 0 10px 10px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area {
|
||||
margin: 10px 0;
|
||||
padding: 10px;
|
||||
background-color: #DFDFDF;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-title {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count {
|
||||
margin: 0 10px 0 0;
|
||||
font-size: 1.7rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count.cal-perms-missing {
|
||||
color: #E44;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-count.cal-perms-full {
|
||||
color: #393;
|
||||
}
|
||||
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-subscription-switch {
|
||||
margin: 0 0 0 10px;
|
||||
padding: 10px;
|
||||
font-size: 1.35rem;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-perm-area .cal-detail-perm-nb-missing-perms {
|
||||
margin: 20px 0 0 0;
|
||||
padding: 5px;
|
||||
background-color: #FFF;
|
||||
border-radius: 4px;
|
||||
text-align: center;
|
||||
font-size: 1.1rem;
|
||||
color: #E44;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-description {
|
||||
color: #DDD;
|
||||
font-style: italic;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
#cal-container .cal-event-details .cal-detail-tag {
|
||||
display: inline-block;
|
||||
margin: 0 5px;
|
||||
padding: 5px;
|
||||
border: 1px solid #DDD;
|
||||
}
|
914
event/static/js/calendar.js
Normal file
914
event/static/js/calendar.js
Normal file
|
@ -0,0 +1,914 @@
|
|||
// Based on https://stackoverflow.com/a/15289883
|
||||
function computeDateDifferenceInHours (date1, date2) {
|
||||
const msPerHour = 60 * 60 * 1000;
|
||||
return Math.floor(Math.abs(date2.getTime() - date1.getTime()) / msPerHour);
|
||||
}
|
||||
|
||||
|
||||
|
||||
class Calendar {
|
||||
|
||||
constructor (calendarParameters = {}) {
|
||||
this.containerNode = calendarParameters.containerNode !== undefined
|
||||
? calendarParameters.containerNode
|
||||
: $("#cal-container");
|
||||
|
||||
this.eventContainerNode = null;
|
||||
this.timeSlotsContainerNode = null;
|
||||
this.eventDetailsContainerNode = null;
|
||||
|
||||
this.startDate = calendarParameters.startDate !== undefined
|
||||
? calendarParameters.startDate
|
||||
: new Date();
|
||||
this.endDate = calendarParameters.endDate !== undefined
|
||||
? calendarParameters.endDate
|
||||
: new Date(Date.now() + (24 * 60 * 60 * 1000));
|
||||
|
||||
this.nbHoursToDisplay = 0;
|
||||
this.firstHourToDisplay = 0;
|
||||
this.endHourToDisplay = 0;
|
||||
|
||||
this.events = [];
|
||||
|
||||
this.subscriptionURLFormat = calendarParameters.subscriptionURLFormat !== undefined
|
||||
? calendarParameters.subscriptionURLFormat
|
||||
: "";
|
||||
this.csrfToken = calendarParameters.csrfToken !== undefined
|
||||
? calendarParameters.csrfToken
|
||||
: "";
|
||||
|
||||
// Map from locations to their CSS styles
|
||||
this.locationStyles = new Map();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init () {
|
||||
this.updateHoursToDisplay();
|
||||
|
||||
this.createTimeSlotContainer();
|
||||
this.createEventContainer();
|
||||
this.createEventDetailsContainer();
|
||||
|
||||
this.updateEventContainerGridStyle();
|
||||
|
||||
this.createTimeSlots();
|
||||
this.createEvents();
|
||||
|
||||
this.createLocationStyles();
|
||||
this.applyLocationStylesAsCSS();
|
||||
this.updateEventLocationStyleID();
|
||||
|
||||
this.sortEventNodesByEndTimeAndLocation();
|
||||
}
|
||||
|
||||
|
||||
// Date change
|
||||
|
||||
setStartDate (newStartDate) {
|
||||
this.startDate = newStartDate;
|
||||
|
||||
this.updateHoursToDisplay();
|
||||
this.updateEventContainerGridStyle();
|
||||
this.updateTimeSlots();
|
||||
this.updateEventVisibilities();
|
||||
}
|
||||
|
||||
setEndDate (newEndDate) {
|
||||
this.endDate = newEndDate;
|
||||
|
||||
this.updateHoursToDisplay();
|
||||
this.updateEventContainerGridStyle();
|
||||
this.updateTimeSlots();
|
||||
this.updateEventVisibilities();
|
||||
}
|
||||
|
||||
updateHoursToDisplay () {
|
||||
this.startHourToDisplay = this.startDate.getHours();
|
||||
this.endHourToDisplay = this.endDate.getHours();
|
||||
|
||||
this.nbHoursToDisplay = computeDateDifferenceInHours(this.startDate, this.endDate);
|
||||
}
|
||||
|
||||
|
||||
// Time slots
|
||||
|
||||
createTimeSlotContainer () {
|
||||
this.timeSlotsContainerNode = $("<div>")
|
||||
.addClass("cal-time-slot-container")
|
||||
.appendTo(this.containerNode);
|
||||
}
|
||||
|
||||
createTimeSlots () {
|
||||
// Populate the container hour by hour
|
||||
let self = this;
|
||||
function getHourStringToDisplay (index, hour) {
|
||||
if (index === self.nbHoursToDisplay - 1
|
||||
|| hour === 23) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (hour >= 10) {
|
||||
return hour + 1;
|
||||
}
|
||||
else {
|
||||
return " " + (hour + 1);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < this.nbHoursToDisplay; i++) {
|
||||
let hour = (this.startHourToDisplay + i) % 24;
|
||||
|
||||
// Time slot hour
|
||||
let timeSlotHourNode = $("<div>")
|
||||
.addClass("cal-time-slot-hour")
|
||||
.css({
|
||||
"grid-column-start": `${i + 1}`,
|
||||
"grid-column-end" : "span 1",
|
||||
"grid-row-start" : "1",
|
||||
"grid-row-end" : "1"
|
||||
})
|
||||
.html(getHourStringToDisplay(i, hour))
|
||||
.prependTo(this.timeSlotsContainerNode);
|
||||
|
||||
// Time slot block
|
||||
let timeSlotBlockNode = $("<div>")
|
||||
.addClass("cal-time-slot")
|
||||
.css({
|
||||
"grid-column-start": `${i + 1}`,
|
||||
"grid-column-end" : "span 1",
|
||||
"grid-row-start" : "2",
|
||||
"grid-row-end" : "2"
|
||||
})
|
||||
.appendTo(this.timeSlotsContainerNode);
|
||||
|
||||
if (i > 0 && hour === 0) {
|
||||
timeSlotHourNode.addClass("cal-new-day");
|
||||
timeSlotBlockNode.addClass("cal-new-day");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTimeSlots () {
|
||||
this.timeSlotsContainerNode.empty();
|
||||
this.createTimeSlots();
|
||||
}
|
||||
|
||||
getHourSlotWidth () {
|
||||
return this.timeSlotsContainerNode.width() / this.nbHoursToDisplay;
|
||||
}
|
||||
|
||||
|
||||
// Events
|
||||
|
||||
createEventContainer () {
|
||||
this.eventContainerNode = $("<div>")
|
||||
.addClass("cal-event-container")
|
||||
.appendTo(this.containerNode);
|
||||
}
|
||||
|
||||
createEvents () {
|
||||
// Move all event nodes into the event container
|
||||
let eventElements = this.containerNode.find(".cal-event");
|
||||
eventElements.appendTo(this.eventContainerNode);
|
||||
|
||||
// Create event objects from them all
|
||||
for (let element of eventElements) {
|
||||
let newEvent = new Event($(element), this);
|
||||
this.events.push(newEvent);
|
||||
}
|
||||
}
|
||||
|
||||
updateEventContainerGridStyle () {
|
||||
this.eventContainerNode.css("grid-template-columns",
|
||||
`repeat(${this.nbHoursToDisplay}, ${100 / this.nbHoursToDisplay }%)`);
|
||||
}
|
||||
|
||||
updateEventVisibilities () {
|
||||
for (let event of this.events) {
|
||||
event.updateVisibility();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Event details
|
||||
|
||||
createEventDetailsContainer () {
|
||||
this.eventDetailsContainerNode = $("<div>")
|
||||
.addClass("cal-details-container")
|
||||
.appendTo(this.containerNode);
|
||||
}
|
||||
|
||||
|
||||
// Location styles
|
||||
|
||||
createLocationStyles () {
|
||||
let locationIndices = new Map();
|
||||
for (let event of this.events) {
|
||||
if (! locationIndices.has(event.location)) {
|
||||
locationIndices.set(event.location, [...locationIndices.keys()].length);
|
||||
}
|
||||
}
|
||||
|
||||
let nbUniqueLocations = [...locationIndices.keys()].length;
|
||||
|
||||
let styleID = 0;
|
||||
for (let [location, index] of locationIndices.entries()) {
|
||||
let hue = (index / (nbUniqueLocations + 1)) * 255;
|
||||
styleID += 1;
|
||||
|
||||
this.locationStyles.set(location, {
|
||||
id: styleID,
|
||||
|
||||
normal: [
|
||||
`background-color: hsl(${hue}, 60%, 80%);`,
|
||||
`border-color: hsl(${hue}, 50%, 50%);`,
|
||||
`color: #000;`
|
||||
],
|
||||
|
||||
hover: [
|
||||
`background-color: hsl(${hue}, 65%, 85%);`,
|
||||
`border-color: hsl(${hue}, 52%, 55%);`,
|
||||
`color: #000;`
|
||||
],
|
||||
|
||||
subscribed: [
|
||||
`background-color: hsl(${hue}, 75%, 75%);`,
|
||||
`border-color: hsl(${hue}, 60%, 50%);`,
|
||||
`color: #000;`
|
||||
],
|
||||
|
||||
selected: [
|
||||
`background-color: hsl(${hue}, 45%, 50%);`,
|
||||
`border-color: hsl(${hue}, 40%, 35%);`,
|
||||
`color: #FFF;`
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
applyLocationStylesAsCSS () {
|
||||
let styleNode = $("<style>");
|
||||
let styleNodeContent = "";
|
||||
|
||||
for (let styles of this.locationStyles.values()) {
|
||||
let normalRules = styles.normal.join("\n");
|
||||
styleNodeContent += `.cal-location-${styles.id} {${normalRules}}\n`;
|
||||
|
||||
let hoverRules = styles.hover.join("\n");
|
||||
styleNodeContent += `.cal-location-${styles.id}:hover {${hoverRules}}\n`;
|
||||
|
||||
let subscribedRules = styles.subscribed.join("\n");
|
||||
styleNodeContent += `.cal-location-${styles.id}.cal-event-subscribed {${subscribedRules}}\n`;
|
||||
|
||||
let selectedRules = styles.selected.join("\n");
|
||||
styleNodeContent += `.cal-location-${styles.id}.cal-selected {${selectedRules}}\n`;
|
||||
}
|
||||
|
||||
styleNode
|
||||
.html(styleNodeContent)
|
||||
.appendTo($("head"));
|
||||
}
|
||||
|
||||
updateEventLocationStyleID () {
|
||||
for (let event of this.events) {
|
||||
let style = this.locationStyles.get(event.location);
|
||||
event.setLocationStyleID(style.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Event node sorting
|
||||
|
||||
sortEventNodesByEndTimeAndLocation () {
|
||||
this.events.sort((event1, event2) => {
|
||||
return event2.endDate.getTime() - event1.endDate.getTime();
|
||||
});
|
||||
|
||||
this.events.sort((event1, event2) => {
|
||||
return event2.location.localeCompare(event1.location);
|
||||
});
|
||||
|
||||
this.eventContainerNode.detach();
|
||||
|
||||
for (let event of this.events) {
|
||||
event.node.prependTo(this.eventContainerNode);
|
||||
}
|
||||
|
||||
this.eventContainerNode.appendTo(this.containerNode);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class Event {
|
||||
|
||||
constructor (eventNode, calendar) {
|
||||
this.node = eventNode;
|
||||
|
||||
this.calendar = calendar;
|
||||
this.details = null;
|
||||
|
||||
this.detailsNodeRemovalTimer = null;
|
||||
|
||||
this.id = null;
|
||||
this.name = null;
|
||||
this.startDate = null;
|
||||
this.endDate = null;
|
||||
this.location = null;
|
||||
this.description = null;
|
||||
this.hasPerms = false;
|
||||
this.nbPerms = null;
|
||||
this.minNbPerms = null;
|
||||
this.maxNbPerms = null;
|
||||
this.subscribedByUser = false;
|
||||
this.tags = [];
|
||||
|
||||
this.displayed = false;
|
||||
this.selected = false;
|
||||
|
||||
this.locationStyleID = 0;
|
||||
|
||||
// TODO: move this elsewhere
|
||||
// Callback to display the details popup on click on the event node
|
||||
this.showDetailPopupOnClickCallback = (event) => {
|
||||
if ($(event.target).closest(".cal-event-details").length === 0) {
|
||||
this.details.showPopup(event);
|
||||
}
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
init () {
|
||||
this.parseAndSetID();
|
||||
this.parseAndSetName();
|
||||
this.parseAndSetDates();
|
||||
this.parseAndSetLocation();
|
||||
this.parseAndSetDescription();
|
||||
this.parseAndSetPermRelatedFields();
|
||||
this.parseAndSetTags();
|
||||
|
||||
this.addPermCounter();
|
||||
|
||||
this.createDetails();
|
||||
|
||||
this.updateVisibility();
|
||||
}
|
||||
|
||||
|
||||
// Event data parsers + setters
|
||||
|
||||
parseAndSetID () {
|
||||
this.id = this.node.find(".cal-event-id").text();
|
||||
}
|
||||
|
||||
parseAndSetName () {
|
||||
this.name = this.node.find(".cal-event-name").text();
|
||||
}
|
||||
|
||||
parseDate (dateString) {
|
||||
let regex = /(\d+)\/(\d+)\/(\d+) (\d+)\:(\d+)/g;
|
||||
let [day, month, year, hours, minutes] = regex
|
||||
.exec(dateString)
|
||||
.slice(1)
|
||||
.map((intString) => {
|
||||
return parseInt(intString);
|
||||
});
|
||||
|
||||
return new Date(year,
|
||||
month - 1,
|
||||
day,
|
||||
hours,
|
||||
minutes);
|
||||
}
|
||||
|
||||
parseAndSetStartDate () {
|
||||
let startDateString = this.node.find(".cal-event-start-date").text();
|
||||
let startDate = this.parseDate(startDateString);
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
parseAndSetEndDate () {
|
||||
let endDateString = this.node.find(".cal-event-end-date").text();
|
||||
let endDate = this.parseDate(endDateString);
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
parseAndSetDates () {
|
||||
this.parseAndSetStartDate();
|
||||
this.parseAndSetEndDate();
|
||||
}
|
||||
|
||||
parseAndSetLocation () {
|
||||
this.location = this.node.find(".cal-event-location").text();
|
||||
}
|
||||
|
||||
parseAndSetDescription () {
|
||||
this.description = this.node.find(".cal-event-description").text();
|
||||
}
|
||||
|
||||
parseAndSetPermRelatedFields () {
|
||||
let hasPerms = !! parseInt(this.node.find(".cal-event-has-perms").text());
|
||||
|
||||
if (hasPerms) {
|
||||
this.hasPerms = true;
|
||||
|
||||
this.nbPerms = parseInt(this.node.find(".cal-event-nb-perms").text());
|
||||
|
||||
this.minNbPerms = parseInt(this.node.find(".cal-event-min-nb-perms").text());
|
||||
this.maxNbPerms = parseInt(this.node.find(".cal-event-max-nb-perms").text());
|
||||
|
||||
this.subscribedByUser = !! parseInt(this.node.find(".cal-event-subscribed").text());
|
||||
}
|
||||
}
|
||||
|
||||
parseAndSetTags () {
|
||||
this.node.find(".cal-event-tag")
|
||||
.each((_, element) => {
|
||||
this.tags.push(element.innerText);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Event details
|
||||
|
||||
createDetails () {
|
||||
this.details = new EventDetails(this);
|
||||
}
|
||||
|
||||
startDisplayingDetailsPopupOnClick () {
|
||||
this.node.on("click", this.showDetailPopupOnClickCallback);
|
||||
}
|
||||
|
||||
stopDisplayingDetailsPopupOnClick () {
|
||||
this.node.off("click", this.showDetailPopupOnClickCallback);
|
||||
}
|
||||
|
||||
|
||||
// Event node content
|
||||
|
||||
addPermCounter () {
|
||||
if (! this.hasPerms) {
|
||||
return;
|
||||
}
|
||||
|
||||
let permCounterNode = $("<div>")
|
||||
.addClass("cal-event-perm-count")
|
||||
.appendTo(this.node);
|
||||
|
||||
this.updatePermCounter();
|
||||
}
|
||||
|
||||
updatePermCounter () {
|
||||
let permCounterNode = this.node.find(".cal-event-perm-count")
|
||||
.html(`<span>👤 ${this.nbPerms}/${this.maxNbPerms}</span>`);
|
||||
|
||||
if (this.minNbPerms > this.nbPerms) {
|
||||
permCounterNode.addClass("cal-perms-missing");
|
||||
}
|
||||
else {
|
||||
permCounterNode.removeClass("cal-perms-missing");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Grid position
|
||||
|
||||
// Assuming the events can appear in the calendar
|
||||
getGridStartColumn () {
|
||||
if (this.startDate.getTime() < this.calendar.startDate.getTime()) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 1 + computeDateDifferenceInHours(this.calendar.startDate, this.startDate);
|
||||
}
|
||||
|
||||
// Assuming the events can appear in the calendar
|
||||
getGridEndColumn () {
|
||||
if (this.endDate.getTime() > this.calendar.endDate.getTime()) {
|
||||
return 1 + this.calendar.nbHoursToDisplay;
|
||||
}
|
||||
|
||||
return 1 + this.calendar.nbHoursToDisplay
|
||||
- computeDateDifferenceInHours(this.endDate, this.calendar.endDate);
|
||||
}
|
||||
|
||||
updatePositionInGrid () {
|
||||
// Align on the grid according to the hours
|
||||
this.node.css({
|
||||
"grid-column-start": `${this.getGridStartColumn()}`,
|
||||
"grid-column-end" : `${this.getGridEndColumn()}`
|
||||
});
|
||||
|
||||
// Add left and right padding according to the minutes
|
||||
let columnWidth = this.calendar.getHourSlotWidth();
|
||||
|
||||
let startMinutes = this.startDate.getMinutes();
|
||||
let endMinutes = this.endDate.getMinutes();
|
||||
|
||||
let marginLeft = 0;
|
||||
if (this.startDate.getTime() >= this.calendar.startDate.getTime()
|
||||
&& startMinutes !== 0) {
|
||||
marginLeft = columnWidth * (startMinutes / 60);
|
||||
}
|
||||
|
||||
let marginRight = 0;
|
||||
if (this.endDate.getTime() <= this.calendar.endDate.getTime()
|
||||
&& endMinutes !== 0) {
|
||||
marginRight = columnWidth * ((60 - endMinutes) / 60);
|
||||
}
|
||||
|
||||
this.node.css({
|
||||
"margin-left" : marginLeft,
|
||||
"margin-right": marginRight
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Event style
|
||||
|
||||
updateNodeStyle () {
|
||||
if (this.subscribedByUser) {
|
||||
this.node.addClass("cal-event-subscribed");
|
||||
}
|
||||
else {
|
||||
this.node.removeClass("cal-event-subscribed");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Location style
|
||||
|
||||
setLocationStyleID (styleID) {
|
||||
this.locationStyleID = styleID;
|
||||
this.node.addClass(`cal-location-${styleID}`);
|
||||
}
|
||||
|
||||
|
||||
// Selection
|
||||
|
||||
select () {
|
||||
this.selected = true;
|
||||
this.node.addClass("cal-selected");
|
||||
}
|
||||
|
||||
deselect () {
|
||||
this.selected = false;
|
||||
this.node.removeClass("cal-selected");
|
||||
}
|
||||
|
||||
|
||||
// Visibility
|
||||
|
||||
updateVisibility () {
|
||||
// Hide events which cannot apear in the calendar time span
|
||||
if (this.calendar.startDate.getTime() >= this.endDate.getTime()
|
||||
|| this.calendar.endDate.getTime() <= this.startDate.getTime()) {
|
||||
this.stopDisplayingDetailsPopupOnClick();
|
||||
|
||||
this.node.hide();
|
||||
|
||||
this.displayed = false;
|
||||
}
|
||||
else {
|
||||
this.node.show();
|
||||
|
||||
this.updateNodeStyle();
|
||||
this.updatePositionInGrid();
|
||||
this.startDisplayingDetailsPopupOnClick();
|
||||
|
||||
this.displayed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class EventDetails {
|
||||
|
||||
constructor (event) {
|
||||
this.event = event;
|
||||
this.node = null;
|
||||
|
||||
// TODO: move this elsewhere
|
||||
// Callback to close the details popup on click outside the popup
|
||||
this.closePopupOnClickOutsideCallback = (event) => {
|
||||
if ($(event.target).closest(".cal-event-details").length === 0) {
|
||||
this.hidePopup();
|
||||
}
|
||||
};
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
|
||||
init () {
|
||||
// Create the container node
|
||||
this.node = $("<div>")
|
||||
.addClass("cal-event-details")
|
||||
.appendTo(this.event.calendar.eventDetailsContainerNode)
|
||||
.hide();
|
||||
|
||||
this.createAndAppendTitle();
|
||||
this.createAndAppendCloseButton();
|
||||
|
||||
// Table of (label, value) details
|
||||
let tableNode = $("<table>")
|
||||
.appendTo(this.node);
|
||||
|
||||
function addRowToDetailTable (label, value) {
|
||||
let rowNode = $("<tr>")
|
||||
.addClass("cal-detail-list")
|
||||
.appendTo(tableNode);
|
||||
|
||||
$("<td>")
|
||||
.addClass("cal-detail-label")
|
||||
.html(label)
|
||||
.appendTo(rowNode);
|
||||
|
||||
$("<td>")
|
||||
.addClass("cal-detail-value")
|
||||
.html(value)
|
||||
.appendTo(rowNode);
|
||||
}
|
||||
|
||||
this.createAndAppendLocation(addRowToDetailTable);
|
||||
this.createAndAppendStartDate(addRowToDetailTable);
|
||||
this.createAndAppendEndDate(addRowToDetailTable);
|
||||
this.createAndAppendDuration(addRowToDetailTable);
|
||||
|
||||
this.createAndAppendPermManagementArea();
|
||||
|
||||
this.createAndAppendDescription();
|
||||
this.createAndAppendTags();
|
||||
}
|
||||
|
||||
|
||||
createAndAppendCloseButton () {
|
||||
$("<button>")
|
||||
.attr("type", "button")
|
||||
.addClass("cal-detail-close-button")
|
||||
.html("✕") // cancelation cross
|
||||
.appendTo(this.node)
|
||||
.on("click", (event) => {
|
||||
this.hidePopup();
|
||||
});
|
||||
}
|
||||
|
||||
createAndAppendTitle () {
|
||||
let linkToEventPageNode = $("<a>")
|
||||
.attr("href", "TODO") // TODO
|
||||
.addClass("cal-detail-name")
|
||||
.appendTo(this.node);
|
||||
|
||||
$("<h3>")
|
||||
.html(this.event.name)
|
||||
.appendTo(linkToEventPageNode);
|
||||
}
|
||||
|
||||
createAndAppendLocation (addRowToDetailTable) {
|
||||
addRowToDetailTable("Emplacement", this.event.location);
|
||||
}
|
||||
|
||||
createAndAppendStartDate (addRowToDetailTable) {
|
||||
let startDateString = this.event.startDate
|
||||
.toLocaleDateString("fr-FR", {year: "2-digit", month: "2-digit", day: "2-digit"});
|
||||
let startTimeString = this.event.startDate
|
||||
.toLocaleDateString("fr-FR", {hour: "2-digit", minute: "2-digit"})
|
||||
.slice(-5);
|
||||
|
||||
addRowToDetailTable("Début", `Le ${startDateString} à ${startTimeString}`);
|
||||
}
|
||||
|
||||
createAndAppendEndDate (addRowToDetailTable) {
|
||||
let endDateString = this.event.endDate
|
||||
.toLocaleDateString("fr-FR", {year: "2-digit", month: "2-digit", day: "2-digit"});
|
||||
let endTimeString = this.event.endDate
|
||||
.toLocaleDateString("fr-FR", {hour: "2-digit", minute: "2-digit"})
|
||||
.slice(-5);
|
||||
|
||||
addRowToDetailTable("Fin", `Le ${endDateString} à ${endTimeString}`);
|
||||
}
|
||||
|
||||
createAndAppendDuration (addRowToDetailTable) {
|
||||
const msPerMinute = 60 * 1000;
|
||||
const msPerHour = 60 * msPerMinute;
|
||||
|
||||
let durationInMs = Math.abs(this.event.startDate.getTime() - this.event.endDate.getTime());
|
||||
|
||||
let hours = Math.floor(durationInMs / msPerHour);
|
||||
let minutes = Math.abs((durationInMs % msPerHour) / msPerMinute);
|
||||
|
||||
addRowToDetailTable("Durée", hours !== 0
|
||||
? `${hours}h ${minutes}min`
|
||||
: `${minutes}min`);
|
||||
}
|
||||
|
||||
createAndAppendPermManagementArea () {
|
||||
if (! this.event.hasPerms) {
|
||||
return;
|
||||
}
|
||||
|
||||
let permAreaNode = $("<div>")
|
||||
.addClass("cal-detail-perm-area")
|
||||
.appendTo(this.node);
|
||||
|
||||
let permTitleNode = this.createPermManagementTitle()
|
||||
.appendTo(permAreaNode);
|
||||
|
||||
let permCountNode = this.createPermManagementCounter()
|
||||
.appendTo(permAreaNode);
|
||||
|
||||
let permSubscribeButtonNode = this.createPermManagementSwitchButton()
|
||||
.appendTo(permAreaNode);
|
||||
|
||||
this.updatePermManagementArea();
|
||||
}
|
||||
|
||||
updatePermManagementArea () {
|
||||
let permAreaNode = this.node.find(".cal-detail-perm-area");
|
||||
|
||||
this.updatePermManagementCounter();
|
||||
this.updatePermManagementSwitchButton();
|
||||
|
||||
// Additional warning message if there are too little perms,
|
||||
// and if the user has not subscribed to this event
|
||||
permAreaNode
|
||||
.find(".cal-detail-perm-nb-missing-perms")
|
||||
.remove();
|
||||
|
||||
if (! this.event.subscribedByUser
|
||||
&& this.event.minNbPerms > this.event.nbPerms) {
|
||||
let nbMissingPerms = this.event.minNbPerms - this.event.nbPerms;
|
||||
let optionnalPluralMark = nbMissingPerms > 1 ? "s" : "";
|
||||
|
||||
$("<p>")
|
||||
.addClass("cal-detail-perm-nb-missing-perms")
|
||||
.html(`${nbMissingPerms} personne${optionnalPluralMark}</br>min. manquante${optionnalPluralMark} !`)
|
||||
.appendTo(permAreaNode);
|
||||
}
|
||||
}
|
||||
|
||||
createPermManagementTitle () {
|
||||
return $("<h4>")
|
||||
.addClass("cal-detail-perm-title")
|
||||
.html("Permanences");
|
||||
}
|
||||
|
||||
createPermManagementCounter () {
|
||||
return $("<span>")
|
||||
.addClass("cal-detail-perm-count")
|
||||
.html(`👤 ${this.event.nbPerms}/${this.event.maxNbPerms}`);
|
||||
}
|
||||
|
||||
updatePermManagementCounter () {
|
||||
let permCounterNode = this.node.find(".cal-detail-perm-count");
|
||||
|
||||
if (this.event.minNbPerms > this.event.nbPerms) {
|
||||
permCounterNode.addClass("cal-perms-missing");
|
||||
}
|
||||
else if (this.event.nbPerms === this.event.maxNbPerms) {
|
||||
permCounterNode.addClass("cal-perms-full");
|
||||
}
|
||||
}
|
||||
|
||||
createPermManagementSwitchButton () {
|
||||
let buttonNode = $("<button>")
|
||||
.addClass("cal-detail-perm-subscription-switch");
|
||||
|
||||
// On click, switch the subscription state
|
||||
// and update objects related to the perm count
|
||||
let event = this.event;
|
||||
buttonNode.on("click", () => {
|
||||
// TODO: subscribe or unsubscribe
|
||||
event.subscribedByUser = ! event.subscribedByUser;
|
||||
|
||||
this.updatePermManagementArea();
|
||||
event.updatePermCounter();
|
||||
});
|
||||
|
||||
return buttonNode;
|
||||
}
|
||||
|
||||
updatePermManagementSwitchButton () {
|
||||
let event = this.event;
|
||||
let buttonNode = this.node.find(".cal-detail-perm-subscription-switch");
|
||||
|
||||
// Reset the button state
|
||||
buttonNode.prop("disabled", false);
|
||||
|
||||
// Update its content and state
|
||||
if (event.subscribedByUser) {
|
||||
buttonNode.html("Se désinscrire");
|
||||
}
|
||||
else {
|
||||
buttonNode.html("S'inscrire");
|
||||
|
||||
// Disable the button if the maximum number of perms is already reached
|
||||
if (event.nbPerms === event.maxNbPerms) {
|
||||
buttonNode.prop("disabled", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createAndAppendDescription () {
|
||||
$("<p>")
|
||||
.addClass("cal-detail-description")
|
||||
.html(this.event.description)
|
||||
.appendTo(this.node);
|
||||
}
|
||||
|
||||
createAndAppendTags () {
|
||||
let tagContainerNode = $("<div>")
|
||||
.addClass("cal-detail-tag-list")
|
||||
.appendTo(this.node);
|
||||
|
||||
for (let tag of this.event.tags) {
|
||||
$("<span>")
|
||||
.addClass("cal-detail-tag")
|
||||
.html(tag)
|
||||
.appendTo(tagContainerNode);
|
||||
}
|
||||
}
|
||||
|
||||
setPopupPosition (mouseClickEvent) {
|
||||
let eventOffset = this.event.node.offset();
|
||||
let eventNodeHeight = this.event.node.outerHeight();
|
||||
|
||||
let detailNodeOffset = this.node.offset();
|
||||
let detailsNodeWidth = this.node.outerWidth();
|
||||
let detailsNodeHeight = this.node.outerHeight();
|
||||
|
||||
let x = mouseClickEvent.pageX - (detailsNodeWidth / 2);
|
||||
let y = eventOffset.top + eventNodeHeight + 10;
|
||||
|
||||
// If the popup is too high to vertically fit in the window,
|
||||
// it is displayed above the event instead
|
||||
let bottomMargin = 50; // px
|
||||
let detailsNodeBottom = eventOffset.top
|
||||
+ eventNodeHeight
|
||||
+ detailsNodeHeight;
|
||||
|
||||
if (detailsNodeBottom > window.innerHeight - bottomMargin) {
|
||||
y = eventOffset.top - detailsNodeHeight - 10;
|
||||
this.node.addClass("above-event")
|
||||
}
|
||||
|
||||
// If the popup is about to be displayed outside of
|
||||
// the left/right side of the screen, correct its future x position
|
||||
let sideMargin = 20; // px
|
||||
|
||||
if (x < sideMargin) {
|
||||
x += sideMargin - x;
|
||||
}
|
||||
else if (x + detailsNodeWidth > window.innerWidth - sideMargin) {
|
||||
x -= (x + detailsNodeWidth) - (window.innerWidth - sideMargin);
|
||||
}
|
||||
|
||||
this.node.css({
|
||||
"left": x,
|
||||
"top": y
|
||||
});
|
||||
}
|
||||
|
||||
showPopup (mouseClickEvent) {
|
||||
if (this.node.is(":visible")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.event.select();
|
||||
|
||||
this.node.show();
|
||||
this.setPopupPosition(mouseClickEvent);
|
||||
|
||||
// TODO: use a clean solution ;)
|
||||
window.setTimeout(() => {
|
||||
this.startHidingOnOutsideClick();
|
||||
}, 20);
|
||||
}
|
||||
|
||||
hidePopup () {
|
||||
this.event.deselect();
|
||||
|
||||
this.stopHidingOnOutsideClick();
|
||||
this.node.hide();
|
||||
}
|
||||
|
||||
// TODO: define the callback elsewhere
|
||||
|
||||
startHidingOnOutsideClick () {
|
||||
$(document).on("click", this.closePopupOnClickOutsideCallback);
|
||||
}
|
||||
|
||||
stopHidingOnOutsideClick () {
|
||||
$(document).off("click", this.closePopupOnClickOutsideCallback);
|
||||
}
|
||||
}
|
|
@ -1,12 +1,61 @@
|
|||
{% extends "shared/fluid.html" %}
|
||||
{% load i18n staticfiles event_tags %}
|
||||
|
||||
{% block extra_css %}
|
||||
{{ block.super }}
|
||||
<link rel="stylesheet" href="{% static "css/calendar.css" %}">
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{{ block.super }}
|
||||
<script type="text/javascript" src="{% static "js/enrol_event.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "js/calendar.js" %}"></script>
|
||||
|
||||
<script type="text/javascript">
|
||||
$(document).ready(() => {
|
||||
let cal = new Calendar({
|
||||
startDate: new Date(2018, 11, 1, 8),
|
||||
endDate: new Date(2018, 11, 2, 18),
|
||||
subscriptionURLFormat: "{% url "event:enrol_activity" 999999 %}?ajax=json",
|
||||
csrfToken: $(".planning [name=csrfmiddlewaretoken]").val()
|
||||
});
|
||||
|
||||
// Debug (to be removed when working)
|
||||
console.log(cal);
|
||||
window["cal"] = cal;
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
lolilol
|
||||
<div class="planning">
|
||||
{% csrf_token %}
|
||||
{% regroup activities by beginning|date:"Y-m-d" as days_list %}
|
||||
<div class="content fluid cal-container">
|
||||
{% for day in days_list %}
|
||||
{% for activity in day.list %}
|
||||
<div class="{% cycle "" "inverted" %} cal-event">
|
||||
<span class="cal-event-id">{{ activity.id }}</span>
|
||||
<span class="cal-event-name">{{ activity.id }}</span>
|
||||
<span class="cal-event-start-date">{{ activity.beginning | date:"j/m/Y H:i" }}</span>
|
||||
<span class="cal-event-end-date">{{ activity.end | date:"j/m/Y H:i" }}</span>
|
||||
<span class="cal-event-location">{{ activity.places.all | join:", " }}</span>
|
||||
<span class="cal-event-description">{{ activity.description }}</span>
|
||||
<span class="cal-event-url">{% url "event:activity" activity.id %}</span>
|
||||
|
||||
{% if activity.has_perm %}
|
||||
<span class="cal-event-has-perms">1</span>
|
||||
<span class="cal-event-min-nb-perms">{{ activity.min_perm }}</span>
|
||||
<span class="cal-event-max-nb-perms">{{ activity.max_perm }}</span>
|
||||
<span class="cal-event-nb-perms">{{ activity.staff.count }}</span>
|
||||
<span class="cal-event-subscribed">{% is_enrolled activity request.user %}</span>
|
||||
{% endif %}
|
||||
|
||||
{% for tag in activity.tags.all %}
|
||||
<span class="cal-event-tag">{{ tag.name }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
@ -14,3 +14,8 @@ def enrol_btn(activity, user):
|
|||
"enrolled": activity.staff.filter(id=user.id).exists(),
|
||||
"activity": activity,
|
||||
}
|
||||
|
||||
@register.simple_tag
|
||||
def is_enrolled(activity, user):
|
||||
user_is_enrolled = activity.staff.filter(id=user.id).exists()
|
||||
return "1" if user_is_enrolled else "0"
|
||||
|
|
Loading…
Reference in a new issue