-
- {% include 'events/event_applications.html' %}
+{% for info in applicant_info %}
+
+
+
+
+ {% include 'events/event_applications.html' %}
- {% endfor %}
+
+{% endfor %}
{% endblock %}
+
+{% block local_js_bottom %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/physionet-django/console/views.py b/physionet-django/console/views.py
index b5c470e092..46f4790e49 100644
--- a/physionet-django/console/views.py
+++ b/physionet-django/console/views.py
@@ -27,7 +27,7 @@
from django.utils import timezone
from django.core.exceptions import PermissionDenied
from events.forms import EventAgreementForm, EventDatasetForm
-from events.models import Event, EventAgreement, EventDataset, EventApplication
+from events.models import Event, EventAgreement, EventDataset, EventApplication, CohostStatus
from notification.models import News
from physionet.forms import set_saved_fields_cookie
from physionet.middleware.maintenance import ServiceUnavailable
@@ -3109,6 +3109,7 @@ def event_management(request, event_slug):
else:
event_dataset_form = EventDatasetForm()
+ cohost_status = {member: value.value for member, value in CohostStatus.__members__.items()}
participants = selected_event.participants.all()
pending_applications = selected_event.applications.filter(
status=EventApplication.EventApplicationStatus.WAITLISTED
@@ -3157,6 +3158,7 @@ def event_management(request, event_slug):
"event_datasets": event_datasets,
"applicant_info": applicant_info,
"participants": participants,
+ "cohostStatus": cohost_status,
},
)
diff --git a/physionet-django/events/models.py b/physionet-django/events/models.py
index 0c253c58b6..13709ecd92 100644
--- a/physionet-django/events/models.py
+++ b/physionet-django/events/models.py
@@ -1,3 +1,4 @@
+from enum import IntEnum
from django.db import models, transaction
from django.utils.crypto import get_random_string
from django.utils import timezone
@@ -10,6 +11,11 @@
from project.validators import validate_version, validate_slug
+class CohostStatus(IntEnum):
+ MAKE_COHOST = 0
+ REMOVE_COHOST = 1
+
+
class Event(models.Model):
"""
Captures information on events such as datathons, workshops and classes.
@@ -59,6 +65,12 @@ def get_cohosts(self):
"""
return self.participants.filter(is_cohost=True)
+ def has_ended(self):
+ """
+ Returns true if the event has ended.
+ """
+ return self.end_date < timezone.now().date()
+
class EventParticipant(models.Model):
"""
diff --git a/physionet-django/events/templates/events/email/event_cohost_cohost_status_change.html b/physionet-django/events/templates/events/email/event_cohost_cohost_status_change.html
new file mode 100644
index 0000000000..6e3a633534
--- /dev/null
+++ b/physionet-django/events/templates/events/email/event_cohost_cohost_status_change.html
@@ -0,0 +1,15 @@
+{% load i18n %}{% autoescape off %}{% filter wordwrap:70 %}
+Dear {{ name }},
+{% if status == True %}
+You have been added as Cohost to the Event : {{ event_title }}.
+
+You can now manage the event participants from your events dashboard.
+{% elif status == False %}
+Your Cohost access has been removed from the Event : {{ event_title }}.
+{% endif %}
+You can view further information about the event using the following link:
+{{ event_url }}
+
+Regards
+The {{ SITE_NAME }} Team
+{% endfilter %}{% endautoescape %}
\ No newline at end of file
diff --git a/physionet-django/events/templates/events/email/event_host_cohost_status_change.html b/physionet-django/events/templates/events/email/event_host_cohost_status_change.html
new file mode 100644
index 0000000000..fcecfef1e4
--- /dev/null
+++ b/physionet-django/events/templates/events/email/event_host_cohost_status_change.html
@@ -0,0 +1,14 @@
+{% load i18n %}{% autoescape off %}{% filter wordwrap:70 %}
+Dear {{ host_name }},
+
+{% if status == True %}
+You have provided {{ cohost_name }} Cohost access to the Event : {{ event_title }}.
+{% elif status == False %}
+You have removed {{ cohost_name }}'s Cohost access from the Event : {{ event_title }}.
+{% endif %}
+If this was done by mistake, you can change the cohost status by visiting the events dashboard.
+{{ event_url }}
+
+Regards
+The {{ SITE_NAME }} Team
+{% endfilter %}{% endautoescape %}
\ No newline at end of file
diff --git a/physionet-django/events/templates/events/event_applications.html b/physionet-django/events/templates/events/event_applications.html
index e501081eb2..62b7f51013 100644
--- a/physionet-django/events/templates/events/event_applications.html
+++ b/physionet-django/events/templates/events/event_applications.html
@@ -1,27 +1,140 @@
{% load participation_status %}
-
-
-
-
-
-
- Username |
- Full name |
- Email |
- Credentialed |
-
-
-
- {% for application in info.objects %}
-
- {{ application.user.username }} |
- {{ application.user.get_full_name }} |
- {{ application.user.email }} |
- {{ application.user.is_credentialed }} |
-
- {% endfor %}
-
-
-
-
+
+
+
+
+ Username |
+ Full name |
+ Email |
+ Credentialed |
+ Comments |
+ Decision date |
+ Cohost |
+
+
+
+ {% for object in info.objects %}
+
+ {{ object.user.username }} |
+ {{ object.user.get_full_name }} |
+ {{ object.user.email }} |
+ {{ object.user.is_credentialed }} |
+ {{ object.comment_to_applicant }} |
+ {{ object.decision_datetime | date }} |
+
+ {% if not event.has_ended %}
+ {% if object.is_cohost %}
+
+ {% endif %}
+ |
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/physionet-django/events/templates/events/event_entries.html b/physionet-django/events/templates/events/event_entries.html
deleted file mode 100644
index 3fc91e2ce1..0000000000
--- a/physionet-django/events/templates/events/event_entries.html
+++ /dev/null
@@ -1,35 +0,0 @@
-{% load participation_status %}
-
diff --git a/physionet-django/events/templates/events/event_home.html b/physionet-django/events/templates/events/event_home.html
index ec33a693d2..fc5746ed30 100644
--- a/physionet-django/events/templates/events/event_home.html
+++ b/physionet-django/events/templates/events/event_home.html
@@ -11,191 +11,200 @@
{% endblock %}
{% block content %}
-
{% include "message_snippet.html" %}
Events Home
- {% if can_change_event %}
-
Create new events and access event details.
-
-
- Create New Event
-
- {% else %}
-
View events that you have registered to attend.
- {% endif %}
-
+ {% if can_change_event %}
+
Create new events and access event details.
+
+
+ Create New Event
+
+ {% else %}
+
View events that you have registered to attend.
+ {% endif %}
+
-
-
-
+
-{% include 'events/event_notifications.html' %}
+ {% include 'events/event_notifications.html' %}
-
+
-
+
-
+
{% for event in events_active %}
- -
-
{{ event.title }}
-
- Host: {{ event.host.get_full_name }}
- {% with event_cohosts=event.get_cohosts %}
- {% if event_cohosts %}
- Cohost:
- {% for cohost in event_cohosts %}
- {{ cohost.user.get_full_name }}{% if not forloop.last %}, {% endif %}
- {% endfor %}
-
- {% endif %}
- {% endwith %}
- Created: {{ event.added_datetime|date }}
- {% if event.host == user %} Number of participants: {{ event.participants.all|length }}
{% endif %}
- Start Date: {{ event.start_date }}
- End Date: {{ event.end_date }}
-
- {% if user|is_participant:event %}
- Registration status: Registered
- {% elif user|is_on_waiting_list:event %}
- Registration status: On waiting list
- {% endif %}
-
- {% if event.host == user %}
- Share the class code: {{ url_prefix }}{% url 'event_detail' event.slug %}
-
-
-
-
-
- Edit Event
- {% endif %}
-
- View Event
-
+ -
+
{{ event.title }}
+
+ Host: {{ event.host.get_full_name }}
+ {% with event_cohosts=event.get_cohosts %}
+ {% if event_cohosts %}
+ Cohost:
+ {% for cohost in event_cohosts %}
+ {{ cohost.user.get_full_name }}{% if not forloop.last %}, {% endif %}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+ Created: {{ event.added_datetime|date }}
+ {% if event.host == user %}
+ Number of participants: {{ event.participants.all|length }}
+
{% endif %}
+ Start Date: {{ event.start_date }}
+ End Date: {{ event.end_date }}
+
+ {% if user|is_participant:event %}
+ Registration status: Registered
+ {% elif user|is_on_waiting_list:event %}
+ Registration status: On waiting list
+ {% endif %}
+
+ {% if event.host == user %}
+ Share the class code: {{ url_prefix }}{% url 'event_detail' event.slug %}
+
+
+
+
+
+ Edit Event
+ {% endif %}
+
+ View Event
+
{% empty %}
-
+
{% endfor %}
{% for event in events_active %}
- {% for info in event_details|get_applicant_info:event.id %}
-
-
-
-
- {% include 'events/event_applications.html' %}
-
-
+ {% for info in event_details|get_applicant_info:event.id %}
+
+
+
+
- {% endfor %}
+ {% include 'events/event_applications.html' %}
+
+
+
+ {% endfor %}
{% endfor %}
-
-
+
-
+
-
+
{% for event in events_past %}
- -
-
{{ event.title }}
-
- Host: {{ event.host.get_full_name }}
- {% with event_cohosts=event.get_cohosts %}
- {% if event_cohosts %}
- Cohost:
- {% for cohost in event_cohosts %}
- {{ cohost.user.get_full_name }}{% if not forloop.last %}, {% endif %}
- {% endfor %}
-
- {% endif %}
- {% endwith %}
- Created: {{ event.added_datetime|date }}
- {% if event.host == user %} Number of participants: {{ event.participants.all|length }}
{% endif %}
- Start Date: {{ event.start_date }}
- End Date: {{ event.end_date }}
-
- {% if user|is_participant:event %}
- Registration status: Approved
- {% elif user|is_on_waiting_list:event %}
- Registration status: Waiting for Approval
- {% endif %}
-
- {% if event.host == user %}
- Share the class code: {{ url_prefix }}{% url 'event_detail' event.slug %}
-
-
- Edit Event
- {% endif %}
-
- View Event
-
+ -
+
{{ event.title }}
+
+ Host: {{ event.host.get_full_name }}
+ {% with event_cohosts=event.get_cohosts %}
+ {% if event_cohosts %}
+ Cohost:
+ {% for cohost in event_cohosts %}
+ {{ cohost.user.get_full_name }}{% if not forloop.last %}, {% endif %}
+ {% endfor %}
+
+ {% endif %}
+ {% endwith %}
+ Created: {{ event.added_datetime|date }}
+ {% if event.host == user %}
+ Number of participants: {{ event.participants.all|length }}
+ {% endif %}
+ Start Date: {{ event.start_date }}
+ End Date: {{ event.end_date }}
+
+ {% if user|is_participant:event %}
+ Registration status: Approved
+ {% elif user|is_on_waiting_list:event %}
+ Registration status: Waiting for Approval
+ {% endif %}
+
+ {% if event.host == user %}
+ Share the class code: {{ url_prefix }}{% url 'event_detail' event.slug %}
+
+
+ Edit Event
+ {% endif %}
+
+ View Event
+
{% empty %}
-
+
{% endfor %}
{% if events_past %}
- {% for event in events_past %}
- {% for info in event_details|get_applicant_info:event.id %}
-
-
-
-
- {% include 'events/event_applications.html' %}
-
-
+ {% for event in events_past %}
+ {% for info in event_details|get_applicant_info:event.id %}
+
+
+
+
- {% endfor %}
- {% endfor %}
- {% endif %}
+ {% include 'events/event_applications.html' %}
+
+
+ {% endfor %}
+ {% endfor %}
+ {% endif %}
+
{% endblock %}
-
{% block local_js_bottom %}
-
-
-{% endblock %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/physionet-django/events/urls.py b/physionet-django/events/urls.py
index 6feaeac384..43c6a5cc7d 100644
--- a/physionet-django/events/urls.py
+++ b/physionet-django/events/urls.py
@@ -6,6 +6,8 @@
urlpatterns = [
path('', views.event_home, name='event_home'),
path('create/', views.create_event, name='create_event'),
+ path('
/add_co_host/', views.add_co_host, name='add_co_host'),
+ path('/remove_co_host/', views.remove_co_host, name='remove_co_host'),
path('/', views.event_detail, name='event_detail'),
path('/edit_event/', views.update_event, name='update_event'),
path('/details/', views.get_event_details, name='get_event_details'),
@@ -14,6 +16,6 @@
# Parameters for testing URLs (see physionet/test_urls.py)
TEST_DEFAULTS = {
'event_slug': 'iLII4L9jSDFh',
- 'participant_id': 1,
+ 'participant_id': '1',
'_user_': 'rgmark',
}
diff --git a/physionet-django/events/views.py b/physionet-django/events/views.py
index 3c2f256560..55ba74b349 100644
--- a/physionet-django/events/views.py
+++ b/physionet-django/events/views.py
@@ -1,20 +1,33 @@
+import json
from datetime import datetime
-from django.http import JsonResponse
+from django.http import JsonResponse, QueryDict
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.db.models import Q
from django.contrib.auth.decorators import login_required
from django.forms import modelformset_factory
from django.urls import reverse
+from django.views.decorators.http import require_http_methods
import notification.utility as notification
from events.forms import AddEventForm, EventApplicationResponseForm
-from events.models import Event, EventApplication, EventParticipant
+from events.models import Event, EventApplication, EventParticipant, CohostStatus
from events.utility import notify_host_cohosts_new_registration
+# Defining utility function that run checks for a participant to be made event co-host
+def check_participant_cohost_eligibility(event, user, participant):
+ if not event.host == user:
+ return False, 'You are not the host of this event'
+ if event.end_date < datetime.now().date():
+ return False, 'You cannot manage co-hosts of an event that has ended'
+ if not event.participants.filter(id=participant.id).exists():
+ return False, 'User is not a participant of this event'
+ return True, None
+
+
@login_required
def update_event(request, event_slug, **kwargs):
user = request.user
@@ -218,6 +231,7 @@ def event_home(request):
participation_response_formset = EventApplicationResponseFormSet(
queryset=participation_requests
)
+ cohost_status = {member: value.value for member, value in CohostStatus.__members__.items()}
return render(
request,
"events/event_home.html",
@@ -230,6 +244,7 @@ def event_home(request):
"form_error": form_error,
"participation_response_formset": participation_response_formset,
"event_details": event_details,
+ "cohostStatus": cohost_status,
},
)
@@ -310,3 +325,75 @@ def event_detail(request, event_slug):
'is_waitlisted': is_waitlisted,
'event_datasets': event_datasets,
})
+
+
+@login_required
+def add_co_host(request, event_slug):
+ """
+ Add a co-host to an event
+ """
+ user = request.user
+ error_message = None
+
+ if request.method == 'GET':
+ return redirect('event_home')
+
+ body = json.loads(request.body)
+ participant_id = body.get('participant_id')
+
+ event = get_object_or_404(Event, slug=event_slug)
+
+ participant = event.participants.get(id=participant_id)
+
+ eligibility_check = check_participant_cohost_eligibility(event, user, participant)
+
+ if eligibility_check[0] is False:
+ error_message = eligibility_check[1]
+ return JsonResponse({'error': error_message}, status=403)
+
+ if participant.is_cohost:
+ return JsonResponse({'error': 'User is already a cohost of this event'}, status=403)
+ participant.is_cohost = True
+
+ participant.save()
+ notification.notify_event_cohost_cohost_status_change(request=request, cohost=participant.user,
+ event=event, status=participant.is_cohost)
+ notification.notify_event_host_cohost_status_change(request=request, cohost=participant.user, event=event,
+ status=participant.is_cohost)
+
+ return JsonResponse({'success': 'Cohost status updated successfully'})
+
+
+@login_required
+def remove_co_host(request, event_slug):
+ """
+ Remove a co-host from an event
+ """
+ user = request.user
+ error_message = None
+
+ if request.method == 'GET':
+ return redirect('event_home')
+
+ body = json.loads(request.body)
+ participant_id = body.get('participant_id')
+
+ event = get_object_or_404(Event, slug=event_slug)
+ participant = event.participants.get(id=participant_id)
+
+ eligibility_check = check_participant_cohost_eligibility(event, user, participant)
+ if eligibility_check[0] is False:
+ error_message = eligibility_check[1]
+ return JsonResponse({'error': error_message}, status=403)
+
+ if not participant.is_cohost:
+ return JsonResponse({'error': 'User is not a cohost of this event'}, status=403)
+ participant.is_cohost = False
+
+ participant.save()
+ notification.notify_event_cohost_cohost_status_change(request=request, cohost=participant.user,
+ event=event, status=participant.is_cohost)
+ notification.notify_event_host_cohost_status_change(request=request, cohost=participant.user, event=event,
+ status=participant.is_cohost)
+
+ return JsonResponse({'success': 'Cohost status updated successfully'})
diff --git a/physionet-django/notification/utility.py b/physionet-django/notification/utility.py
index 83697e2603..5673a6322d 100644
--- a/physionet-django/notification/utility.py
+++ b/physionet-django/notification/utility.py
@@ -1006,6 +1006,43 @@ def notify_participant_event_decision(request, user, event, decision, comment_to
send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [user.email], fail_silently=False)
+def notify_event_cohost_cohost_status_change(request, cohost, event, status):
+ """
+ Send email to co-host about their cohost status change for the event.
+ """
+ subject = f"{settings.SITE_NAME} Event Cohost Status Change"
+ context = {
+ 'name': cohost.get_full_name(),
+ 'domain': get_current_site(request),
+ 'event_title': event.title,
+ 'event_url': request.build_absolute_uri(reverse('event_detail', args=[event.slug])),
+ 'status': status,
+ 'SITE_NAME': settings.SITE_NAME,
+ }
+ body = loader.render_to_string('events/email/event_cohost_cohost_status_change.html', context)
+ # Not resend the email if there was an integrity error
+ send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [cohost.email], fail_silently=False)
+
+
+def notify_event_host_cohost_status_change(request, cohost, event, status='Make cohost'):
+ """
+ Send email to host about cohost status change for the event.
+ """
+ subject = f"{settings.SITE_NAME} Event Cohost Status Change"
+ context = {
+ 'host_name': event.host.get_full_name(),
+ 'cohost_name': cohost.get_full_name(),
+ 'domain': get_current_site(request),
+ 'event_title': event.title,
+ 'event_url': request.build_absolute_uri(reverse('event_detail', args=[event.slug])),
+ 'status': status,
+ 'SITE_NAME': settings.SITE_NAME,
+ }
+ body = loader.render_to_string('events/email/event_host_cohost_status_change.html', context)
+ # Not resend the email if there was an integrity error
+ send_mail(subject, body, settings.DEFAULT_FROM_EMAIL, [cohost.email], fail_silently=False)
+
+
def notify_event_participant_application(request, user, registered_user, event):
"""
Send email to host and co-host about new registration request on event.