diff --git a/data/events.csv b/data/events.csv deleted file mode 100644 index c05f8003..00000000 --- a/data/events.csv +++ /dev/null @@ -1,24 +0,0 @@ -Title,Start Date,End Date,Description,Links,Contributors,Image -SSI CW23 Hack Day 2023,2023-05-04,2023-05-04,"Slides; Notes; Teams: HackMD, Miro, Github","Slides Notes Teams HackMD Miro GitHub","Aleksandra Nenadic, Aman Goel, Dave Horsfall, Diego Alonso Álvarez, Eli Chadwick, Iain Barrass, Hannah Williams, Lieke de Boer, Paul K Korir, Martin O’Reilly, Matthew Bluteau, Nadine Spychala, Sean Marshallsay",assets/img/blog/single/image.jpg -UKHSA internal presentation,2023-05-12,2023-05-12,,Slides,Hannah Williams,assets/img/blog/single/image.jpg -RSE South workshop,2023-05-16,2023-05-16,,Slides,Hannah Williams,assets/img/blog/single/image.jpg -UK Carpentries Community Call,2023-06-26,2023-06-26,,Slides,"Aleksandra Nenadic, Matthew Bluteau, Dave Horsfall",assets/img/blog/single/image.jpg -RS London South-East,2023-07-17,2023-07-17,,Abstract,"Hannah Williams, Diego Alonso Álvarez",assets/img/blog/single/image.jpg -RSE Conference 2023,2023-09-05,2023-09-08,,Meeting notes,"Dave Horsfall, Hannah Williams, Matthew Bluteau",assets/img/blog/single/image.jpg -Professionalising data science roles workshop,2023-10-02,2023-10-02,,,Dave Horsfall,assets/img/blog/single/image.jpg -DRI retreat,2024-03-18,2024-03-22,Emma & Sam attending; Emma involved in panels; Promote project via panels and poster,,,assets/img/blog/single/image.jpg -Dagstuhl Seminar 24161 Research Software Engineering: Bridging Knowledge Gaps,2024-04-14,2024-04-19,Toby attending and will try to promote project,,,assets/img/blog/single/image.jpg -Collaborations Workshop 2024,2024-04-30,2024-05-02,"Lightning talk/workshop; Target discussions, invite people to short interviews",,,assets/img/blog/single/image.jpg -N8CIR In person RSE Meetup Summer 2024,2024-07-18,2024-07-18,,Event Page,,assets/img/blog/single/image.jpg -RSECon24,2024-09-03,2024-09-05,Submit panel in collaboration with Met Office via Emma; Submit workshop focused on defining & categorising skills,"Programme Recording","Becky Osselton, Emma Hogan, Aleksandra Nenadic, Matt Craddock, Gabriel Hanganu, Eli Chadwick, Dave Horsfall",assets/img/blog/single/image.jpg -CarpentryConnect Heidelberg,2024-11-12,2024-11-14,"Aleks, Toby, Matt B working on submissions",,,assets/img/blog/single/image.jpg -SSI Fellows Community Call February 2025,2025-02-24,2025-02-24,10 minute talk given as a part of the regular SSI Fellows Community calls,"Recording Slides",Aman Goel,assets/img/blog/single/image.jpg -CW25,2025-05-13,2025-05-15,"Proposal; Workshop notes & blogpost & Hack Day notes; Workshop slides, Hack Day slides","Workshop notes Blog","Workshop: Aleksandra Nenadic, David Horsfall, Eli Chadwick, , Aman Goel, Phil Reed, Adrian D’Alessandro, Jonathan Cooper Hack Day: Phil Reed, Aleksandra Nenadic, Patricia Loto, Adrian D’Alessandro, Bryn Ubald, Andrew Gait, Tamora James, Connor Aird, Ryan Smith",assets/img/blog/single/image.jpg -"Open Research Conference 2025, Manchester",2025-06-09,2025-06-10,Aleks and Phil lightning talk at The University of Manchester,"Event Page Slides","Aleksandra Nenadic, Phil Reed",assets/img/blog/single/image.jpg -UK-Ireland Digital Humanities Association Event 2025,2025-06-17,2025-06-18,"Workshop for capturing a digital humanities voice in the framework, part of SSI Fellowship for Phil Reed",https://digitalhumanities-uk-ie.org/2025-annual-event/,"Phil Reed, Dave Horsfall, Aleksandra Nenadic, Lucia Michielin, Anna-Maria Sichani",assets/img/blog/single/image.jpg -N8 CIR RSE Annual Meeting 2025,2025-07-10,2025-07-10,Lightning talk,"Event Page Slides",Aman Goel,assets/img/blog/single/image.jpg -SSI internal ideas meeting,2025-09-02,2025-09-02,Presentation + activity to explore the framework,Slides,Aleksandra Nenadic,assets/img/blog/single/image.jpg -RSECon25,2025-09-09,2025-09-11,Workshop for user experience testing the web app visualisations and profiles,Programme,"Dave Horsfall, Adrian D’Alessandro, Aman Goel, Aleksandra Nenadic, Phil Reed, Emma Hogan, Muhammad Salihu, Gabriel Hanganu",assets/img/blog/single/image.jpg -"Satellite Training Community Day, RSECon25",2025-09-12,2025-09-12,Workshop for capturing and mapping training and learning opportunities,Event Page,"Dave Horsfall, Toby Hodges, Weronika Filinger, Phil Reed, Samantha Ahern, Jonathan Cooper, Aman Goel",assets/img/blog/single/image.jpg -CaSDaR launch,2025-09-18,2025-09-18,Lightning talk at the launch of Careers and Skills for Data-driven Research (CaSDaR),https://www.eventbrite.com/e/careers-and-skills-for-data-driven-research-casdar-hybrid-event-launch-tickets-1496323607189,Phil Reed,assets/img/blog/single/image.jpg -CW26,2026-04-28,2026-04-30,"Workshop: Strengthening the Research Software Community",https://www.software.ac.uk/workshop/collaborations-workshop-2026-cw26,"David Horsfall, Phil Reed, Sara Villa, Aleksandra Nenadic, Jonathan Cooper, Samantha Ahern, Adrian D'Alessandro, Emma Hogan, Mike Simpson, Kirsty Pringle",assets/img/blog/single/image.jpg diff --git a/main/admin.py b/main/admin.py index fc76b9e4..226cf6bf 100644 --- a/main/admin.py +++ b/main/admin.py @@ -10,6 +10,7 @@ from .io_resources import ( CompetencyDomainResource, CompetencyResource, + EventResource, LearningResourceResource, ProviderResource, SkillLevelResource, @@ -19,6 +20,7 @@ from .models import ( Competency, CompetencyDomain, + Event, LearningResource, Provider, Skill, @@ -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. diff --git a/main/io_resources.py b/main/io_resources.py index 672bd85d..57cc3582 100644 --- a/main/io_resources.py +++ b/main/io_resources.py @@ -15,6 +15,7 @@ from .models import ( Competency, CompetencyDomain, + Event, LearningResource, Provider, Skill, @@ -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. diff --git a/main/migrations/0029_alter_userproxy_options.py b/main/migrations/0029_alter_userproxy_options.py new file mode 100644 index 00000000..e946acbb --- /dev/null +++ b/main/migrations/0029_alter_userproxy_options.py @@ -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'}, + ), + ] diff --git a/main/migrations/0030_event.py b/main/migrations/0030_event.py new file mode 100644 index 00000000..e36a5a1e --- /dev/null +++ b/main/migrations/0030_event.py @@ -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')}, + }, + ), + ] diff --git a/main/models/__init__.py b/main/models/__init__.py index 2d6dc0a2..bc3dd610 100644 --- a/main/models/__init__.py +++ b/main/models/__init__.py @@ -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 diff --git a/main/models/community_models.py b/main/models/community_models.py new file mode 100644 index 00000000..214f5a85 --- /dev/null +++ b/main/models/community_models.py @@ -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})" diff --git a/main/static/assets/img/avatar/13.jpg b/main/static/assets/img/avatar/13.jpg deleted file mode 100755 index 75bd11e6..00000000 Binary files a/main/static/assets/img/avatar/13.jpg and /dev/null differ diff --git a/main/static/assets/img/avatar/14.jpg b/main/static/assets/img/avatar/14.jpg deleted file mode 100755 index c1d58119..00000000 Binary files a/main/static/assets/img/avatar/14.jpg and /dev/null differ diff --git a/main/static/assets/img/avatar/15.jpg b/main/static/assets/img/avatar/15.jpg deleted file mode 100755 index 6dd334dd..00000000 Binary files a/main/static/assets/img/avatar/15.jpg and /dev/null differ diff --git a/main/static/assets/img/avatar/16.jpg b/main/static/assets/img/avatar/16.jpg deleted file mode 100755 index 5ac64b81..00000000 Binary files a/main/static/assets/img/avatar/16.jpg and /dev/null differ diff --git a/main/static/assets/img/avatar/17.jpg b/main/static/assets/img/avatar/17.jpg deleted file mode 100755 index 74764256..00000000 Binary files a/main/static/assets/img/avatar/17.jpg and /dev/null differ diff --git a/main/static/assets/img/avatar/18.jpg b/main/static/assets/img/avatar/18.jpg deleted file mode 100755 index f511f2cc..00000000 Binary files a/main/static/assets/img/avatar/18.jpg and /dev/null differ diff --git a/main/static/assets/img/blog/list/01.jpg b/main/static/assets/img/blog/list/01.jpg deleted file mode 100644 index 07e8e398..00000000 Binary files a/main/static/assets/img/blog/list/01.jpg and /dev/null differ diff --git a/main/static/assets/img/blog/list/02.jpg b/main/static/assets/img/blog/list/02.jpg deleted file mode 100644 index 4701e36a..00000000 Binary files a/main/static/assets/img/blog/list/02.jpg and /dev/null differ diff --git a/main/static/assets/img/blog/placeholder.png b/main/static/assets/img/blog/placeholder.png new file mode 100644 index 00000000..3e9a196d Binary files /dev/null and b/main/static/assets/img/blog/placeholder.png differ diff --git a/main/static/assets/img/blog/single/image.jpg b/main/static/assets/img/blog/single/image.jpg deleted file mode 100644 index f7c366e0..00000000 Binary files a/main/static/assets/img/blog/single/image.jpg and /dev/null differ diff --git a/main/templates/main/pages/events.html b/main/templates/main/pages/events.html index b62488b9..0bd30160 100644 --- a/main/templates/main/pages/events.html +++ b/main/templates/main/pages/events.html @@ -24,32 +24,63 @@
{{ event.description }}
-- Date: {{ event.start_date }} - {% if event.end_date and event.end_date != event.start_date %}– {{ event.end_date }}{% endif %} -
-- Contributors: {{ event.contributors }} -
+ {% regroup events by start_date.year as year_groups %} + {% if year_groups %} +
+ + {{ event.start_date }} + {% if event.end_date and event.end_date != event.start_date %}– {{ event.end_date }}{% endif %} + {% if event.location %} + {{ event.location }} + {% endif %} +
+{{ event.description|truncatewords:35 }}
+ {% if event.contributors %} ++ {{ event.contributors }} +
+ {% endif %} + {% if event.event_link or event.blog %} +No events yet.
+ {% endif %} {% endblock content %} diff --git a/main/views/page_views.py b/main/views/page_views.py index e3273a25..37931acc 100644 --- a/main/views/page_views.py +++ b/main/views/page_views.py @@ -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 @@ -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 @@ -21,6 +19,7 @@ from ..models import ( CompetencyDomain, + Event, LearningResource, Skill, SkillLevel, @@ -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 diff --git a/tests/main/conftest.py b/tests/main/conftest.py index 393eeac6..dfca97b4 100644 --- a/tests/main/conftest.py +++ b/tests/main/conftest.py @@ -5,6 +5,7 @@ from main.models import ( Competency, CompetencyDomain, + Event, LearningResource, Provider, Skill, @@ -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", + ) diff --git a/tests/main/test_community_models.py b/tests/main/test_community_models.py new file mode 100644 index 00000000..782942b2 --- /dev/null +++ b/tests/main/test_community_models.py @@ -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] diff --git a/tests/main/test_main_views.py b/tests/main/test_main_views.py index 12815bad..29db8776 100644 --- a/tests/main/test_main_views.py +++ b/tests/main/test_main_views.py @@ -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):