You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python HTMLCalendar如何正确检测显示跨月重叠日历事件

Python HTMLCalendar 跨月事件渲染问题修复

问题背景

基于Python内置HTMLCalendar模块实现事件日历,需要支持起止时间跨月的事件调度(例如事件从6月开始,到7月结束的场景)。当前实现已支持当月起始、当月结束的事件渲染,6月内的事件显示效果符合预期,但跨月事件无法在结束月的对应日期单元格中展示剩余时段。

现有实现代码

HTMLCalendar 工具类

class Calendar(HTMLCalendar):
    def __init__(self, year=None, month=None, dropdown=None):
        self.dropdown = dropdown
        self.year = year
        self.month = month
        super(Calendar, self).__init__()

    # 格式化单个日期为td单元格,按日期过滤事件
    def formatday(self, day, requests):
        requests_per_day = requests.filter(start_time__day__lte=day,start_time__month=self.month)
        d = ''
        for request in requests_per_day:
            d += f'<li> {request.get_html_url} </li>'

        if day != 0:
            return f"<td><span class='date'>{day}</span><ul> {d} </ul></td>"
        return '<td></td>'

    # 格式化单周为tr行
    def formatweek(self, theweek, requests):
        week = ''
        for d, weekday in theweek:
            week += self.formatday(d, requests)
        return f'<tr> {week} </tr>'

    # 格式化整月为表格,按年月过滤事件
    def formatmonth(self, withyear=True):
        # 原有筛选逻辑(存在缺陷,仅拉取起始时间在当月的事件)
        requests = VacationRequest.objects.filter(vacation_calendar=self.dropdown,start_time__year=self.year,start_time__month=self.month)
        cal = f'<table border="0" cellpadding="0" cellspacing="0" class="calendar">\n'
        cal += f'{self.formatmonthname(self.year, self.month, withyear=withyear)}\n'
        cal += f'{self.formatweekheader()}\n'
        for week in self.monthdays2calendar(self.year, self.month):
            cal += f'{self.formatweek(week, requests)}\n'
        cal += f'</table>\n'
        cal += f'<h1 class="title107">Vacation Schedule For {self.formatmonthname(self.year, self.month, withyear=withyear)}</h1>\n'
        return cal

视图层代码

class VacationRequestCalendarView(LoginRequiredMixin,generic.ListView):
    model = VacationRequest
    template_name = 'vacation_request_calendar_view.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        dropdown = (self.request.GET.get("dropdown", None))
        d = get_date(self.request.GET.get('month', None))
        cal = Calendar(d.year, d.month, dropdown)
        cal.setfirstweekday(calendar.SUNDAY)
        html_cal = cal.formatmonth(withyear=True)
        vacation_calendar = VacationCalendar.objects.get(id=self.request.GET.get("dropdown", None))
        context['calendar'] = mark_safe(html_cal)
        context['dropdown'] = dropdown
        context['next_month'] = next_month(d)
        context['prev_month'] = prev_month(d)
        context['vacation_calendar'] = vacation_calendar
        return context

    def get_object(self, queryset=None):
        return get_object_or_404(VacationCalendar, id=self.request.GET.get("dropdown"))

    def get(self, request, *args, **kwargs):
        dropdown=self.request.GET.get("dropdown")
        if dropdown is not None:
            if VacationCalendar.objects.filter(id=dropdown,team_members=self.request.user):
                self.object_list = self.get_queryset()
                context = self.get_context_data(object=self.object_list)
                return self.render_to_response(context)
            else:
                raise Http404
        else:
            messages.add_message(self.request, messages.INFO, 'Vacation Calendar is required.')
            return HttpResponseRedirect(reverse('VacationRequests:vacation_request_by_calendar_name'))

def get_date(req_month):
    if req_month:
        year, month = (int(x) for x in req_month.split('-'))
        return date(year, month, day=1)
    return datetime.today()

def prev_month(d):
    first = d.replace(day=1)
    prev_month = first - timedelta(days=1)
    month = 'month=' + str(prev_month.year) + '-' + str(prev_month.month)
    return month

def next_month(d):
    days_in_month = calendar.monthrange(d.year, d.month)[1]
    last = d.replace(day=days_in_month)
    next_month = last + timedelta(days=1)
    month = 'month=' + str(next_month.year) + '-' + str(next_month.month)
    return month

问题根因

现有代码存在两处核心逻辑缺陷:

  1. 事件筛选范围错误:原有逻辑仅拉取start_time在当前渲染月份的事件,此前尝试的Q对象组合「起始在当月或结束在当月」的筛选逻辑,既无法覆盖跨多个整月的长事件,也没有解决单日匹配逻辑的缺陷,因此无法完全实现预期效果。
  2. 单日事件匹配逻辑错误:formatday方法仅判断事件的start_time__day <= 遍历日期,完全没有校验事件的结束时间是否覆盖当前日期,也没有处理跨月时的日期比较,导致次月的日期无法匹配到上月起始的跨月事件。

修复方案

  • 修正整月事件筛选逻辑:计算当前渲染月份的第一天和最后一天,拉取所有和当前月时间范围有重叠的事件,覆盖三类场景:
    • 事件起始时间在当月内
    • 事件结束时间在当月内
    • 事件起始早于当月第一天、结束晚于当月最后一天(跨整月的长事件)
  • 修正单日事件匹配逻辑:遍历每个日期单元格时,构造完整的日期对象,判断事件的起止时间范围是否覆盖该日期,避免仅比较日期数字导致的跨月匹配失效。

修改后的核心代码

from datetime import date
from django.db.models import Q

class Calendar(HTMLCalendar):
    def __init__(self, year=None, month=None, dropdown=None):
        self.dropdown = dropdown
        self.year = year
        self.month = month
        super(Calendar, self).__init__()

    def formatday(self, day, requests):
        d = ''
        if day != 0:
            # 构造当前遍历到的完整日期对象
            current_date = date(self.year, self.month, day)
            # 筛选所有覆盖当前日期的事件
            day_events = []
            for req in requests:
                # 取事件起止的日期部分,兼容带时区的datetime字段
                req_start = req.start_time.date() if hasattr(req.start_time, 'date') else req.start_time
                req_end = req.end_time.date() if hasattr(req.end_time, 'date') else req.end_time
                if req_start <= current_date <= req_end:
                    day_events.append(req)
            # 渲染事件列表
            for request in day_events:
                d += f'<li> {request.get_html_url} </li>'
            return f"<td><span class='date'>{day}</span><ul> {d} </ul></td>"
        return '<td></td>'

    def formatweek(self, theweek, requests):
        week = ''
        for d, weekday in theweek:
            week += self.formatday(d, requests)
        return f'<tr> {week} </tr>'

    def formatmonth(self, withyear=True):
        # 计算当月第一天和最后一天
        first_day = date(self.year, self.month, 1)
        last_day = date(self.year, self.month, calendar.monthrange(self.year, self.month)[1])
        # 筛选所有和当月时间范围有重叠的事件
        requests = VacationRequest.objects.filter(
            vacation_calendar=self.dropdown
        ).filter(
            start_time__date__lte=last_day,
            end_time__date__gte=first_day
        ).distinct().order_by('start_time')
        
        cal = f'<table border="0" cellpadding="0" cellspacing="0" class="calendar">\n'
        cal += f'{self.formatmonthname(self.year, self.month, withyear=withyear)}\n'
        cal += f'{self.formatweekheader()}\n'
        for week in self.monthdays2calendar(self.year, self.month):
            cal += f'{self.formatweek(week, requests)}\n'
        cal += f'</table>\n'
        cal += f'<h1 class="title107">Vacation Schedule For {self.formatmonthname(self.year, self.month, withyear=withyear)}</h1>\n'
        return cal

说明:如果使用Django 3.1以下版本,不支持__date查询表达式,可以直接取出事件列表后在Python层做日期判断,逻辑和formatday中的日期比较逻辑保持一致即可。


内容的提问来源于stack exchange,提问作者Steve Smith

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:36:22