+ {% if conversation_form %}
+
+
Contact User
+
+ {% crispy conversation_form %}
+
+ {% endif %}
+
{% if object.status != object.SUCCESS and incomplete_jobs %}
Prerequisite Jobs
diff --git a/app/grandchallenge/evaluation/views/__init__.py b/app/grandchallenge/evaluation/views/__init__.py
index 9348d9af06..0b8ef99029 100644
--- a/app/grandchallenge/evaluation/views/__init__.py
+++ b/app/grandchallenge/evaluation/views/__init__.py
@@ -3,6 +3,7 @@
from dateutil.relativedelta import relativedelta
from django.conf import settings
from django.contrib import messages
+from django.contrib.auth import get_user_model
from django.contrib.messages.views import SuccessMessageMixin
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
@@ -31,6 +32,7 @@
filter_by_permission,
)
from grandchallenge.datatables.views import Column, PaginatedTableListView
+from grandchallenge.direct_messages.forms import ConversationForm
from grandchallenge.evaluation.forms import (
CombinedLeaderboardForm,
EvaluationForm,
@@ -541,6 +543,25 @@ class EvaluationDetail(ObjectPermissionRequiredMixin, DetailView):
permission_required = "view_evaluation"
raise_exception = True
+ def get_conversation_form(self):
+ if self.object.submission.creator:
+ conversation_form = ConversationForm(
+ participants=get_user_model().objects.filter(
+ pk__in={
+ self.request.user.pk,
+ self.object.submission.creator.pk,
+ }
+ )
+ )
+ conversation_form.helper.form_action = reverse(
+ "direct_messages:conversation-create",
+ kwargs={"username": self.object.submission.creator.username},
+ )
+ else:
+ conversation_form = None
+
+ return conversation_form
+
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -613,6 +634,7 @@ def get_context_data(self, **kwargs):
"json": json,
"predictions": predictions,
"incomplete_jobs": incomplete_jobs,
+ "conversation_form": self.get_conversation_form(),
}
)
diff --git a/app/grandchallenge/profiles/templatetags/profiles.py b/app/grandchallenge/profiles/templatetags/profiles.py
index 5d47610ede..f5af18e131 100644
--- a/app/grandchallenge/profiles/templatetags/profiles.py
+++ b/app/grandchallenge/profiles/templatetags/profiles.py
@@ -31,17 +31,7 @@ def user_profile_link(user: AbstractUser | None) -> str:
)
try:
- if user.verification.is_verified:
- email = (
- user.verification.email
- if user.verification.email
- else user.email
- )
- verified = format_html(
- '',
- email.split("@")[1],
- )
+ verified = user.verification.verification_badge
except ObjectDoesNotExist:
# No verification request
pass
diff --git a/app/grandchallenge/verifications/models.py b/app/grandchallenge/verifications/models.py
index 581060d11b..97dc39f866 100644
--- a/app/grandchallenge/verifications/models.py
+++ b/app/grandchallenge/verifications/models.py
@@ -78,6 +78,17 @@ def user_info(self):
self.user.user_profile.website,
)
+ @property
+ def verification_badge(self):
+ if self.is_verified:
+ return format_html(
+ '',
+ self.email.split("@")[1],
+ )
+ else:
+ return ""
+
def send_verification_email(self):
if self.email_is_verified:
# Nothing to do
diff --git a/app/tests/direct_messages_tests/__init__.py b/app/tests/direct_messages_tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/app/tests/direct_messages_tests/factories.py b/app/tests/direct_messages_tests/factories.py
new file mode 100644
index 0000000000..738f1b36d8
--- /dev/null
+++ b/app/tests/direct_messages_tests/factories.py
@@ -0,0 +1,29 @@
+import factory
+
+from grandchallenge.direct_messages.models import (
+ Conversation,
+ DirectMessage,
+ Mute,
+)
+from tests.factories import UserFactory
+
+
+class ConversationFactory(factory.django.DjangoModelFactory):
+ class Meta:
+ model = Conversation
+
+
+class DirectMessageFactory(factory.django.DjangoModelFactory):
+ conversation = factory.SubFactory(ConversationFactory)
+ sender = factory.SubFactory(UserFactory)
+
+ class Meta:
+ model = DirectMessage
+
+
+class MuteFactory(factory.django.DjangoModelFactory):
+ source = factory.SubFactory(UserFactory)
+ target = factory.SubFactory(UserFactory)
+
+ class Meta:
+ model = Mute
diff --git a/app/tests/direct_messages_tests/test_models.py b/app/tests/direct_messages_tests/test_models.py
new file mode 100644
index 0000000000..90858b9dc7
--- /dev/null
+++ b/app/tests/direct_messages_tests/test_models.py
@@ -0,0 +1,176 @@
+import pytest
+
+from grandchallenge.direct_messages.models import Conversation
+from tests.direct_messages_tests.factories import (
+ ConversationFactory,
+ DirectMessageFactory,
+ MuteFactory,
+)
+from tests.evaluation_tests.test_permissions import get_users_with_set_perms
+from tests.factories import UserFactory
+
+
+@pytest.mark.django_db
+def test_unread_by_cleared_when_deleted():
+ messages = DirectMessageFactory.create_batch(2)
+ user = UserFactory()
+
+ for message in messages:
+ message.unread_by.add(user)
+
+ messages[0].delete()
+
+ for message in messages:
+ message.refresh_from_db()
+
+ assert messages[0].is_deleted is True
+ assert {*messages[0].unread_by.all()} == set()
+ assert messages[1].is_deleted is False
+ assert {*messages[1].unread_by.all()} == {user}
+
+
+@pytest.mark.django_db
+def test_unread_by_cleared_when_marked_junk():
+ messages = DirectMessageFactory.create_batch(2)
+ user = UserFactory()
+
+ for message in messages:
+ message.unread_by.add(user)
+
+ messages[0].is_reported_as_spam = True
+ messages[0].save()
+
+ for message in messages:
+ message.refresh_from_db()
+
+ assert messages[0].is_reported_as_spam is True
+ assert {*messages[0].unread_by.all()} == set()
+ assert messages[1].is_reported_as_spam is False
+ assert {*messages[1].unread_by.all()} == {user}
+
+
+@pytest.mark.django_db
+def test_direct_message_permissions():
+ user = UserFactory()
+ message = DirectMessageFactory(sender=user)
+
+ assert get_users_with_set_perms(message) == {
+ user: {"delete_directmessage"}
+ }
+
+
+@pytest.mark.django_db
+def test_mute_permissions():
+ user = UserFactory()
+ mute = MuteFactory(source=user)
+
+ assert get_users_with_set_perms(mute) == {user: {"delete_mute"}}
+
+
+@pytest.mark.django_db
+def test_unread_by_updated_for_mute():
+ sender, muter, other = UserFactory.create_batch(3)
+
+ message = DirectMessageFactory(sender=sender)
+ message.unread_by.add(muter, other)
+
+ other_message = DirectMessageFactory()
+ other_message.unread_by.add(muter, other)
+
+ MuteFactory(source=muter, target=sender)
+
+ assert {*message.unread_by.all()} == {other}
+ assert {*other_message.unread_by.all()} == {muter, other}
+
+
+@pytest.mark.django_db
+def test_conversation_with_most_recent_message():
+ conversation = ConversationFactory()
+ user = UserFactory()
+
+ ConversationFactory() # No messages
+ DirectMessageFactory(conversation=conversation)
+ most_recent_message = DirectMessageFactory(conversation=conversation)
+
+ queryset = Conversation.objects.order_by(
+ "created"
+ ).with_most_recent_message(user=user)
+
+ assert (
+ queryset[0].most_recent_message_created == most_recent_message.created
+ )
+ assert queryset[0].unread_message_count == 0
+ assert queryset[0].unread_by_user is False
+ assert queryset[1].most_recent_message_created is None
+ assert queryset[1].unread_message_count == 0
+ assert queryset[1].unread_by_user is False
+
+
+@pytest.mark.django_db
+def test_conversation_with_most_recent_message_unread():
+ conversation = ConversationFactory()
+ user = UserFactory()
+
+ ConversationFactory() # No messages
+ DirectMessageFactory(conversation=conversation)
+ message_1 = DirectMessageFactory(conversation=conversation)
+ message_2 = DirectMessageFactory(conversation=conversation)
+ message_3 = DirectMessageFactory()
+
+ for message in {message_1, message_2, message_3}:
+ message.unread_by.add(user)
+
+ message_3.unread_by.add(UserFactory())
+
+ queryset = Conversation.objects.order_by(
+ "created"
+ ).with_most_recent_message(user=user)
+
+ assert queryset[0].most_recent_message_created == message_2.created
+ assert queryset[0].unread_message_count == 2
+ assert queryset[0].unread_by_user is True
+ assert queryset[1].most_recent_message_created is None
+ assert queryset[1].unread_message_count == 0
+ assert queryset[1].unread_by_user is False
+ assert queryset[2].most_recent_message_created == message_3.created
+ assert queryset[2].unread_message_count == 1
+ assert queryset[2].unread_by_user is True
+
+
+@pytest.mark.django_db
+def test_conversation_unread_by_user():
+ conversation = ConversationFactory()
+ user = UserFactory()
+
+ messages = DirectMessageFactory.create_batch(3, conversation=conversation)
+
+ messages[1].unread_by.add(user)
+ messages[2].unread_by.add(UserFactory())
+
+ other_message = DirectMessageFactory()
+ other_message.unread_by.add(user)
+
+ assert [
+ message.unread_by_user
+ for message in Conversation.objects.order_by("created")
+ .with_unread_by_user(user=user)
+ .first()
+ .direct_messages.all()
+ ] == [False, True, False]
+
+
+@pytest.mark.django_db
+def test_conversation_for_participants():
+ u1, u2, u3, u4 = UserFactory.create_batch(4)
+
+ c1, c2, c3, c4, c5, _ = ConversationFactory.create_batch(6)
+ c1.participants.set([u1, u2])
+ c2.participants.set([u1, u3])
+ c3.participants.set([u2, u3])
+ c4.participants.set([u1, u2, u3])
+ c5.participants.set([u3, u4])
+
+ assert (
+ Conversation.objects.for_participants(participants=[u1, u2]).get()
+ == c1
+ )
diff --git a/app/tests/direct_messages_tests/test_signals.py b/app/tests/direct_messages_tests/test_signals.py
new file mode 100644
index 0000000000..b6d59694da
--- /dev/null
+++ b/app/tests/direct_messages_tests/test_signals.py
@@ -0,0 +1,93 @@
+import pytest
+from guardian.utils import get_anonymous_user
+
+from grandchallenge.direct_messages.models import Conversation
+from tests.direct_messages_tests.factories import (
+ ConversationFactory,
+ DirectMessageFactory,
+)
+from tests.evaluation_tests.test_permissions import get_users_with_set_perms
+from tests.factories import UserFactory
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize("reverse", [True, False])
+def test_conversation_participants_permissions_signal(reverse):
+ c1, c2 = ConversationFactory.create_batch(2)
+ u1, u2, u3, u4 = UserFactory.create_batch(4)
+
+ if reverse:
+ for user in [u1, u2, u3, u4]:
+ user.conversations.add(c1, c2)
+ for user in [u3, u4]:
+ user.conversations.remove(c1, c2)
+ for user in [u1, u2]:
+ user.conversations.remove(c2)
+ else:
+ c1.participants.add(u1, u2, u3, u4)
+ c1.participants.remove(u3, u4)
+
+ assert get_users_with_set_perms(c1) == {
+ u1: {
+ "view_conversation",
+ "create_conversation_direct_message",
+ "mark_conversation_read",
+ "mark_conversation_message_as_spam",
+ },
+ u2: {
+ "view_conversation",
+ "create_conversation_direct_message",
+ "mark_conversation_read",
+ "mark_conversation_message_as_spam",
+ },
+ }
+ assert get_users_with_set_perms(c2) == {}
+
+ # Test clearing
+ if reverse:
+ u1.conversations.clear()
+ u2.conversations.clear()
+ else:
+ c1.participants.clear()
+
+ assert get_users_with_set_perms(c1) == {}
+ assert get_users_with_set_perms(c2) == {}
+
+
+@pytest.mark.django_db
+def test_anon_not_participant():
+ conversation = ConversationFactory()
+
+ with pytest.raises(RuntimeError) as e:
+ conversation.participants.add(get_anonymous_user())
+ assert str(e) == "The Anonymous User cannot be added to this group"
+
+
+@pytest.mark.django_db
+def test_unread_removal():
+ users = UserFactory.create_batch(2)
+
+ for _ in range(2):
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ message = DirectMessageFactory(
+ conversation=conversation, sender=users[0]
+ )
+ message.unread_by.set(users)
+
+ conversation.participants.remove(users[1])
+
+ conversations = Conversation.objects.order_by(
+ "created"
+ ).with_most_recent_message(user=users[1])
+
+ assert conversations[0].unread_by_user is True
+ assert conversations[1].unread_by_user is False
+
+ conversations = Conversation.objects.order_by(
+ "created"
+ ).with_most_recent_message(user=users[0])
+
+ assert conversations[0].unread_by_user is True
+ assert conversations[1].unread_by_user is True
diff --git a/app/tests/direct_messages_tests/test_views.py b/app/tests/direct_messages_tests/test_views.py
new file mode 100644
index 0000000000..03acffb54d
--- /dev/null
+++ b/app/tests/direct_messages_tests/test_views.py
@@ -0,0 +1,490 @@
+import pytest
+from guardian.shortcuts import remove_perm
+from guardian.utils import get_anonymous_user
+
+from grandchallenge.direct_messages.models import (
+ Conversation,
+ DirectMessage,
+ Mute,
+)
+from tests.direct_messages_tests.factories import (
+ ConversationFactory,
+ DirectMessageFactory,
+ MuteFactory,
+)
+from tests.factories import ChallengeFactory, UserFactory
+from tests.utils import get_view_for_user
+
+
+@pytest.mark.django_db
+def test_conversation_create_permissions(client, settings):
+ def try_create_conversation(creator, target):
+ return get_view_for_user(
+ client=client,
+ viewname="direct_messages:conversation-create",
+ reverse_kwargs={"username": target.username},
+ user=creator,
+ )
+
+ admin, participant = UserFactory.create_batch(2)
+ challenge = ChallengeFactory()
+
+ # Normal users cannot create conversations with each other
+ response = try_create_conversation(creator=admin, target=participant)
+ assert response.status_code == 403
+
+ # Challenge admins cannot message anyone
+ challenge.add_admin(user=admin)
+ response = try_create_conversation(creator=admin, target=participant)
+ assert response.status_code == 403
+
+ # Only admins should be able to contact participants
+ challenge.add_participant(user=participant)
+ response = try_create_conversation(creator=admin, target=participant)
+ assert response.status_code == 200
+
+ # One way, participants should not be able to initiate conversations
+ response = try_create_conversation(creator=participant, target=admin)
+ assert response.status_code == 403
+
+ # Anonymous user shouldn't be messaged
+ anon = get_anonymous_user()
+ challenge.participants_group.user_set.add(anon)
+ response = try_create_conversation(creator=admin, target=anon)
+ assert response.status_code == 403
+
+ # Anonymous user shouldn't be able to create messages
+ challenge.admins_group.user_set.add(anon)
+ response = try_create_conversation(creator=anon, target=participant)
+ assert response.status_code == 302
+ assert response.url.startswith(settings.LOGIN_URL)
+
+
+@pytest.mark.django_db
+def test_existing_converstion_redirect(client):
+ admin, participant = UserFactory.create_batch(2)
+ challenge = ChallengeFactory()
+ challenge.add_admin(user=admin)
+ challenge.add_participant(user=participant)
+
+ conversation = ConversationFactory()
+ conversation.participants.set([admin, participant])
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:conversation-create",
+ reverse_kwargs={"username": participant.username},
+ user=admin,
+ method=client.post,
+ )
+ assert response.status_code == 302
+ assert (
+ response.url
+ == f"https://testserver/messages/?conversation={conversation.pk}"
+ )
+
+
+@pytest.mark.django_db
+def test_new_conversation_redirect(client):
+ admin, participant = UserFactory.create_batch(2)
+ challenge = ChallengeFactory()
+ challenge.add_admin(user=admin)
+ challenge.add_participant(user=participant)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:conversation-create",
+ reverse_kwargs={"username": participant.username},
+ user=admin,
+ method=client.post,
+ )
+ assert response.status_code == 302
+
+ conversation = Conversation.objects.get()
+
+ assert (
+ response.url
+ == f"https://testserver/messages/?conversation={conversation.pk}"
+ )
+
+
+@pytest.mark.django_db
+def test_direct_message_unread(client):
+ users = UserFactory.create_batch(2)
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:direct-message-create",
+ reverse_kwargs={"pk": conversation.pk},
+ user=users[0],
+ method=client.post,
+ data={"message": "🙈"},
+ )
+
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ conversation.refresh_from_db()
+
+ assert {*conversation.direct_messages.last().unread_by.all()} == {users[1]}
+
+
+@pytest.mark.django_db
+def test_direct_message_unread_with_mute(client):
+ target, source, other = UserFactory.create_batch(3)
+ conversation = ConversationFactory()
+ conversation.participants.set([target, source, other])
+
+ MuteFactory(target=target, source=source)
+ MuteFactory(target=other, source=target)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:direct-message-create",
+ reverse_kwargs={"pk": conversation.pk},
+ user=target,
+ method=client.post,
+ data={"message": "🙈"},
+ )
+
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ conversation.refresh_from_db()
+
+ assert {*conversation.direct_messages.last().unread_by.all()} == {other}
+
+
+@pytest.mark.django_db
+def test_mark_spam(client):
+ users = UserFactory.create_batch(2)
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+ message = DirectMessageFactory(conversation=conversation, sender=users[0])
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:direct-message-report-spam",
+ reverse_kwargs={"conversation_pk": conversation.pk, "pk": message.pk},
+ user=users[1],
+ method=client.post,
+ data={"is_reported_as_spam": True},
+ )
+
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ message.refresh_from_db()
+
+ assert message.is_reported_as_spam
+
+
+@pytest.mark.django_db
+def test_mute_form(client):
+ users = UserFactory.create_batch(2)
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:mute-create",
+ reverse_kwargs={"username": users[0].username},
+ user=users[0],
+ method=client.post,
+ data={"conversation": conversation.pk},
+ )
+ assert response.status_code == 200
+ assert response.context["form"].errors == {
+ "__all__": ["You cannot mute yourself"]
+ }
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:mute-create",
+ reverse_kwargs={"username": users[1].username},
+ user=users[0],
+ method=client.post,
+ data={"conversation": conversation.pk},
+ )
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:mute-create",
+ reverse_kwargs={"username": users[1].username},
+ user=users[0],
+ method=client.post,
+ data={"conversation": conversation.pk},
+ )
+ assert response.status_code == 200
+ assert response.context["form"].errors == {
+ "__all__": ["Mute with this Target and Source already exists."]
+ }
+
+
+@pytest.mark.django_db
+def test_mute_delete_redirect(client):
+ users = UserFactory.create_batch(2)
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ mute = MuteFactory(source=users[0], target=users[1])
+ MuteFactory(source=users[0])
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:mute-delete",
+ reverse_kwargs={"username": users[1].username},
+ user=users[0],
+ method=client.post,
+ data={"conversation": conversation.pk},
+ )
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ with pytest.raises(Mute.DoesNotExist):
+ mute.refresh_from_db()
+
+
+@pytest.mark.django_db
+def test_mute_delete_permission(client):
+ users = UserFactory.create_batch(2)
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ mute = MuteFactory(source=users[0], target=users[1])
+
+ remove_perm("delete_mute", users[0], mute)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:mute-delete",
+ reverse_kwargs={"username": users[1].username},
+ user=users[0],
+ method=client.post,
+ data={"conversation": conversation.pk},
+ )
+ assert response.status_code == 403
+
+
+@pytest.mark.django_db
+def test_conversation_list_filtered(client):
+ users = UserFactory.create_batch(2)
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ other_conversation = ConversationFactory()
+ other_conversation.participants.set([users[1]])
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:conversation-list",
+ user=users[0],
+ )
+ assert response.status_code == 200
+ assert {*response.context["object_list"]} == {conversation}
+
+
+@pytest.mark.django_db
+@pytest.mark.parametrize(
+ "viewname",
+ [
+ "conversation-detail",
+ "conversation-select-detail",
+ "conversation-mark-read",
+ "direct-message-create",
+ ],
+)
+def test_conversation_detail_permissions(client, viewname):
+ users = UserFactory.create_batch(2)
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ response = get_view_for_user(
+ client=client,
+ viewname=f"direct_messages:{viewname}",
+ reverse_kwargs={"pk": conversation.pk},
+ user=users[0],
+ )
+ assert response.status_code == 200
+
+ conversation.participants.remove(users[0])
+
+ response = get_view_for_user(
+ client=client,
+ viewname=f"direct_messages:{viewname}",
+ reverse_kwargs={"pk": conversation.pk},
+ user=users[0],
+ )
+ assert response.status_code == 403
+
+
+@pytest.mark.django_db
+def test_conversation_mark_read(client):
+ users = UserFactory.create_batch(2)
+
+ for _ in range(2):
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ message = DirectMessageFactory(
+ conversation=conversation, sender=users[0]
+ )
+ message.unread_by.set(users)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:conversation-mark-read",
+ reverse_kwargs={"pk": conversation.pk},
+ user=users[1],
+ method=client.post,
+ )
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ conversation.participants.remove(users[1])
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:conversation-mark-read",
+ reverse_kwargs={"pk": conversation.pk},
+ user=users[1],
+ method=client.post,
+ )
+ assert response.status_code == 403
+
+ conversations = Conversation.objects.order_by(
+ "created"
+ ).with_most_recent_message(user=users[1])
+
+ assert conversations[0].unread_by_user is True
+ assert conversations[1].unread_by_user is False
+
+ conversations = Conversation.objects.order_by(
+ "created"
+ ).with_most_recent_message(user=users[0])
+
+ assert conversations[0].unread_by_user is True
+ assert conversations[1].unread_by_user is True
+
+
+@pytest.mark.django_db
+def test_direct_message_report_spam(client):
+ users = UserFactory.create_batch(2)
+
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ for _ in range(2):
+ message = DirectMessageFactory(
+ conversation=conversation, sender=users[0]
+ )
+ message.unread_by.set(users)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:direct-message-report-spam",
+ reverse_kwargs={"conversation_pk": conversation.pk, "pk": message.pk},
+ user=users[1],
+ method=client.post,
+ data={"is_reported_as_spam": True},
+ )
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ conversation.participants.remove(users[1])
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:direct-message-report-spam",
+ reverse_kwargs={"conversation_pk": conversation.pk, "pk": message.pk},
+ user=users[1],
+ method=client.post,
+ data={"is_reported_as_spam": True},
+ )
+ assert response.status_code == 403
+
+ messages = DirectMessage.objects.order_by("created")
+ assert messages[0].is_reported_as_spam is False
+ assert messages[1].is_reported_as_spam is True
+
+
+@pytest.mark.django_db
+def test_direct_message_delete(client):
+ users = UserFactory.create_batch(2)
+
+ conversation = ConversationFactory()
+ conversation.participants.set(users)
+
+ for _ in range(2):
+ message = DirectMessageFactory(
+ conversation=conversation, sender=users[0]
+ )
+ message.unread_by.set(users)
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:direct-message-delete",
+ reverse_kwargs={"conversation_pk": conversation.pk, "pk": message.pk},
+ user=users[0],
+ method=client.post,
+ data={"conversation": conversation.pk},
+ )
+ assert response.status_code == 302
+ assert response.url == conversation.get_absolute_url()
+
+ response = get_view_for_user(
+ client=client,
+ viewname="direct_messages:direct-message-delete",
+ reverse_kwargs={"conversation_pk": conversation.pk, "pk": message.pk},
+ user=users[1],
+ method=client.post,
+ data={"conversation": conversation.pk},
+ )
+ assert response.status_code == 403
+
+ messages = DirectMessage.objects.order_by("created")
+ assert messages[0].is_deleted is False
+ assert messages[1].is_deleted is True
+
+
+@pytest.mark.django_db
+def test_all_views_require_login(client, settings):
+ user = UserFactory()
+ anon = get_anonymous_user()
+
+ conversation = ConversationFactory()
+ conversation.participants.set([user])
+
+ message = DirectMessageFactory(conversation=conversation)
+
+ tests = (
+ ("conversation-list", {}),
+ ("conversation-create", {"username": user.username}),
+ ("mute-create", {"username": user.username}),
+ ("mute-delete", {"username": user.username}),
+ ("conversation-detail", {"pk": conversation.pk}),
+ ("conversation-mark-read", {"pk": conversation.pk}),
+ ("conversation-select-detail", {"pk": conversation.pk}),
+ ("direct-message-create", {"pk": conversation.pk}),
+ (
+ "direct-message-delete",
+ {"pk": message.pk, "conversation_pk": conversation.pk},
+ ),
+ (
+ "direct-message-report-spam",
+ {"pk": message.pk, "conversation_pk": conversation.pk},
+ ),
+ )
+ for viewname, reverse_kwargs in tests:
+ response = get_view_for_user(
+ client=client,
+ viewname=f"direct_messages:{viewname}",
+ reverse_kwargs=reverse_kwargs,
+ user=anon,
+ )
+ assert response.status_code == 302
+ assert response.url.startswith(settings.LOGIN_URL)
diff --git a/scripts/development_fixtures.py b/scripts/development_fixtures.py
index b5bd16a18d..8d31e9b7db 100644
--- a/scripts/development_fixtures.py
+++ b/scripts/development_fixtures.py
@@ -1,7 +1,9 @@
import base64
+import itertools
import json
import logging
import os
+import random
from datetime import timedelta
from allauth.account.models import EmailAddress
@@ -15,6 +17,7 @@
from django.core.files.base import ContentFile
from django.db import IntegrityError
from django.utils import timezone
+from faker import Faker
from guardian.shortcuts import assign_perm
from knox import crypto
from knox.models import AuthToken
@@ -31,6 +34,7 @@
ComponentInterfaceValue,
)
from grandchallenge.core.fixtures import create_uploaded_image
+from grandchallenge.direct_messages.models import Conversation, DirectMessage
from grandchallenge.evaluation.models import (
CombinedLeaderboard,
Evaluation,
@@ -83,6 +87,7 @@ def run():
except IntegrityError as e:
raise RuntimeError("Fixtures already initialized") from e
+ _create_direct_messages(users)
_set_user_permissions(users)
_create_task_types_regions_modalities(users)
_create_workstation(users)
@@ -111,12 +116,15 @@ def _create_flatpages():
def _create_users(usernames):
users = {}
+ fake = Faker()
for username in usernames:
user = get_user_model().objects.create(
username=username,
email=f"{username}@example.com",
is_active=True,
+ first_name=fake.first_name(),
+ last_name=fake.last_name(),
)
user.set_password(username)
user.save()
@@ -133,6 +141,10 @@ def _create_users(usernames):
email=user.email,
is_verified=True,
)
+
+ user.user_profile.institution = fake.company()
+ user.user_profile.department = f"Department of {fake.job().title()}s"
+ user.user_profile.country = fake.country_code()
user.user_profile.receive_newsletter = True
user.user_profile.save()
users[username] = user
@@ -140,6 +152,26 @@ def _create_users(usernames):
return users
+def _create_direct_messages(users):
+ fake = Faker()
+
+ for combination in itertools.combinations(users.values(), 2):
+ conversation = Conversation.objects.create()
+ conversation.participants.set(combination)
+
+ unread = random.choice([True, False])
+
+ for _ in range(5):
+ sender = random.choice(combination)
+ message = DirectMessage.objects.create(
+ conversation=conversation,
+ sender=sender,
+ message=fake.text(max_nb_chars=160),
+ )
+ if unread:
+ message.unread_by.set({*combination} - {sender})
+
+
def _set_user_permissions(users):
users["admin"].is_staff = True
users["admin"].save()
@@ -180,7 +212,7 @@ def _create_help_forum():
def _create_demo_challenge(users, algorithm):
demo = Challenge.objects.create(
short_name="demo",
- description="demo project",
+ description="Demo Challenge",
creator=users["demo"],
use_workspaces=True,
hidden=False,