Skip to content

Commit

Permalink
Merge pull request #155 from CentreForDigitalHumanities/feature/order…
Browse files Browse the repository at this point in the history
…-episodes-in-source

Feature/order episodes in source
  • Loading branch information
XanderVertegaal authored Nov 12, 2024
2 parents ccdc41a + eb77454 commit 7980b4a
Show file tree
Hide file tree
Showing 24 changed files with 462 additions and 20 deletions.
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
56 changes: 56 additions & 0 deletions backend/event/mutations/UpdateEpisodeOrderMutation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from graphene import Mutation, Boolean, List, NonNull, ID, ResolveInfo

from event.models import Episode
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]):
if len(episode_ids) == 0:
error = LettercraftErrorType(
field="episode_ids", messages=["No episode IDs provided."]
)
return cls(ok=False, errors=[error]) # type: ignore

# Check if all episode IDs are valid.
episodes = Episode.objects.filter(id__in=episode_ids).prefetch_related("source")
if episodes.count() != len(episode_ids):
error = LettercraftErrorType(
field="episode_ids",
messages=["Not every provided episode ID is valid."],
)
return cls(ok=False, errors=[error]) # type: ignore

corresponding_sources: set[Source] = {episode.source for episode in episodes}

# Check if there is at least one source for the provided episode IDs.
# (There should always be.)
if len(corresponding_sources) == 0:
error = LettercraftErrorType(
field="episode_ids", messages=["No source found for given episode IDs."]
)
return cls(ok=False, errors=[error]) # type: ignore

# Check if all provided episode IDs belong to the same source.
if len(corresponding_sources) > 1:
error = LettercraftErrorType(
field="episode_ids",
messages=["The provided episode IDs belong to more than one source."],
)
return cls(ok=False, errors=[error]) # type: ignore

source = corresponding_sources.pop()

source.set_episode_order(episode_ids) # type: ignore

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,19 @@
<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"
[upDisabled]="isFirst"
[downDisabled]="isLast"
(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 @@ -19,9 +26,13 @@ type QueriedEpisode = NonNullable<
styleUrls: ["./episode-preview.component.scss"],
})
export class EpisodePreviewComponent {
@Input({ required: true })
public episode!: QueriedEpisode;
@Input({ required: true }) public episode!: QueriedEpisode;
@Input() public isLast = false;
@Input() public isFirst = false;
@Output() public changeEpisodeOrder = new EventEmitter<OrderChange>();

public dataIcons = dataIcons;

agentIcon = agentIcon;
locationIcon = locationIcon;

Expand All @@ -40,7 +51,8 @@ export class EpisodePreviewComponent {
})
.then(() => {
this.performDelete(episodeId);
}).catch(() => {
})
.catch(() => {
// Do nothing on cancel / dismissal.
});
}
Expand Down
Loading

0 comments on commit 7980b4a

Please sign in to comment.