Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions data/events.csv

This file was deleted.

12 changes: 12 additions & 0 deletions main/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .io_resources import (
CompetencyDomainResource,
CompetencyResource,
EventResource,
LearningResourceResource,
ProviderResource,
SkillLevelResource,
Expand All @@ -19,6 +20,7 @@
from .models import (
Competency,
CompetencyDomain,
Event,
LearningResource,
Provider,
Skill,
Expand Down Expand Up @@ -162,6 +164,16 @@ class UserSkillInline(admin.TabularInline[UserSkill, User]):
extra = 1


@admin.register(Event)
class EventAdmin(ImportExportModelAdmin[Event]):
"""Admin class for the Event model."""

list_display = ("title", "start_date", "end_date", "location")
search_fields = ("title", "description", "contributors")
ordering = ("-start_date",)
resource_classes = (EventResource,)


class UserProxy(User):
"""A proxy model of the User model to create a User Skills admin view.

Expand Down
13 changes: 13 additions & 0 deletions main/io_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .models import (
Competency,
CompetencyDomain,
Event,
LearningResource,
Provider,
Skill,
Expand Down Expand Up @@ -259,6 +260,18 @@ class Meta:
exclude = ("id",)


class EventResource(resources.ModelResource):
"""A ModelResource to facilitate importing and exporting Events."""

class Meta:
"""Meta options for EventResource."""

model = Event
skip_unchanged = True
import_id_fields = ("title", "start_date")
exclude = ("id",)


def export_framework() -> dict[str, list[dict[str, str]]]:
"""Exports the core framework into one dictionary with each model as a key.

Expand Down
17 changes: 17 additions & 0 deletions main/migrations/0029_alter_userproxy_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 6.0.6 on 2026-07-07 10:02

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('main', '0028_alter_toollanguagemethodology_options_and_more'),
]

operations = [
migrations.AlterModelOptions(
name='userproxy',
options={'verbose_name': 'User skills profile'},
),
]
32 changes: 32 additions & 0 deletions main/migrations/0030_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Generated by Django 6.0.6 on 2026-07-07 10:02

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('main', '0029_alter_userproxy_options'),
]

operations = [
migrations.CreateModel(
name='Event',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('start_date', models.DateField()),
('end_date', models.DateField(blank=True, null=True)),
('location', models.CharField(blank=True, max_length=255)),
('description', models.TextField(blank=True)),
('event_link', models.URLField(blank=True, max_length=500, null=True)),
('blog', models.URLField(blank=True, max_length=500, null=True)),
('contributors', models.TextField(blank=True)),
('image', models.CharField(blank=True, max_length=500)),
],
options={
'ordering': ('-start_date', 'title'),
'unique_together': {('title', 'start_date')},
},
),
]
1 change: 1 addition & 0 deletions main/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Init in models."""

from .community_models import * # noqa: F403
from .framework_models import * # noqa: F403
from .user_models import * # noqa: F403
28 changes: 28 additions & 0 deletions main/models/community_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Models for community-facing content, kept separate from the core framework."""

from django.db import models


class Event(models.Model):
"""Model for community events the DIRECT project has contributed to."""

class Meta:
"""Meta options for Event model."""

ordering = ("-start_date", "title")
unique_together = ("title", "start_date")

title = models.CharField(max_length=255)
start_date = models.DateField()
end_date = models.DateField(blank=True, null=True)
location = models.CharField(max_length=255, blank=True)
description = models.TextField(blank=True)
event_link = models.URLField(max_length=500, blank=True, null=True)
blog = models.URLField(max_length=500, blank=True, null=True)
contributors = models.TextField(blank=True)
# Placeholder until image storage (static path vs. uploaded file) is decided.
image = models.CharField(max_length=500, blank=True)

def __str__(self) -> str:
"""Return the event title and start date."""
return f"{self.title} ({self.start_date})"
Binary file removed main/static/assets/img/avatar/13.jpg
Binary file not shown.
Binary file removed main/static/assets/img/avatar/14.jpg
Binary file not shown.
Binary file removed main/static/assets/img/avatar/15.jpg
Binary file not shown.
Binary file removed main/static/assets/img/avatar/16.jpg
Binary file not shown.
Binary file removed main/static/assets/img/avatar/17.jpg
Binary file not shown.
Binary file removed main/static/assets/img/avatar/18.jpg
Binary file not shown.
Binary file removed main/static/assets/img/blog/list/01.jpg
Binary file not shown.
Binary file removed main/static/assets/img/blog/list/02.jpg
Binary file not shown.
Binary file added main/static/assets/img/blog/placeholder.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file removed main/static/assets/img/blog/single/image.jpg
Binary file not shown.
81 changes: 56 additions & 25 deletions main/templates/main/pages/events.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,32 +24,63 @@ <h1 class="mb-lg-0">Events</h1>
</p>
</div>
</div>
<!-- Events list -->
<div class="row gy-4">
<!-- Event item -->
{% for event in events %}
<div class="col-12">
<div class="d-flex align-items-start">
<div class="flex-shrink-0 me-3">
<img src="{{ event.image }}"
alt="Event image"
class="rounded-2"
width="150">
</div>
<div>
<h4 class="mb-1">{{ event.title }}</h4>
<p class="mb-2">{{ event.description }}</p>
<p class="text-muted mb-1 small">
<strong>Date:</strong> {{ event.start_date }}
{% if event.end_date and event.end_date != event.start_date %}– {{ event.end_date }}{% endif %}
</p>
<p class="text-muted mb-2 small">
<strong>Contributors:</strong> {{ event.contributors }}
</p>
{% regroup events by start_date.year as year_groups %}
{% if year_groups %}
<div class="d-flex flex-column gap-5">
{% for year_group in year_groups %}
<div>
<h2 class="h4 text-uppercase text-body-secondary pb-2 border-bottom mb-4">{{ year_group.grouper }}</h2>
<div class="d-flex flex-column gap-4">
{% for event in year_group.list %}
<div class="card card-hover card-lifted shadow-sm h-100">
<div class="row g-0">
<div class="col-md-3">
<img src="{% static event.image|default:'assets/img/blog/placeholder.png' %}"
alt="Event image"
class="rounded-start w-100 h-100"
style="object-fit: cover">
</div>
<div class="col-md-9">
<div class="card-body">
<h4 class="card-title mb-2">{{ event.title }}</h4>
<p class="card-text text-body-secondary small mb-2">
<i class="fa-solid fa-calendar-days me-2"></i>{{ event.start_date }}
{% if event.end_date and event.end_date != event.start_date %}– {{ event.end_date }}{% endif %}
{% if event.location %}
<span class="ms-3"><i class="fa-solid fa-location-dot me-2"></i>{{ event.location }}</span>
{% endif %}
</p>
<p class="card-text mb-2">{{ event.description|truncatewords:35 }}</p>
{% if event.contributors %}
<p class="card-text text-body-secondary small mb-2">
<i class="fa-solid fa-users me-2"></i>{{ event.contributors }}
</p>
{% endif %}
{% if event.event_link or event.blog %}
<div class="d-flex gap-3 mt-2">
{% if event.event_link %}
<a href="{{ event.event_link }}" target="_blank" class="small fw-medium">
<i class="fa-solid fa-arrow-up-right-from-square me-1"></i>Event page
</a>
{% endif %}
{% if event.blog %}
<a href="{{ event.blog }}" target="_blank" class="small fw-medium">
<i class="fa-solid fa-newspaper me-1"></i>Blog post
</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
{% else %}
<p class="text-body-secondary">No events yet.</p>
{% endif %}
</section>
{% endblock content %}
31 changes: 4 additions & 27 deletions main/views/page_views.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Views for the page-related pages of the main app."""

import csv
import json
import logging
from collections.abc import Mapping
Expand All @@ -12,7 +11,6 @@
import nh3
import requests
from django.shortcuts import get_object_or_404
from django.templatetags.static import static
from django.utils.decorators import method_decorator
from django.utils.safestring import mark_safe
from django.views.decorators.cache import cache_page
Expand All @@ -21,6 +19,7 @@

from ..models import (
CompetencyDomain,
Event,
LearningResource,
Skill,
SkillLevel,
Expand Down Expand Up @@ -118,32 +117,10 @@ class EventsPageView(TemplateView):

template_name = "main/pages/events.html"

def get_context_data(self, **kwargs: Mapping[str, Any]) -> dict[str, Any]:
"""Add events from CSV to the template context."""
def get_context_data(self, **kwargs: Mapping[str, object]) -> dict[str, object]:
"""Add events from the database to the template context."""
context = super().get_context_data(**kwargs)
csv_path = Path("data/events.csv")
events = []

if csv_path.exists():
with open(csv_path, newline="", encoding="utf-8") as csvfile:
reader = csv.DictReader(csvfile)
for row in reader:
events.append(
{
"title": row.get("Title", ""),
"start_date": row.get("Start Date", ""),
"end_date": row.get("End Date", ""),
"description": row.get("Description", ""),
"contributors": row.get("Contributors", ""),
"image": static(
row.get("Image", "assets/img/blog/single/image.jpg")
),
}
)

events.sort(key=lambda e: e["start_date"], reverse=True)

context["events"] = events
context["events"] = Event.objects.all()
return context
Comment on lines 120 to 124

Copilot AI Apr 17, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current EventsPageView tests only assert that an events list exists, but they don’t validate the new fields (location, event_link, blog) or the missing-image behavior introduced here. Adding assertions for at least one row (including that empty image values don’t crash rendering) would help prevent regressions when the CSV schema evolves again.

Copilot uses AI. Check for mistakes.


Expand Down
16 changes: 16 additions & 0 deletions tests/main/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from main.models import (
Competency,
CompetencyDomain,
Event,
LearningResource,
Provider,
Skill,
Expand Down Expand Up @@ -103,3 +104,18 @@ def skill_level() -> SkillLevel:
def user_skill(user, skill: Skill, skill_level: SkillLevel) -> UserSkill:
"""Fixture for creating a UserSkill instance."""
return UserSkill.objects.create(user=user, skill=skill, skill_level=skill_level)


@pytest.fixture
def event() -> Event:
"""Fixture for creating an Event instance."""
return Event.objects.create(
title="Collaborations Workshop",
start_date="2026-04-01",
end_date="2026-04-02",
location="Manchester",
description="A workshop about the framework.",
event_link="https://example.com/event",
blog="https://example.com/blog",
contributors="Test Contributor",
)
25 changes: 25 additions & 0 deletions tests/main/test_community_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Test suite for the community related models."""

import pytest

from main.models import Event


@pytest.mark.django_db
def test_event_model(event: Event) -> None:
"""Test the Event model."""
assert event.title == "Collaborations Workshop"
assert str(event) == "Collaborations Workshop (2026-04-01)"
assert event.location == "Manchester"
assert event.description == "A workshop about the framework."
assert event.event_link == "https://example.com/event"
assert event.blog == "https://example.com/blog"
assert event.contributors == "Test Contributor"
assert event.image == ""


@pytest.mark.django_db
def test_event_ordering(event: Event) -> None:
"""Test that events are ordered by most recent start date first."""
earlier_event = Event.objects.create(title="Earlier Event", start_date="2023-01-01")
assert list(Event.objects.all()) == [event, earlier_event]
3 changes: 2 additions & 1 deletion tests/main/test_main_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,12 +443,13 @@ class TestEventsPageView(TemplateOkMixin):
def _get_url(self):
return reverse("events")

@pytest.mark.django_db
def test_provides_required_context(self, client):
"""Test that the events view provides the events context."""
response = client.get(self._get_url())
assert response.status_code == HTTPStatus.OK
assert "events" in response.context
assert isinstance(response.context["events"], list)
assert isinstance(response.context["events"], QuerySet)


class TestAccountOverviewView(LoginRequiredMixin):
Expand Down