diff --git a/addons/website_event_track_online/__manifest__.py b/addons/website_event_track_online/__manifest__.py index a846a2942a7..d9f2e82d2d6 100644 --- a/addons/website_event_track_online/__manifest__.py +++ b/addons/website_event_track_online/__manifest__.py @@ -27,6 +27,7 @@ 'views/event_track_stage_views.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_templates.xml', + 'views/event_track_templates_agenda.xml', 'views/event_track_templates_reminder.xml', 'views/event_track_views.xml', 'views/event_track_tag_views.xml', diff --git a/addons/website_event_track_online/controllers/event_track.py b/addons/website_event_track_online/controllers/event_track.py index 675908b678f..9e36151e09d 100644 --- a/addons/website_event_track_online/controllers/event_track.py +++ b/addons/website_event_track_online/controllers/event_track.py @@ -2,8 +2,10 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. from werkzeug.exceptions import Forbidden, NotFound +from datetime import timedelta +import pytz -from odoo import exceptions, http +from odoo import exceptions, http, fields from odoo.addons.website_event_track.controllers.main import WebsiteEventTrackController from odoo.http import request @@ -16,6 +18,156 @@ class EventTrackOnlineController(WebsiteEventTrackController): tracks_sudo = tracks_sudo.filtered(lambda track: track.is_published or track.stage_id.is_accepted) return tracks_sudo + def time_slot_rounder(self, time, rounded_minutes): + """ Rounds to nearest hour by adding a timedelta hour if minute >= rounded_minutes + E.g. : If rounded_minutes = 15 -> 09:26:00 becomes 09:30:00 + 09:17:00 becomes 09:15:00 + """ + return (time.replace(second=0, microsecond=0, minute=0, hour=time.hour) + + timedelta(minutes=rounded_minutes * (time.minute // rounded_minutes))) + + def _split_track_by_days(self, track, local_tz): + """ + Based on the track start_date and the duration, + split the track duration into : + start_time by day : number of time slot (15 minutes) that the track takes on that day. + E.g. : start date = 01-01-2000 10:00 PM and duration = 3 hours + return { + 01-01-2000 10:00:00 PM: 8 (2 * 4), + 01-02-2000 00:00:00 AM: 4 (1 * 4) + } + Also return a set of all the time slots + """ + start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) + start_datetime = self.time_slot_rounder(start_date, 15) + end_datetime = self.time_slot_rounder(start_datetime + timedelta(hours=(track.duration or 0.25)), 15) + time_slots_count = int(((end_datetime - start_datetime).total_seconds() / 3600) * 4) + + time_slots_by_day_start_time = {start_datetime: 0} + for i in range(0, time_slots_count): + # If the new time slot is still on the current day + next_day = (start_datetime + timedelta(days=1)).date() + if (start_datetime + timedelta(minutes=15*i)).date() <= next_day: + time_slots_by_day_start_time[start_datetime] += 1 + else: + start_datetime = next_day.datetime() + time_slots_by_day_start_time[start_datetime] = 0 + + return time_slots_by_day_start_time + + def _get_occupied_cells(self, track, rowspan, locations, local_tz): + """ + In order to use only once the cells that the tracks will occupy, we need to reserve those cells + (time_slot, location) coordinate. Those coordinated will be given to the template to avoid adding + blank cells where already occupied by a track. + """ + occupied_cells = [] + + start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) + start_date = self.time_slot_rounder(start_date, 15) + for i in range(0, rowspan): + time_slot = start_date + timedelta(minutes=15*i) + if track.location_id: + occupied_cells.append((time_slot, track.location_id)) + # when no location, reserve all locations + else: + occupied_cells += [(time_slot, location) for location in locations if location] + + return occupied_cells + + def _prepare_calendar_values(self, event): + """ + Override that should completely replace original method in v14. + + This methods slit the day (max end time - min start time) into 15 minutes time slots. + For each time slot, we assign the tracks that start at this specific time slot, and we add the number + of time slot that the track covers (track duration / 15 min) + The calendar will be divided into rows of 15 min, and the talks will cover the corresponding number of rows + (15 min slots). + """ + event = event.with_context(tz=event.date_tz or 'UTC') + local_tz = pytz.timezone(event.date_tz or 'UTC') + lang_code = request.env.context.get('lang') + event_track_ids = self._event_agenda_get_tracks(event) + + locations = list(set(track.location_id for track in event_track_ids)) + locations.sort(key=lambda x: x.id) + + # First split day by day (based on start time) + time_slots_by_tracks = {track: self._split_track_by_days(track, local_tz) for track in event_track_ids} + + # extract all the tracks time slots + track_time_slots = set().union(*(time_slot.keys() for time_slot in [time_slots for time_slots in time_slots_by_tracks.values()])) + + # extract unique days + days = list(set(time_slot.date() for time_slot in track_time_slots)) + days.sort() + + # Create the dict that contains the tracks at the correct time_slots / locations coordinates + tracks_by_days = dict.fromkeys(days, 0) + time_slots_by_day = dict((day, dict(start=set(), end=set())) for day in days) + tracks_by_rounded_times = dict((time_slot, dict((location, {}) for location in locations)) for time_slot in track_time_slots) + for track, time_slots in time_slots_by_tracks.items(): + start_date = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz) + end_date = start_date + timedelta(hours=(track.duration or 0.25)) + + for time_slot, duration in time_slots.items(): + tracks_by_rounded_times[time_slot][track.location_id][track] = { + 'rowspan': duration, # rowspan + 'start_date': self._get_locale_time(start_date, lang_code), + 'end_date': self._get_locale_time(end_date, lang_code), + 'occupied_cells': self._get_occupied_cells(track, duration, locations, local_tz) + } + + # get all the time slots by day to determine the max duration of a day. + day = time_slot.date() + time_slots_by_day[day]['start'].add(time_slot) + time_slots_by_day[day]['end'].add(time_slot+timedelta(minutes=15*duration)) + tracks_by_days[day] += 1 + + # split days into 15 minutes time slots + global_time_slots_by_day = dict((day, {}) for day in days) + for day, time_slots in time_slots_by_day.items(): + start_time_slot = min(time_slots['start']) + end_time_slot = max(time_slots['end']) + + time_slots_count = int(((end_time_slot - start_time_slot).total_seconds() / 3600) * 4) + current_time_slot = start_time_slot + for i in range(0, time_slots_count + 1): + global_time_slots_by_day[day][current_time_slot] = tracks_by_rounded_times.get(current_time_slot, {}) + global_time_slots_by_day[day][current_time_slot]['formatted_time'] = self._get_locale_time(current_time_slot, lang_code) + current_time_slot = current_time_slot + timedelta(minutes=15) + + # count the number of tracks by days + tracks_by_days = dict.fromkeys(days, 0) + for track in event_track_ids: + track_day = fields.Datetime.from_string(track.date).replace(tzinfo=pytz.utc).astimezone(local_tz).date() + tracks_by_days[track_day] += 1 + + return { + 'days': days, + 'tracks_by_days': tracks_by_days, + 'time_slots': global_time_slots_by_day, + 'locations': locations + } + + @http.route(['''/event//agenda'''], type='http', auth="public", website=True, sitemap=False) + def event_agenda(self, event, tag=None, **post): + if not event.can_access_from_current_website(): + raise NotFound() + + event = event.with_context(tz=event.date_tz or 'UTC') + vals = { + 'event': event, + 'main_object': event, + 'tag': tag, + 'user_event_manager': request.env.user.has_group('event.group_event_manager'), + } + + vals.update(self._prepare_calendar_values(event)) + + return request.render("website_event_track_online.agenda_online", vals) + def _fetch_track(self, track_id, allow_is_accepted=False): track = request.env['event.track'].browse(track_id).exists() if not track: diff --git a/addons/website_event_track_online/static/src/scss/event_track_templates.scss b/addons/website_event_track_online/static/src/scss/event_track_templates.scss index d9fcb3786d8..1772d02ea91 100644 --- a/addons/website_event_track_online/static/src/scss/event_track_templates.scss +++ b/addons/website_event_track_online/static/src/scss/event_track_templates.scss @@ -43,6 +43,103 @@ } } +/* + * AGENDA + */ +.o_we_online_agenda { + overflow-x: hidden; + @media (max-width: map-get($grid-breakpoints, 'md')) { + overflow-x: scroll; + } + + table { + border-collapse: separate; + border-spacing: 0em 0em; + tr { + height: 15px; + line-height: 1em; + &.active { + td.active { + padding: 0em 0.5em; + font-size: smaller; + border-top: 1px solid lightgrey; + } + } + } + th.active, td:not(.active) { + background-color: #d3d3d36e; + } + th.position-sticky { + left: 0; + } + td { + height: 0px; + border: 0px; + &.active { + position: sticky; + left: 0; + min-width: 100px; + background-color: white; + } + div.o_we_agenda_card_content { + height: 100%; + span { + cursor: pointer; + } + } + .badge { + height: fit-content; + } + &.invisible { + visibility: visible !important; + opacity: 0.3; + } + &.o_we_agenda_time_slot_main { + border-top: 1px solid lightgrey; + } + &.o_we_agenda_time_slot_half { + border-top: 1px dashed lightgrey; + } + &.event_color_0 { + background-color: lightgrey; + } + &.event_color_1 { + background-color: rgba(240, 96, 80, 0.2); + } + &.event_color_2 { + background-color: rgba(244, 164, 96, 0.2); + } + &.event_color_3 { + background-color: rgba(247, 205, 31, 0.2); + } + &.event_color_4 { + background-color: rgba(108,193,237,0.2); + } + &.event_color_5 { + background-color: rgba(129,73,104,0.2); + } + &.event_color_6 { + background-color: rgba(235,126,127,0.2); + } + &.event_color_7 { + background-color: rgba(44,131,151,0.2); + } + &.event_color_8 { + background-color: rgba(71,85,119,0.2);; + } + &.event_color_9 { + background-color: rgba(214,20,95,0.2); + } + &.event_color_10 { + background-color: rgba(48,195,129,0.2); + } + &.event_color_11 { + background-color: rgba(147,101,184,0.2); + } + } + } +} + /* * EVENT TOOL: SPONSOR WIDGET */ diff --git a/addons/website_event_track_online/views/event_track_templates_agenda.xml b/addons/website_event_track_online/views/event_track_templates_agenda.xml new file mode 100644 index 00000000000..b6159f55f23 --- /dev/null +++ b/addons/website_event_track_online/views/event_track_templates_agenda.xml @@ -0,0 +1,190 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + +