Skip to content

Commit 5bb24ab

Browse files
authored
KK-72 | Add event mutation (#37)
* Add mutation API for create events/venues/occurrences
1 parent 59f3dee commit 5bb24ab

12 files changed

Lines changed: 365 additions & 13 deletions

File tree

common/models.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from django.db import models
44
from django.utils.translation import ugettext_lazy as _
5+
from parler.managers import TranslatableQuerySet as ParlerTranslatableQuerySet
6+
from parler.models import TranslatableModel as ParlerTranslatableModel
57

68

79
class TimestampedModel(models.Model):
@@ -19,3 +21,20 @@ class UUIDPrimaryKeyModel(models.Model):
1921

2022
class Meta:
2123
abstract = True
24+
25+
26+
class TranslatableQuerySet(ParlerTranslatableQuerySet):
27+
def create_translatable_object(self, **kwargs):
28+
translations = kwargs.pop("translations")
29+
obj = self.create(**kwargs)
30+
for translation in translations:
31+
language_code = translation.pop("language_code")
32+
obj.create_translation(language_code=language_code, **translation)
33+
return obj
34+
35+
36+
class TranslatableModel(ParlerTranslatableModel):
37+
objects = TranslatableQuerySet.as_manager()
38+
39+
class Meta:
40+
abstract = True

events/admin.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@
44
from .models import Event, Occurrence
55

66

7+
class OccurrencesInline(admin.StackedInline):
8+
model = Occurrence
9+
extra = 1
10+
11+
712
@admin.register(Event)
813
class EventAdmin(TranslatableAdmin):
914
list_display = (
@@ -17,6 +22,9 @@ class EventAdmin(TranslatableAdmin):
1722
)
1823
list_display_links = ("id", "name")
1924
exclude = ("id", "created_at", "updated_at")
25+
inlines = [
26+
OccurrencesInline,
27+
]
2028

2129

2230
@admin.register(Occurrence)

events/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from django.db import models
22
from django.utils.translation import ugettext_lazy as _
3-
from parler.models import TranslatableModel, TranslatedFields
3+
from parler.models import TranslatedFields
44

5-
from common.models import TimestampedModel
5+
from common.models import TimestampedModel, TranslatableModel
66
from venues.models import Venue
77

88

events/schema.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import graphene
22
from django.apps import apps
3+
from django.db import transaction
34
from graphene import relay
45
from graphene_django import DjangoConnectionField, DjangoObjectType
5-
from graphql_jwt.decorators import login_required
6+
from graphql_jwt.decorators import login_required, staff_member_required
7+
from graphql_relay import from_global_id
68

79
from events.models import Event, Occurrence
8-
from venues.schema import VenueNode
10+
from kukkuu.exceptions import KukkuuGraphQLError
11+
from venues.models import Venue
912

1013
EventTranslation = apps.get_model("events", "EventTranslation")
1114

@@ -34,8 +37,9 @@ def get_node(cls, info, id):
3437

3538

3639
class OccurrenceNode(DjangoObjectType):
37-
venue = graphene.Field(VenueNode)
38-
event = graphene.Field(EventNode)
40+
venue_id = graphene.GlobalID()
41+
event_id = graphene.GlobalID()
42+
time = graphene.DateTime()
3943

4044
@classmethod
4145
@login_required
@@ -53,9 +57,72 @@ class Meta:
5357
interfaces = (relay.Node,)
5458

5559

60+
class EventTranslationsInput(graphene.InputObjectType):
61+
name = graphene.String(required=True)
62+
short_description = graphene.String()
63+
description = graphene.String()
64+
language_code = graphene.String(required=True)
65+
66+
67+
class AddEventMutation(graphene.relay.ClientIDMutation):
68+
class Input:
69+
translations = graphene.List(EventTranslationsInput)
70+
duration = graphene.Int()
71+
participants_per_invite = graphene.String(required=True)
72+
capacity_per_occurrence = graphene.Int(required=True)
73+
published_at = graphene.DateTime()
74+
75+
event = graphene.Field(EventNode)
76+
77+
@classmethod
78+
@staff_member_required
79+
@transaction.atomic
80+
def mutate_and_get_payload(cls, root, info, **kwargs):
81+
# TODO: Add validation
82+
event = Event.objects.create_translatable_object(**kwargs)
83+
# TODO: Add support for image upload
84+
return AddEventMutation(event=event)
85+
86+
87+
class AddOccurrenceMutation(graphene.relay.ClientIDMutation):
88+
class Input:
89+
time = graphene.DateTime(required=True)
90+
event_id = graphene.GlobalID(required=True)
91+
venue_id = graphene.GlobalID(required=True)
92+
93+
occurrence = graphene.Field(OccurrenceNode)
94+
95+
@classmethod
96+
@staff_member_required
97+
@transaction.atomic
98+
def mutate_and_get_payload(cls, root, info, **kwargs):
99+
# TODO: Validate data
100+
event_id = from_global_id(kwargs["event_id"])[1]
101+
try:
102+
event = Event.objects.get(pk=event_id)
103+
kwargs["event_id"] = event_id
104+
except Event.DoesNotExist as e:
105+
raise KukkuuGraphQLError(e)
106+
107+
venue_id = from_global_id(kwargs["venue_id"])[1]
108+
try:
109+
venue = Venue.objects.get(pk=venue_id)
110+
kwargs["venue_id"] = venue_id
111+
except Venue.DoesNotExist as e:
112+
raise KukkuuGraphQLError(e)
113+
114+
occurrence = Occurrence.objects.create(**kwargs, event=event, venue=venue)
115+
return AddOccurrenceMutation(occurrence=occurrence)
116+
117+
56118
class Query:
57119
events = DjangoConnectionField(EventNode)
58120
occurrences = DjangoConnectionField(OccurrenceNode)
59121

60122
event = relay.Node.Field(EventNode)
61123
occurrence = relay.Node.Field(OccurrenceNode)
124+
125+
126+
class Mutation:
127+
add_event = AddEventMutation.Field()
128+
add_occurrence = AddOccurrenceMutation.Field()

events/tests/snapshots/snap_test_api.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,37 @@
164164
}
165165
}
166166
}
167+
168+
snapshots["test_add_event_staff_user 1"] = {
169+
"data": {
170+
"addEvent": {
171+
"event": {
172+
"capacityPerOccurrence": 30,
173+
"duration": 1000,
174+
"image": "",
175+
"participantsPerInvite": "FAMILY",
176+
"publishedAt": "1986-12-12T16:40:48+00:00",
177+
"translations": [
178+
{
179+
"description": "desc",
180+
"languageCode": "FI",
181+
"name": "Event test",
182+
"shortDescription": "Short desc",
183+
}
184+
],
185+
}
186+
}
187+
}
188+
}
189+
190+
snapshots["test_add_occurrence_staff_user 1"] = {
191+
"data": {
192+
"addOccurrence": {
193+
"occurrence": {
194+
"event": {"id": "RXZlbnROb2RlOjg="},
195+
"time": "1986-12-12T16:40:48+00:00",
196+
"venue": {"id": "VmVudWVOb2RlOjU="},
197+
}
198+
}
199+
}
200+
}

events/tests/test_api.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
from copy import deepcopy
2+
13
import pytest
24
from graphql_relay import to_global_id
35

46
from common.tests.utils import assert_permission_denied
57
from events.factories import EventFactory, OccurrenceFactory
8+
from venues.factories import VenueFactory
69

710

811
@pytest.fixture(autouse=True)
@@ -153,6 +156,65 @@ def autouse_db(db):
153156
}
154157
"""
155158

159+
ADD_EVENT_MUTATION = """
160+
mutation AddEvent($input: AddEventMutationInput!) {
161+
addEvent(input: $input) {
162+
event {
163+
translations{
164+
languageCode
165+
name
166+
description
167+
shortDescription
168+
}
169+
duration
170+
image
171+
participantsPerInvite
172+
capacityPerOccurrence
173+
publishedAt
174+
}
175+
}
176+
}
177+
"""
178+
179+
ADD_EVENT_VARIABLES = {
180+
"input": {
181+
"translations": [
182+
{
183+
"name": "Event test",
184+
"shortDescription": "Short desc",
185+
"description": "desc",
186+
"languageCode": "fi",
187+
}
188+
],
189+
"duration": 1000,
190+
# "image": None,
191+
"participantsPerInvite": "family",
192+
"capacityPerOccurrence": 30,
193+
"publishedAt": "1986-12-12T16:40:48+00:00",
194+
}
195+
}
196+
197+
ADD_OCCURRENCE_MUTATION = """
198+
mutation AddOccurrence($input: AddOccurrenceMutationInput!) {
199+
addOccurrence(input: $input) {
200+
occurrence{
201+
event{
202+
id,
203+
}
204+
venue {
205+
id
206+
}
207+
time
208+
}
209+
}
210+
}
211+
212+
"""
213+
214+
ADD_OCCURRENCE_VARIABLES = {
215+
"input": {"eventId": "", "venueId": "", "time": "1986-12-12T16:40:48+00:00"}
216+
}
217+
156218

157219
def test_events_query_unauthenticated(api_client):
158220
executed = api_client.execute(EVENTS_QUERY)
@@ -212,3 +274,44 @@ def test_occurrence_query_normal_user(snapshot, user_api_client):
212274
executed = user_api_client.execute(OCCURRENCE_QUERY, variables=variables)
213275

214276
snapshot.assert_match(executed)
277+
278+
279+
def test_add_event_permission_denied(api_client, user_api_client):
280+
executed = api_client.execute(ADD_EVENT_MUTATION, variables=ADD_EVENT_VARIABLES)
281+
assert_permission_denied(executed)
282+
283+
executed = user_api_client.execute(
284+
ADD_EVENT_MUTATION, variables=ADD_EVENT_VARIABLES
285+
)
286+
assert_permission_denied(executed)
287+
288+
289+
def test_add_event_staff_user(snapshot, staff_api_client):
290+
executed = staff_api_client.execute(
291+
ADD_EVENT_MUTATION, variables=ADD_EVENT_VARIABLES
292+
)
293+
snapshot.assert_match(executed)
294+
295+
296+
def test_add_occurrence_permission_denied(api_client, user_api_client):
297+
executed = api_client.execute(
298+
ADD_OCCURRENCE_MUTATION, variables=ADD_OCCURRENCE_VARIABLES
299+
)
300+
assert_permission_denied(executed)
301+
302+
executed = user_api_client.execute(
303+
ADD_OCCURRENCE_MUTATION, variables=ADD_OCCURRENCE_VARIABLES
304+
)
305+
assert_permission_denied(executed)
306+
307+
308+
def test_add_occurrence_staff_user(snapshot, staff_api_client):
309+
event = EventFactory()
310+
venue = VenueFactory()
311+
occurrence_variables = deepcopy(ADD_OCCURRENCE_VARIABLES)
312+
occurrence_variables["input"]["eventId"] = to_global_id("EventNode", event.id)
313+
occurrence_variables["input"]["venueId"] = to_global_id("VenueNode", venue.id)
314+
executed = staff_api_client.execute(
315+
ADD_OCCURRENCE_MUTATION, variables=occurrence_variables
316+
)
317+
snapshot.assert_match(executed)

kukkuu/schema.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@
66
import venues.schema
77

88

9+
class Mutation(
10+
children.schema.Mutation,
11+
users.schema.Mutation,
12+
events.schema.Mutation,
13+
venues.schema.Mutation,
14+
graphene.ObjectType,
15+
):
16+
pass
17+
18+
919
class Query(
1020
children.schema.Query,
1121
users.schema.Query,
@@ -16,8 +26,4 @@ class Query(
1626
pass
1727

1828

19-
class Mutation(children.schema.Mutation, users.schema.Mutation, graphene.ObjectType):
20-
pass
21-
22-
2329
schema = graphene.Schema(query=Query, mutation=Mutation)

venues/admin.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from django.contrib import admin
22
from parler.admin import TranslatableAdmin
33

4+
from events.admin import OccurrencesInline
45
from venues.models import Venue
56

67

@@ -9,3 +10,7 @@ class VenueAdmin(TranslatableAdmin):
910
list_display = ("id", "name", "created_at", "updated_at")
1011
list_display_links = ("id", "name")
1112
exclude = ("id", "created_at", "updated_at")
13+
14+
inlines = [
15+
OccurrencesInline,
16+
]

venues/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from django.db import models
22
from django.utils.translation import ugettext_lazy as _
3-
from parler.models import TranslatableModel, TranslatedFields
3+
from parler.models import TranslatedFields
44

5-
from common.models import TimestampedModel
5+
from common.models import TimestampedModel, TranslatableModel
66

77

88
class Venue(TimestampedModel, TranslatableModel):

0 commit comments

Comments
 (0)