Skip to content
Merged
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
8 changes: 7 additions & 1 deletion apps/resources/admin.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Register your models here.
from django.contrib import admin

from apps.resources.models import CaseStudy, ContactRequest
from apps.resources.models import CaseStudy, ContactRequest, RequestDemo
from apps.tool_picker.admin import ReadOnlyMixin


Expand All @@ -17,3 +17,9 @@ class CaseStudyAdmin(admin.ModelAdmin[CaseStudy]):
search_fields = ("title",)
list_select_related = ("tool",)
autocomplete_fields = ("tool",)


@admin.register(RequestDemo)
class RequestDemoAdmin(ReadOnlyMixin, admin.ModelAdmin[RequestDemo]):
list_display = ("name", "email", "national_society", "created_at")
search_fields = ("name", "email")
11 changes: 11 additions & 0 deletions apps/resources/graphql/inputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ class ContactRequestInput:
content: str
captcha_hashkey: str
captcha_code: str


@strawberry.input
class RequestDemoInput:
name: str
email: str
content: str
national_society: str
tool: int
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

shouldn't this be ID type?

captcha_hashkey: str
captcha_code: str
14 changes: 11 additions & 3 deletions apps/resources/graphql/mutations.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import strawberry
import strawberry_django

from apps.resources.graphql.inputs import ContactRequestInput
from apps.resources.graphql.types import ContactRequestType
from apps.resources.serializers import ContactRequestSerializer
from apps.resources.graphql.inputs import ContactRequestInput, RequestDemoInput
from apps.resources.graphql.types import ContactRequestType, RequestDemoType
from apps.resources.serializers import ContactRequestSerializer, RequestDemoSerializer
from main.graphql.context import Info
from utils.graphql.mutations import ModelMutation
from utils.graphql.types import MutationResponseType
Expand All @@ -18,3 +18,11 @@ async def create_contact_request(
data: ContactRequestInput,
) -> MutationResponseType[ContactRequestType]:
return await ModelMutation(ContactRequestSerializer).handle_create_mutation(data, info, None)

@strawberry_django.mutation()
async def create_demo_request(
self,
info: Info,
data: RequestDemoInput,
) -> MutationResponseType[RequestDemoType]:
return await ModelMutation(RequestDemoSerializer).handle_create_mutation(data, info, None)
13 changes: 12 additions & 1 deletion apps/resources/graphql/types.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import strawberry
import strawberry_django

from apps.resources.models import CaseStudy, ContactRequest
from apps.resources.models import CaseStudy, ContactRequest, RequestDemo
from utils.graphql.types import DjangoFileType


Expand All @@ -23,3 +23,14 @@ class ContactRequestType:
created_at: strawberry.auto
content: strawberry.auto
national_society: strawberry.auto


@strawberry_django.type(RequestDemo)
class RequestDemoType:
id: strawberry.ID
name: strawberry.auto
email: strawberry.auto
created_at: strawberry.auto
content: strawberry.auto
national_society: strawberry.auto
tool: strawberry.auto
30 changes: 30 additions & 0 deletions apps/resources/migrations/0003_requestdemo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Generated by Django 5.2.9 on 2026-03-17 08:23

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('resources', '0002_casestudy_link'),
('tool_picker', '0004_alter_checkboxoption_order_alter_question_order'),
]

operations = [
migrations.CreateModel(
name='RequestDemo',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('email', models.CharField(max_length=200)),
('created_at', models.DateTimeField(auto_now_add=True)),
('national_society', models.CharField(max_length=200)),
('content', models.TextField(blank=True, null=True)),
('tool', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='request_demo_tool', to='tool_picker.tool', verbose_name='Related Tool')),
],
options={
'ordering': ['name'],
},
),
]
23 changes: 23 additions & 0 deletions apps/resources/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,26 @@ class Meta(UserResource.Meta):
@typing.override
def __str__(self):
return self.title


class RequestDemo(models.Model):
"""Model representing contact where anyone can request the tool demo to the admins."""

name = models.CharField[str, str](max_length=200)
email = models.CharField[str, str](max_length=200)
created_at = models.DateTimeField[datetime.datetime, datetime.datetime](auto_now_add=True)
national_society = models.CharField[str, str](max_length=200)
content = models.TextField[str, str](blank=True, null=True)
tool = models.ForeignKey[Tool, Tool](
Tool,
related_name="request_demo_tool",
verbose_name="Related Tool",
on_delete=models.CASCADE,
)

class Meta:
ordering = ["name"]

@typing.override
def __str__(self):
return self.name
30 changes: 29 additions & 1 deletion apps/resources/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
from captcha.serializers import CaptchaModelSerializer
from rest_framework import serializers

from apps.resources.models import ContactRequest
from apps.resources.models import ContactRequest, RequestDemo
from apps.tool_picker.models import Tool


class ContactRequestSerializer(CaptchaModelSerializer):
Expand All @@ -26,3 +27,30 @@ def create(self, validated_data: dict[str, typing.Any]):
validated_data.pop("captcha_code", None)
validated_data.pop("captcha_hashkey", None)
return super().create(validated_data)


class RequestDemoSerializer(CaptchaModelSerializer):
captcha_code = serializers.CharField(write_only=True)
captcha_hashkey = serializers.CharField(write_only=True)

tool = serializers.PrimaryKeyRelatedField(
queryset=Tool.objects.all(),
)

class Meta:
model = RequestDemo
fields = (
"name",
"email",
"national_society",
"content",
"tool",
"captcha_code",
"captcha_hashkey",
)

@typing.override
def create(self, validated_data: dict[str, typing.Any]):
validated_data.pop("captcha_code", None)
validated_data.pop("captcha_hashkey", None)
return super().create(validated_data)
120 changes: 119 additions & 1 deletion apps/resources/tests/mutation_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

from captcha.models import CaptchaStore

from apps.resources.models import ContactRequest
from apps.resources.models import ContactRequest, RequestDemo
from apps.tool_picker.factories import ToolFactory
from apps.user.factories import UserFactory
from main.tests import TestCase

Expand Down Expand Up @@ -114,3 +115,120 @@ def test_create_contact_request_without_captcha(self):
"pydantic_errors": None,
},
]


class TestRequestDemoMutation(TestCase):
class Mutation:
CREATE_DEMO_REQUEST = """
mutation createDemoRequest($data: RequestDemoInput!) {
createDemoRequest(data: $data) {
... on RequestDemoTypeMutationResponseType {
errors
ok
result {
id
name
email
nationalSociety
content
tool{
pk
}
}
}
... on OperationInfo {
__typename
messages {
code
field
kind
message
}
}
}
}
"""

@typing.override
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.user = UserFactory.create(email="test@gmail.com")
cls.tool = ToolFactory.create()

def _create_tool_demo_request_mutation(self, data: dict[str, str | int], **kwargs: typing.Any):
return self.query_check(
query=self.Mutation.CREATE_DEMO_REQUEST,
variables={
"data": data,
},
)

def test_create_tool_demo_request(self):
captcha_key = CaptchaStore.generate_key()
captcha_obj = CaptchaStore.objects.get(hashkey=captcha_key)
captcha_code = captcha_obj.response

tool_demo_request_data = {
"name": "John",
"email": "john@test.com",
"nationalSociety": "Test National Society",
"content": "This is test content",
"tool": self.tool.id,
"captchaHashkey": captcha_key,
"captchaCode": captcha_code,
}

content = self._create_tool_demo_request_mutation(data=tool_demo_request_data)
response_data = content["data"]["createDemoRequest"]

assert response_data["ok"] is True
assert response_data["errors"] is None

demo_request = RequestDemo.objects.get(pk=response_data["result"]["id"])
assert response_data["result"] == {
"id": self.gID(demo_request.pk),
"name": demo_request.name,
"email": demo_request.email,
"content": demo_request.content,
"nationalSociety": demo_request.national_society,
"tool": {
"pk": self.gID(demo_request.tool.pk),
},
}

def test_create_tool_demo_request_without_captcha(self):
tool_demo_request_data = {
"name": "John",
"email": "john@test.com",
"nationalSociety": "Test National Society",
"content": "This is test content",
"tool": self.tool.id,
"captchaHashkey": "",
"captchaCode": "",
}

content = self._create_tool_demo_request_mutation(data=tool_demo_request_data)
response = content["data"]["createDemoRequest"]

assert response["ok"] is False
assert response["result"] is None

assert response["errors"] == [
{
"field": "captchaCode",
"client_id": None,
"messages": "This field may not be blank.",
"object_errors": None,
"array_errors": None,
"pydantic_errors": None,
},
{
"field": "captchaHashkey",
"client_id": None,
"messages": "This field may not be blank.",
"object_errors": None,
"array_errors": None,
"pydantic_errors": None,
},
]
36 changes: 36 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@ type ContactRequestTypeMutationResponseType {

union CreateContactRequestPayload = ContactRequestTypeMutationResponseType | OperationInfo

union CreateDemoRequestPayload = RequestDemoTypeMutationResponseType | OperationInfo

union CreateUserSubmissionPayload = UserSubmissionTypeMutationResponseType | OperationInfo

"""A generic type to return error messages"""
Expand All @@ -133,8 +135,13 @@ type DjangoFileType {
url: String!
}

type DjangoModelType {
pk: ID!
}

type Mutation {
createContactRequest(data: ContactRequestInput!): CreateContactRequestPayload!
createDemoRequest(data: RequestDemoInput!): CreateDemoRequestPayload!
createUserSubmission(data: UserSubmissionInput!): CreateUserSubmissionPayload!
}

Expand Down Expand Up @@ -273,6 +280,35 @@ type RecommendationResultTypeOffsetPaginated {
results: [RecommendationResultType!]!
}

input RequestDemoInput {
name: String!
email: String!
content: String!
nationalSociety: String!
tool: Int!
captchaHashkey: String!
captchaCode: String!
}

"""
Model representing contact where anyone can request the tool demo to the admins.
"""
type RequestDemoType {
id: ID!
name: String!
email: String!
createdAt: DateTime!
content: String
nationalSociety: String!
tool: DjangoModelType!
}

type RequestDemoTypeMutationResponseType {
ok: Boolean!
errors: CustomErrorType
result: RequestDemoType
}

"""Model representing tool's selection of question and its answer."""
type ToolAnswerType {
id: ID!
Expand Down
Loading