Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/order episodes in source #155

Merged
merged 10 commits into from
Nov 12, 2024
12 changes: 12 additions & 0 deletions backend/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ def episode(db, source, agent_description, agent_description_2, letter_descripti
return event


@pytest.fixture()
def episode_2(db, source, agent_description, agent_description_2, letter_description):
event = Episode.objects.create(
name="Ernie eats a letter",
source=source,
)
event.agents.add(agent_description)
event.agents.add(agent_description_2)
event.letters.add(letter_description)
return event


@pytest.fixture()
def case_study(db):
case_study = CaseStudy.objects.create(name="Test Case Study")
Expand Down
1 change: 1 addition & 0 deletions backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ class EntityDescription(Named, models.Model):

class Meta:
abstract = True
order_with_respect_to = "source"

def __str__(self):
return f"{self.name} ({self.source})"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 4.2.7 on 2024-10-30 13:39

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('event', '0028_alter_episode_source_mention_and_more'),
]

operations = [
migrations.AlterOrderWithRespectTo(
name='episode',
order_with_respect_to='source',
),
]
13 changes: 13 additions & 0 deletions backend/event/mutations/CreateEpisodeMutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,26 @@ def mutate(cls, root: None, info: ResolveInfo, episode_data: CreateEpisodeInput)
name=getattr(episode_data, "name"),
source=source,
)
cls.append_to_source_episode_order(episode)
cls.add_contribution(episode, episode_data, info)

user = info.context.user
episode.contributors.add(user)

return cls(episode=episode, errors=[]) # type: ignore

@staticmethod
def append_to_source_episode_order(episode: Episode) -> None:
"""
Append the episode ID to the source's episode order.

This makes sure that the newly created episode is at the bottom of the episode list.
"""
ordered_episodes = episode.source.get_episode_order()
episode_ids = list(ordered_episodes.values_list("id", flat=True))
episode_ids.append(episode.pk)
episode.source.set_episode_order(episode_ids)

@staticmethod
def add_contribution(obj: Episode, data: CreateEpisodeInput, info: ResolveInfo):
if info.context:
Expand Down
38 changes: 38 additions & 0 deletions backend/event/mutations/UpdateEpisodeOrderMutation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from graphene import Mutation, Boolean, List, NonNull, ID, ResolveInfo

from graphql_app.types.LettercraftErrorType import LettercraftErrorType
from source.models import Source


class UpdateEpisodeOrderMutation(Mutation):
ok = Boolean(required=True)
errors = List(NonNull(LettercraftErrorType), required=True)

class Arguments:
episode_ids = List(
NonNull(ID), required=True, description="Ordered list of episode IDs"
)

@classmethod
def mutate(cls, root: None, info: ResolveInfo, episode_ids: list[str]):
source = Source.objects.filter(episode__in=episode_ids).distinct()

if source.count() > 1:
error = LettercraftErrorType(
field="episode_ids",
messages=["Multiple sources found for given episode IDs."],
)
return cls(ok=False, errors=[error]) # type: ignore

source = source.first()

if not source:
error = LettercraftErrorType(
field="episode_ids",
messages=["No source found for given episode IDs."],
)
return cls(ok=False, errors=[error]) # type: ignore
XanderVertegaal marked this conversation as resolved.
Show resolved Hide resolved

source.set_episode_order(episode_ids) # type: ignore
XanderVertegaal marked this conversation as resolved.
Show resolved Hide resolved

return cls(ok=True, errors=[]) # type: ignore
29 changes: 29 additions & 0 deletions backend/event/tests/test_event_mutations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from django.db.models.query import QuerySet


def test_episode_order_mutation(graphql_client, episode, episode_2):
result = graphql_client.execute(
f"""
mutation TestMutation {{
updateEpisodeOrder(
episodeIds: [
{episode_2.pk}, {episode.pk}
]
) {{
ok
errors {{
field
messages
}}
}}
}}
"""
)

assert result["data"]["updateEpisodeOrder"]["ok"] == True
assert result["data"]["updateEpisodeOrder"]["errors"] == []

order_queryset = episode.source.get_episode_order() # type: QuerySet
ids_list = list(order_queryset.values_list("id", flat=True))

assert ids_list == [episode_2.pk, episode.pk]
2 changes: 2 additions & 0 deletions backend/graphql_app/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from event.mutations.CreateEpisodeMutation import CreateEpisodeMutation
from event.mutations.DeleteEpisodeMutation import DeleteEpisodeMutation
from event.mutations.UpdateEpisodeOrderMutation import UpdateEpisodeOrderMutation
from event.queries import EventQueries
from letter.queries import LetterQueries
from person.queries import PersonQueries
Expand Down Expand Up @@ -57,6 +58,7 @@ class Mutation(ObjectType):
create_episode_entity_link = CreateEpisodeEntityLinkMutation.Field()
update_episode_entity_link = UpdateEpisodeEntityLinkMutation.Field()
delete_episode_entity_link = DeleteEpisodeEntityLinkMutation.Field()
update_episode_order = UpdateEpisodeOrderMutation.Field()
create_agent = CreateAgentMutation.Field()
update_agent = UpdateAgentMutation.Field()
delete_agent = DeleteAgentMutation.Field()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Generated by Django 4.2.7 on 2024-10-30 13:39

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('letter', '0021_alter_giftdescription_source_mention_and_more'),
]

operations = [
migrations.AlterOrderWithRespectTo(
name='giftdescription',
order_with_respect_to='source',
),
migrations.AlterOrderWithRespectTo(
name='letterdescription',
order_with_respect_to='source',
),
]
41 changes: 41 additions & 0 deletions frontend/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ export type Mutation = {
updateAgent?: Maybe<UpdateAgentMutation>;
updateEpisode?: Maybe<UpdateEpisodeMutation>;
updateEpisodeEntityLink?: Maybe<UpdateEpisodeEntityLinkMutation>;
updateEpisodeOrder?: Maybe<UpdateEpisodeOrderMutation>;
updateGift?: Maybe<UpdateGiftMutation>;
updateLetter?: Maybe<UpdateLetterMutation>;
updateOrCreateSource?: Maybe<UpdateOrCreateSourceMutation>;
Expand Down Expand Up @@ -557,6 +558,11 @@ export type MutationUpdateEpisodeEntityLinkArgs = {
};


export type MutationUpdateEpisodeOrderArgs = {
episodeIds: Array<Scalars['ID']['input']>;
};


export type MutationUpdateGiftArgs = {
giftData: UpdateGiftInput;
};
Expand Down Expand Up @@ -987,6 +993,12 @@ export type UpdateEpisodeMutation = {
ok: Scalars['Boolean']['output'];
};

export type UpdateEpisodeOrderMutation = {
__typename?: 'UpdateEpisodeOrderMutation';
errors: Array<LettercraftErrorType>;
ok: Scalars['Boolean']['output'];
};

export type UpdateGiftInput = {
book?: InputMaybe<Scalars['String']['input']>;
categories?: InputMaybe<Array<Scalars['ID']['input']>>;
Expand Down Expand Up @@ -1390,6 +1402,13 @@ export type DataEntrySourceDetailQueryVariables = Exact<{

export type DataEntrySourceDetailQuery = { __typename?: 'Query', source: { __typename?: 'SourceType', id: string, name: string, episodes: Array<{ __typename?: 'EpisodeType', id: string, name: string, description: string, summary: string, book: string, chapter: string, page: string, contributors: Array<{ __typename?: 'UserType', id: string, fullName: string }>, agents: Array<{ __typename?: 'AgentDescriptionType', id: string, name: string, isGroup: boolean, identified: boolean }>, gifts: Array<{ __typename?: 'GiftDescriptionType', id: string, name: string }>, letters: Array<{ __typename?: 'LetterDescriptionType', id: string, name: string }>, spaces: Array<{ __typename?: 'SpaceDescriptionType', id: string, name: string, hasIdentifiableFeatures: boolean }> }> } };

export type DataEntryUpdateEpisodeOrderMutationVariables = Exact<{
episodeIds: Array<Scalars['ID']['input']> | Scalars['ID']['input'];
}>;


export type DataEntryUpdateEpisodeOrderMutation = { __typename?: 'Mutation', updateEpisodeOrder?: { __typename?: 'UpdateEpisodeOrderMutation', ok: boolean, errors: Array<{ __typename?: 'LettercraftErrorType', field: string, messages: Array<string> }> } | null };

export type DataEntrySourceListQueryVariables = Exact<{ [key: string]: never; }>;


Expand Down Expand Up @@ -2589,6 +2608,28 @@ export const DataEntrySourceDetailDocument = gql`
export class DataEntrySourceDetailGQL extends Apollo.Query<DataEntrySourceDetailQuery, DataEntrySourceDetailQueryVariables> {
override document = DataEntrySourceDetailDocument;

constructor(apollo: Apollo.Apollo) {
super(apollo);
}
}
export const DataEntryUpdateEpisodeOrderDocument = gql`
mutation DataEntryUpdateEpisodeOrder($episodeIds: [ID!]!) {
updateEpisodeOrder(episodeIds: $episodeIds) {
ok
errors {
field
messages
}
}
}
`;

@Injectable({
providedIn: 'root'
})
export class DataEntryUpdateEpisodeOrderGQL extends Apollo.Mutation<DataEntryUpdateEpisodeOrderMutation, DataEntryUpdateEpisodeOrderMutationVariables> {
override document = DataEntryUpdateEpisodeOrderDocument;

constructor(apollo: Apollo.Apollo) {
super(apollo);
}
Expand Down
9 changes: 9 additions & 0 deletions frontend/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,10 @@ type Mutation {
updateAgent(agentData: UpdateAgentInput!): UpdateAgentMutation
updateEpisode(episodeData: UpdateEpisodeInput!): UpdateEpisodeMutation
updateEpisodeEntityLink(data: UpdateEpisodeEntityLinkInput!): UpdateEpisodeEntityLinkMutation
updateEpisodeOrder(
"""Ordered list of episode IDs"""
episodeIds: [ID!]!
): UpdateEpisodeOrderMutation
updateGift(giftData: UpdateGiftInput!): UpdateGiftMutation
updateLetter(letterData: UpdateLetterInput!): UpdateLetterMutation
updateOrCreateSource(sourceData: UpdateCreateSourceInput!): UpdateOrCreateSourceMutation
Expand Down Expand Up @@ -839,6 +843,11 @@ type UpdateEpisodeMutation {
ok: Boolean!
}

type UpdateEpisodeOrderMutation {
errors: [LettercraftErrorType!]!
ok: Boolean!
}

input UpdateGiftInput {
book: String
categories: [ID!]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@
<span *ngIf="episode.page" class="inline-list-item">page {{ episode.page }}</span>
</small>
</div>
<lc-action-button-group
[editLink]="['/', 'data-entry', 'episodes', episode.id]"
(deleteAction)="onClickDelete(episode.id)"
[showButtonText]="false"
/>
<div class="button-group-wrapper">
<lc-order-button-group
entityName="episode"
(changeOrder)="changeEpisodeOrder.emit($event)"
/>
<lc-action-button-group
[editLink]="['/', 'data-entry', 'episodes', episode.id]"
(deleteAction)="onClickDelete(episode.id)"
[showButtonText]="false"
/>
</div>
</div>
</div>
<div class="card-body">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.button-group-wrapper {
display: flex;
align-items: center;
gap: 1rem;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import { Component, DestroyRef, Input } from "@angular/core";
import {
Component,
DestroyRef,
EventEmitter,
Input,
Output,
} from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ModalService } from "@services/modal.service";
import { ToastService } from "@services/toast.service";
import { dataIcons } from "@shared/icons";
import { agentIcon, locationIcon } from "@shared/icons-utils";
import { OrderChange } from "@shared/order-button-group/order-button-group.component";
import {
DataEntryDeleteEpisodeGQL,
DataEntrySourceDetailQuery,
Expand All @@ -21,7 +28,12 @@ type QueriedEpisode = NonNullable<
export class EpisodePreviewComponent {
@Input({ required: true })
public episode!: QueriedEpisode;

@Output()
public changeEpisodeOrder = new EventEmitter<OrderChange>();

public dataIcons = dataIcons;

agentIcon = agentIcon;
locationIcon = locationIcon;

Expand All @@ -40,7 +52,8 @@ export class EpisodePreviewComponent {
})
.then(() => {
this.performDelete(episodeId);
}).catch(() => {
})
.catch(() => {
// Do nothing on cancel / dismissal.
});
}
Expand Down
1 change: 1 addition & 0 deletions frontend/src/app/data-entry/source/source.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ <h2 class="mb-4">Episodes</h2>
<lc-episode-preview
*ngFor="let episode of source.episodes; trackBy: identify"
[episode]="episode"
(changeEpisodeOrder)="reorderEpisodes(source.episodes, episode.id, $event)"
/>
</div>
<div class="btn-group mb-4" *ngIf="source$ | async as source">
Expand Down
Loading