diff --git a/.github/ISSUE_TEMPLATE/BUG.yml b/.github/ISSUE_TEMPLATE/BUG.yml new file mode 100644 index 000000000..9adc2b03a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/BUG.yml @@ -0,0 +1,119 @@ +name: "Bug Report" +description: Report a bug with pyton.org website to help us improve +title: "Bug: " +labels: ["bug", "Triage Required"] + +body: + - type: markdown + attributes: + value: | + This is the repository and issue tracker for the https://www.pyton.org website. + + If you're looking to file an issue with CPython itself, please click here: [CPython Issues](https://github.com/python/cpython/issues/new/choose). + + Issues related to [Python's documentation](https://docs.python.org) can also be filed [here](https://github.com/python/cpython/issues/new?assignees=&labels=docs&template=documentation.md). + + - type: textarea + id: description + attributes: + label: "Describe the bug" + description: A clear and concise description of what the bug is. + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: "To Reproduce" + description: Steps to reproduce the behavior + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true + + - type: textarea + id: expected + attributes: + label: "Expected behavior" + description: A clear and concise description of what you expected to happen. + validations: + required: true + + - type: input + id: reprod-url + attributes: + label: "URL to the issue" + description: Please enter the URL to provide a reproduction of the issue, if applicable + placeholder: ex. https://python.org/my-issue/here + validations: + required: false + + - type: textarea + id: screenshot + attributes: + label: "Screenshots" + description: If applicable, add screenshots to help explain your problem. + value: | + "![SCREENSHOT_DESCRIPTION](SCREENSHOT_LINK.png)" + render: bash + validations: + required: false + + - type: dropdown + id: browsers + attributes: + label: "Browsers" + description: What browsers are you seeing the problem on? + multiple: true + options: + - Firefox + - Chrome + - Safari + - Microsoft Edge + - Other + validations: + required: true + + - type: dropdown + id: os + attributes: + label: "Operating System" + description: What operating system are you using? + options: + - Windows + - macOS + - Linux + - iOS + - Android + - Other + validations: + required: true + + - type: input + id: version + attributes: + label: "Browser Version" + description: What version of the browser are you using? + placeholder: "e.g. 22" + validations: + required: false + + - type: textarea + id: logs + attributes: + label: "Relevant log output" + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: shell + validations: + required: false + + - type: textarea + id: additional + attributes: + label: "Additional context" + description: Add any other context about the problem here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/DOCS.yml b/.github/ISSUE_TEMPLATE/DOCS.yml new file mode 100644 index 000000000..df7a2c231 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/DOCS.yml @@ -0,0 +1,23 @@ +name: "Documentation Update" +description: Create an issue for documentation changes +title: "Docs: <title>" +labels: ["documentation"] + +body: + - type: markdown + attributes: + value: | + This is the repository and issue tracker for the https://www.pyton.org website. + + If you're looking to file an issue with CPython itself, please click here: [CPython Issues](https://github.com/python/cpython/issues/new/choose). + + Issues related to [Python's documentation](https://docs.python.org) can also be filed [here](https://github.com/python/cpython/issues/new?assignees=&labels=docs&template=documentation.md). + + - type: textarea + id: summary + attributes: + label: "Summary" + description: Provide a brief summary of your request + placeholder: We need to update the documentation to include information about... + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/REQUEST.yml b/.github/ISSUE_TEMPLATE/REQUEST.yml new file mode 100644 index 000000000..144ad75c1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/REQUEST.yml @@ -0,0 +1,66 @@ +name: "Feature Request" +description: Suggest an idea for www.pyton.org +title: "Enhancement: <title>" +labels: ["enhancement"] + +body: + - type: markdown + attributes: + value: | + This is the repository and issue tracker for the https://www.pyton.org website. + + If you're looking to file an issue with CPython itself, please click here: [CPython Issues](https://github.com/python/cpython/issues/new/choose). + + Issues related to [Python's documentation](https://docs.python.org) can also be filed [here](https://github.com/python/cpython/issues/new?assignees=&labels=docs&template=documentation.md). + + - type: textarea + id: problem + attributes: + label: "Is your feature request related to a problem? Please describe." + description: A clear and concise description of what the problem is. + placeholder: Ex. I'm always frustrated when [...] + validations: + required: true + + - type: textarea + id: solution + attributes: + label: "Describe the solution you'd like" + description: A clear and concise description of what you want to happen. + placeholder: Ex. It would be great if [...] + validations: + required: true + + - type: textarea + id: basic_example + attributes: + label: "Basic Example" + description: Provide some basic examples of your feature request. + placeholder: Describe how your feature would work with a simple example. + validations: + required: false + + - type: textarea + id: alternatives + attributes: + label: "Describe alternatives you've considered" + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: false + + - type: textarea + id: drawbacks + attributes: + label: "Drawbacks and Impact" + description: What are the drawbacks or impacts of your feature request? + placeholder: Describe any potential drawbacks or impacts of implementing this feature. + validations: + required: false + + - type: textarea + id: additional_context + attributes: + label: "Additional context" + description: Add any other context or screenshots about the feature request here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index c958c11a4..000000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,45 +0,0 @@ ---- -name: Bug report -about: Report a bug with Python.org website to help us improve ---- - -<!-- -This is the repository and issue tracker for https://www.python.org -website. - -If you're looking to file an issue with CPython itself, please go to -https://github.com/python/cpython/issues/new/choose - -Issues related to Python's documentation (https://docs.python.org) can -also be filed at https://github.com/python/cpython/issues/new?assignees=&labels=docs&template=documentation.md. ---> - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..cd8c31d2a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: CPython Documentation + url: https://docs.python.org/ + about: Official CPython documentation - please check here before opening an issue. + - name: Python Website + url: https://python.org/ + about: For all things Python + - name: PyPI Issues / Support + url: https://github.com/pypi/support + about: For issues with PyPI itself, PyPI accounts, or with packages hosted on PyPI. + - name: CPython Issues + url: https://github.com/python/cpython/issues + about: For issues with the CPython interpreter itself. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index 514274e5f..000000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for www.python.org ---- - -<!-- -This is the repository and issue tracker for https://www.python.org -website. - -If you're looking to file an issue with CPython itself, please go to -https://github.com/python/cpython/issues/new/choose - -Issues related to Python's documentation (https://docs.python.org) can -also be filed at https://github.com/python/cpython/issues/new?assignees=&labels=docs&template=documentation.md. ---> - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. Ex. It would be great if [...] - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/base-requirements.txt b/base-requirements.txt index 2c74e825f..6171eebd2 100644 --- a/base-requirements.txt +++ b/base-requirements.txt @@ -1,8 +1,8 @@ dj-database-url==0.5.0 -django-pipeline==3.0.0 # 3.0.0 is first version that supports Django 4.2 +django-pipeline==3.1.0 # 3.0.0 is first version that supports Django 4.2 django-sitetree==1.18.0 # >=1.17.1 is (?) first version that supports Django 4.2 django-apptemplates==1.5 -django-admin-interface==0.24.2 +django-admin-interface==0.28.9 django-translation-aliases==0.1.0 Django==4.2.16 docutils==0.21.2 @@ -11,7 +11,7 @@ cmarkgfm==0.6.0 Pillow==10.4.0 psycopg2-binary==2.9.9 python3-openid==3.2.0 -python-decouple==3.4 +python-decouple==3.8 # lxml used by BeautifulSoup. lxml==5.2.2 cssselect==1.1.0 @@ -19,7 +19,7 @@ feedparser==6.0.11 beautifulsoup4==4.12.3 icalendar==4.0.7 chardet==4.0.0 -celery[redis]==5.3.6 +celery[redis]==5.4.0 django-celery-beat==2.5.0 # TODO: We may drop 'django-imagekit' completely. django-imagekit==5.0 # 5.0 is first version that supports Django 4.2 diff --git a/dev-requirements.txt b/dev-requirements.txt index 8d61d0f9d..1ee11a333 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -2,7 +2,7 @@ # Required for running tests -factory-boy==3.2.1 +factory-boy==3.3.1 Faker==0.8.1 tblib==1.7.0 responses==0.13.3 diff --git a/downloads/models.py b/downloads/models.py index 4a9c5781c..4576afb2f 100644 --- a/downloads/models.py +++ b/downloads/models.py @@ -272,6 +272,7 @@ def purge_fastly_download_pages(sender, instance, **kwargs): if instance.is_published: # Purge our common pages purge_url('/downloads/') + purge_url('/downloads/feed.rss') purge_url('/downloads/latest/python2/') purge_url('/downloads/latest/python3/') purge_url('/downloads/macos/') diff --git a/downloads/tests/test_views.py b/downloads/tests/test_views.py index c585fe05c..b559a2adc 100644 --- a/downloads/tests/test_views.py +++ b/downloads/tests/test_views.py @@ -554,3 +554,45 @@ def test_filter_release_file_delete_by_release(self): headers={"authorization": self.Authorization} ) self.assertEqual(response.status_code, 405) + +class ReleaseFeedTests(BaseDownloadTests): + """Tests for the downloads/feed.rss endpoint. + + Content is ensured via setUp in BaseDownloadTests. + """ + + url = reverse("downloads:feed") + + + def test_endpoint_reachable(self) -> None: + response = self.client.get(self.url) + self.assertEqual(response.status_code, 200) + + def test_feed_content(self) -> None: + """Ensure feed content is as expected. + + Some things we want to check: + - Feed title, description, pubdate + - Feed items (releases) are in the correct order + - We get the expected number of releases (10) + """ + response = self.client.get(self.url) + content = response.content.decode() + + self.assertIn("Python 2.7.5", content) + self.assertIn("Python 3.10", content) + # Published but hidden show up in the API and thus the feed + self.assertIn("Python 0.0.0", content) + + # No unpublished releases + self.assertNotIn("Python 9.7.2", content) + + # Pre-releases are shown + self.assertIn("Python 3.9.90", content) + + def test_feed_item_count(self) -> None: + response = self.client.get(self.url) + content = response.content.decode() + + # In BaseDownloadTests, we create 5 releases, 4 of which are published, 1 of those published are hidden.. + self.assertEqual(content.count("<item>"), 4) diff --git a/downloads/urls.py b/downloads/urls.py index d64f0a1ad..f553caeaa 100644 --- a/downloads/urls.py +++ b/downloads/urls.py @@ -9,4 +9,5 @@ path('release/<slug:release_slug>/', views.DownloadReleaseDetail.as_view(), name='download_release_detail'), path('<slug:slug>/', views.DownloadOSList.as_view(), name='download_os_list'), path('', views.DownloadHome.as_view(), name='download'), + path("feed.rss", views.ReleaseFeed(), name="feed"), ] diff --git a/downloads/views.py b/downloads/views.py index 746845402..6a7f9d95e 100644 --- a/downloads/views.py +++ b/downloads/views.py @@ -1,7 +1,14 @@ +from typing import Any + +from datetime import datetime + from django.db.models import Prefetch from django.urls import reverse +from django.utils import timezone from django.views.generic import DetailView, TemplateView, ListView, RedirectView from django.http import Http404 +from django.contrib.syndication.views import Feed +from django.utils.feedgenerator import Rss201rev2Feed from .models import OS, Release, ReleaseFile @@ -147,3 +154,41 @@ def get_context_data(self, **kwargs): ) return context + + +class ReleaseFeed(Feed): + """Generate an RSS feed of the latest Python releases. + + .. note:: It may seem like these are unused methods, but the superclass uses them + using Django's Syndication framework. + Docs: https://docs.djangoproject.com/en/4.2/ref/contrib/syndication/ + """ + + feed_type = Rss201rev2Feed + title = "Python Releases" + description = "Latest Python releases from Python.org" + + @staticmethod + def link() -> str: + """Return the URL to the main downloads page.""" + return reverse("downloads:download") + + def items(self) -> list[dict[str, Any]]: + """Return the latest Python releases.""" + return Release.objects.filter(is_published=True).order_by("-release_date")[:10] + + def item_title(self, item: Release) -> str: + """Return the release name as the item title.""" + return item.name + + def item_description(self, item: Release) -> str: + """Return the release version and release date as the item description.""" + return f"Version: {item.version}, Release Date: {item.release_date}" + + def item_pubdate(self, item: Release) -> datetime | None: + """Return the release date as the item publication date.""" + if item.release_date: + if timezone.is_naive(item.release_date): + return timezone.make_aware(item.release_date) + return item.release_date + return None diff --git a/events/models.py b/events/models.py index b41d92b22..1f2ab2cb0 100644 --- a/events/models.py +++ b/events/models.py @@ -211,8 +211,15 @@ def previous_time(self): return None @property - def next_or_previous_time(self): - return self.next_time or self.previous_time + def next_or_previous_time(self) -> models.Model: + """Return the next or previous time of the event OR the occurring rule.""" + if next_time := self.next_time: + return next_time + + if previous_time := self.previous_time: + return previous_time + + return self.occurring_rule if hasattr(self, "occurring_rule") else None @property def is_past(self): diff --git a/events/tests/test_views.py b/events/tests/test_views.py index 691817036..1291252a5 100644 --- a/events/tests/test_views.py +++ b/events/tests/test_views.py @@ -35,11 +35,50 @@ def setUpTestData(cls): finish=cls.now - datetime.timedelta(days=1), ) + # Future event + cls.future_event = Event.objects.create(title='Future Event', creator=cls.user, calendar=cls.calendar, featured=True) + RecurringRule.objects.create( + event=cls.future_event, + begin=cls.now + datetime.timedelta(days=1), + finish=cls.now + datetime.timedelta(days=2), + ) + + # Happening now event + cls.current_event = Event.objects.create(title='Current Event', creator=cls.user, calendar=cls.calendar) + RecurringRule.objects.create( + event=cls.current_event, + begin=cls.now - datetime.timedelta(hours=1), + finish=cls.now + datetime.timedelta(hours=1), + ) + + # Just missed event + cls.just_missed_event = Event.objects.create(title='Just Missed Event', creator=cls.user, calendar=cls.calendar) + RecurringRule.objects.create( + event=cls.just_missed_event, + begin=cls.now - datetime.timedelta(hours=3), + finish=cls.now - datetime.timedelta(hours=1), + ) + + # Past event + cls.past_event = Event.objects.create(title='Past Event', creator=cls.user, calendar=cls.calendar) + RecurringRule.objects.create( + event=cls.past_event, + begin=cls.now - datetime.timedelta(days=2), + finish=cls.now - datetime.timedelta(days=1), + ) + def test_events_homepage(self): url = reverse('events:events') response = self.client.get(url) + events = response.context['object_list'] + event_titles = [event.title for event in events] + self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context['object_list']), 1) + self.assertEqual(len(events), 6) + + self.assertIn('Future Event', event_titles) + self.assertIn('Current Event', event_titles) + self.assertIn('Past Event', event_titles) def test_calendar_list(self): calendars_count = Calendar.objects.count() @@ -54,7 +93,7 @@ def test_event_list(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context['object_list']), 1) + self.assertEqual(len(response.context['object_list']), 3) url = reverse('events:event_list_past', kwargs={"calendar_slug": 'unexisting'}) response = self.client.get(url) @@ -66,7 +105,7 @@ def test_event_list_past(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) - self.assertEqual(len(response.context['object_list']), 1) + self.assertEqual(len(response.context['object_list']), 4) def test_event_list_category(self): category = EventCategory.objects.create( @@ -114,7 +153,7 @@ def test_event_list_date(self): response = self.client.get(url) self.assertEqual(response.status_code, 200) self.assertEqual(response.context['object'], dt.date()) - self.assertEqual(len(response.context['object_list']), 2) + self.assertEqual(len(response.context['object_list']), 6) def test_eventlocation_list(self): venue = EventLocation.objects.create( @@ -150,12 +189,20 @@ def test_event_detail(self): self.assertEqual(self.event, response.context['object']) def test_upcoming_tag(self): - self.assertEqual(len(get_events_upcoming()), 1) - self.assertEqual(len(get_events_upcoming(only_featured=True)), 0) + self.assertEqual(len(get_events_upcoming()), 3) + self.assertEqual(len(get_events_upcoming(only_featured=True)), 1) self.rule.begin = self.now - datetime.timedelta(days=3) self.rule.finish = self.now - datetime.timedelta(days=2) self.rule.save() - self.assertEqual(len(get_events_upcoming()), 0) + self.assertEqual(len(get_events_upcoming()), 2) + + def test_context_data(self): + url = reverse("events:events") + response = self.client.get(url) + + self.assertIn("events_just_missed", response.context) + self.assertIn("upcoming_events", response.context) + self.assertIn("events_now", response.context) class EventSubmitTests(TestCase): diff --git a/events/views.py b/events/views.py index 2490626e3..56df88dcb 100644 --- a/events/views.py +++ b/events/views.py @@ -40,10 +40,21 @@ def get_context_data(self, **kwargs): class EventHomepage(ListView): """ Main Event Landing Page """ - template_name = 'events/event_list.html' + template_name = "events/event_list.html" - def get_queryset(self): - return Event.objects.for_datetime(timezone.now()).order_by('occurring_rule__dt_start') + def get_queryset(self) -> Event: + """Queryset to return all events, ordered by START date.""" + return Event.objects.all().order_by("-occurring_rule__dt_start") + + def get_context_data(self, **kwargs: dict) -> dict: + """Add more ctx, specifically events that are happening now, just missed, and upcoming.""" + context = super().get_context_data(**kwargs) + context["events_just_missed"] = Event.objects.until_datetime(timezone.now())[:2] + context["upcoming_events"] = Event.objects.for_datetime(timezone.now()) + context["events_now"] = Event.objects.filter( + occurring_rule__dt_start__lte=timezone.now(), + occurring_rule__dt_end__gte=timezone.now())[:2] + return context class EventDetail(DetailView): @@ -68,11 +79,13 @@ def get_context_data(self, **kwargs): class EventList(EventListBase): def get_queryset(self): - return Event.objects.for_datetime(timezone.now()).filter(calendar__slug=self.kwargs['calendar_slug']).order_by('occurring_rule__dt_start') + return Event.objects.for_datetime(timezone.now()).filter(calendar__slug=self.kwargs['calendar_slug']).order_by( + 'occurring_rule__dt_start') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context['events_today'] = Event.objects.until_datetime(timezone.now()).filter(calendar__slug=self.kwargs['calendar_slug'])[:2] + context['events_today'] = Event.objects.until_datetime(timezone.now()).filter( + calendar__slug=self.kwargs['calendar_slug'])[:2] context['calendar'] = get_object_or_404(Calendar, slug=self.kwargs['calendar_slug']) return context diff --git a/successstories/forms.py b/successstories/forms.py index f623001b0..45ec1dfd3 100644 --- a/successstories/forms.py +++ b/successstories/forms.py @@ -24,6 +24,10 @@ class Meta: labels = { 'name': 'Story name', } + help_texts = { + "content": "Note: Submissions in <a href='https://www.markdownguide.org/basic-syntax/'>Markdown</a> " + "are strongly preferred and can be processed faster.", + } def clean_name(self): name = self.cleaned_data.get('name') diff --git a/templates/base.html b/templates/base.html index 578dc1204..a1cfba788 100644 --- a/templates/base.html +++ b/templates/base.html @@ -96,6 +96,8 @@ href="https://feeds.feedburner.com/PythonSoftwareFoundationNews"> <link rel="alternate" type="application/rss+xml" title="Python Insider" href="https://feeds.feedburner.com/PythonInsider"> + <link rel="alternate" type="application/rss+xml" title="Python Releases" + href="https://www.python.org/downloads/feed.rss"> {% comment %} No support for these yet... diff --git a/templates/events/event_list.html b/templates/events/event_list.html index bbfb764d2..8d7b0d60f 100644 --- a/templates/events/event_list.html +++ b/templates/events/event_list.html @@ -8,73 +8,110 @@ {% block header_content %} {% if featured %} - <div class="featured-event"> + <div class="featured-event"> - <h2 class="welcome-message">Featured Python Event</h2> + <h2 class="welcome-message">Featured Python Event</h2> - <h1 class="call-to-action">{{ featured.title|striptags }}</h1> + <h1 class="call-to-action">{{ featured.title|striptags }}</h1> - <p class="event-date"><time datetime="{{ featured.next_datetime.dt_start|date:'c' }}"> - {{ featured.next_datetime.dt_start|date:"l, F d, Y" }} - </time></p> - <p class="excerpt">{{ featured.description.rendered|striptags|truncatewords:"60" }} <a class="readmore" href="{{ featured.get_absolute_url }}">Read more</a></p> - </div> + <p class="event-date"> + <time datetime="{{ featured.next_datetime.dt_start|date:'c' }}"> + {{ featured.next_datetime.dt_start|date:"l, F d, Y" }} + </time> + </p> + <p class="excerpt">{{ featured.description.rendered|striptags|truncatewords:"60" }} <a class="readmore" + href="{{ featured.get_absolute_url }}">Read + more</a></p> + </div> {% endif %} {% endblock header_content %} - -{# Based on python/events.html #} - {% block content %} - {% if calendar %} + {% if calendar %} <header class="article-header"> <h3>from the {{ calendar.name }}</h3> </header> - {% endif %} + {% endif %} - <div class="most-recent-events"> + <div class="most-recent-events"> + {% if events_now %} <div class="shrubbery"> - <h2 class="widget-title"><span aria-hidden="true" class="icon-calendar"></span>Upcoming Events</h2> - {% if page_obj.has_next %} - <p class="give-me-more"><a href="?page={{ page_obj.next_page_number }}" title="More Events">More</a></p> - {% endif %} + <h2 class="widget-title"><span aria-hidden="true" class="icon-alert"></span>Happening Now</h2> <ul class="list-recent-events menu"> - {% for object in object_list %} + {% for object in events_now %} + <li> + <h3 class="event-title"><a + href="{{ object.get_absolute_url }}">{{ object.title|striptags }}</a></h3> + <p> + {% with object.occurring_rule as next_time %} + {% include "events/includes/time_tag.html" %} + {% endwith %} + + {% if object.venue %} + <span class="event-location">{% if object.venue.url %} + <a href="{{ object.venue.url }}">{% endif %}{{ object.venue.name }} + {% if object.venue.url %}</a>{% endif %}{% if object.venue.address %}, + {{ object.venue.address }}{% endif %}</span> + {% endif %} + </p> + </li> + {% endfor %} + </ul> + </div> + {% endif %} + + <div class="shrubbery"> + <h2 class="widget-title"><span aria-hidden="true" class="icon-calendar"></span>Upcoming Events</h2> + {% if page_obj.has_next %} + <p class="give-me-more"><a href="?page={{ page_obj.next_page_number }}" title="More Events">More</a></p> + {% endif %} + <ul class="list-recent-events menu"> + {% for object in upcoming_events %} <li> - <h3 class="event-title"><a href="{{ object.get_absolute_url }}">{{ object.title|striptags }}</a></h3> + <h3 class="event-title"><a href="{{ object.get_absolute_url }}">{{ object.title|striptags }}</a> + </h3> <p> {% with object.next_time as next_time %} - {% include "events/includes/time_tag.html" %} + {% include "events/includes/time_tag.html" %} {% endwith %} {% if object.venue %} - <span class="event-location">{% if object.venue.url %}<a href="{{ object.venue.url }}">{% endif %}{{ object.venue.name }}{% if object.venue.url %}</a>{% endif %}{% if object.venue.address %}, {{ object.venue.address }}{% endif %}</span> + <span class="event-location">{% if object.venue.url %} + <a href="{{ object.venue.url }}">{% endif %}{{ object.venue.name }} + {% if object.venue.url %}</a>{% endif %}{% if object.venue.address %}, + {{ object.venue.address }}{% endif %}</span> {% endif %} </p> </li> {% endfor %} - </ul> - </div> - - {% if events_today %} - <h3 class="widget-title just-missed">You just missed...</h3> - <ul class="list-recent-events menu"> - {% for object in events_today %} - <li> - <h3 class="event-title"><a href="{{ object.get_absolute_url }}">{{ object.title|striptags }}</a></h3> - <p> - {% with object.previous_time as next_time %} - {% include "events/includes/time_tag.html" %} - {% endwith %} - - {% if object.venue %} - <span class="event-location">{% if object.venue.url %}<a href="{{ object.venue.url }}">{% endif %}{{ object.venue.name }}{% if object.venue.url %}</a>{% endif %}{% if object.venue.address %}, {{ object.venue.address }}{% endif %}</span> - {% endif %} - </p> - </li> - {% endfor %} </ul> - {% endif %} </div> + + {% if events_just_missed %} + <div class="shrubbery"> + <h3 class="widget-title just-missed">You just missed...</h3> + <ul class="list-recent-events menu"> + {% for object in events_just_missed %} + <li> + <h3 class="event-title"><a + href="{{ object.get_absolute_url }}">{{ object.title|striptags }}</a></h3> + <p> + {% with object.previous_time as next_time %} + {% include "events/includes/time_tag.html" %} + {% endwith %} + + {% if object.venue %} + <span class="event-location">{% if object.venue.url %} + <a href="{{ object.venue.url }}">{% endif %}{{ object.venue.name }} + {% if object.venue.url %}</a>{% endif %}{% if object.venue.address %}, + {{ object.venue.address }}{% endif %}</span> + {% endif %} + </p> + </li> + {% endfor %} + </ul> + </div> + {% endif %} + </div> {% endblock content %}