Skip to content

Commit

Permalink
Merge pull request #117 from CentreForDigitalHumanities/feature/contr…
Browse files Browse the repository at this point in the history
…ibutions

Feature/contributions
  • Loading branch information
XanderVertegaal authored Aug 27, 2024
2 parents dd3fb51 + db5b678 commit ec2f19c
Show file tree
Hide file tree
Showing 14 changed files with 267 additions and 13 deletions.
138 changes: 138 additions & 0 deletions backend/core/tests/test_contributors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import json
from graphene_django.utils.testing import GraphQLTestCase

from event.models import Episode
from source.models import Source
from user.models import User


class ContributorTestCase(GraphQLTestCase):
GRAPHQL_URL = "/api/graphql"

# Any mutation involving a model that inherits EntityDescription will do.
# For these tests, we'll use the Episode model.

UPDATE_EPISODE_MUTATION = """
mutation UPDATE_EPISODE($input: UpdateEpisodeMutationInput!) {
updateEpisode(input: $input) {
ok
errors {
field
messages
}
}
}
"""

@classmethod
def setUpTestData(cls) -> None:
source = Source.objects.create(name="A New Hope")
cls.episode = Episode.objects.create(
name="Death Star destroyed", source_id=source.pk
)
cls.user_1 = User.objects.create(username="Yoda")
cls.user_2 = User.objects.create(username="Obi-Wan")

def test_single_contributor_one_contribution(self):
self.client.force_login(self.user_1)

response = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Palpatine"},
)
self.assertResponseNoErrors(response)

episode = Episode.objects.get(id=self.episode.pk)

first_contributor = episode.contributors.first()

if not first_contributor:
self.fail("No contributors found after mutation.")

self.assertEqual(first_contributor.username, "Yoda")

def test_one_contributor_multiple_contributions(self):
self.client.force_login(self.user_1)

response = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Palpatine"},
)
self.assertResponseNoErrors(response)

response = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Darth Vader"},
)
self.assertResponseNoErrors(response)

episode = Episode.objects.get(id=self.episode.pk)

self.assertEqual(
len(episode.contributors.all()),
1,
"Contributors should not increase after multiple contributions.",
)

def test_multiple_contributors_one_contribution(self):
self.client.force_login(self.user_1)
response = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Han"},
)
self.assertResponseNoErrors(response)

self.client.force_login(self.user_2)
response = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Leia"},
)
self.assertResponseNoErrors(response)

episode = Episode.objects.get(id=self.episode.pk)

first_contributor = episode.contributors.first()
second_contributor = episode.contributors.last()

if not first_contributor or not second_contributor:
self.fail("No contributors found after mutation.")

self.assertEqual(first_contributor.username, "Yoda")
self.assertEqual(second_contributor.username, "Obi-Wan")

def test_multiple_contributors_multiple_contributions(self):
self.client.force_login(self.user_1)
response_1_1 = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Jabba"},
)
response_1_2 = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Anakin"},
)

self.client.force_login(self.user_2)
response_2_1 = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Jango"},
)
response_2_2 = self.query(
query=self.UPDATE_EPISODE_MUTATION,
input_data={"id": self.episode.pk, "name": "Boba"},
)

self.assertResponseNoErrors(response_1_1)
self.assertResponseNoErrors(response_1_2)
self.assertResponseNoErrors(response_2_1)
self.assertResponseNoErrors(response_2_2)

episode = Episode.objects.get(id=self.episode.pk)

first_contributor = episode.contributors.first()
second_contributor = episode.contributors.last()

if not first_contributor or not second_contributor:
self.fail("No contributors found after mutation.")

self.assertEqual(first_contributor.username, "Yoda")
self.assertEqual(second_contributor.username, "Obi-Wan")
1 change: 1 addition & 0 deletions backend/core/types/EntityDescriptionType.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from user.models import User
from user.types.UserType import UserType


class EntityDescriptionType(NamedType, AbstractDjangoObjectType):
"""
Type for models that extend the EntityDescription model.
Expand Down
5 changes: 4 additions & 1 deletion backend/event/mutations/UpdateEpisodeMutation.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def mutate(cls, root: None, info: ResolveInfo, input: UpdateEpisodeMutationInput
error = LettercraftErrorType(field="id", messages=[str(e)])
return cls(ok=False, errors=[error]) # type: ignore

episode = retrieved_object.object
episode: Episode = retrieved_object.object # type: ignore

try:
cls.mutate_object(input, episode, info)
Expand All @@ -44,6 +44,9 @@ def mutate(cls, root: None, info: ResolveInfo, input: UpdateEpisodeMutationInput
)
return cls(ok=False, errors=[error]) # type: ignore

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

episode.save()

return cls(ok=True, errors=[]) # type: ignore
8 changes: 7 additions & 1 deletion backend/user/types/UserType.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
from graphene import ResolveInfo
from graphene import ResolveInfo, String
from graphene_django import DjangoObjectType

from django.db.models import QuerySet
from user.models import User


class UserType(DjangoObjectType):
full_name = String(required=True)

class Meta:
model = User
fields = [
Expand All @@ -19,3 +21,7 @@ def get_queryset(
cls, queryset: QuerySet[User], info: ResolveInfo
) -> QuerySet[User]:
return queryset.all()

@staticmethod
def resolve_full_name(parent: User, info: ResolveInfo) -> str:
return f"{parent.first_name} {parent.last_name}"
7 changes: 6 additions & 1 deletion frontend/generated/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -796,6 +796,7 @@ export type UpdateOrCreateSourceMutation = {
export type UserType = {
__typename?: 'UserType';
firstName: Scalars['String']['output'];
fullName: Scalars['String']['output'];
id: Scalars['ID']['output'];
lastName: Scalars['String']['output'];
};
Expand Down Expand Up @@ -889,7 +890,7 @@ export type DataEntrySourceDetailQueryVariables = Exact<{
}>;


export type DataEntrySourceDetailQuery = { __typename?: 'Query', source: { __typename?: 'SourceType', id: string, name: string, editionAuthor: string, editionTitle: string, medievalAuthor: string, medievalTitle: string, numOfEpisodes: number, episodes: Array<{ __typename?: 'EpisodeType', id: string, name: string, description: string, summary: string, book: string, chapter: string, page: string, agents: Array<{ __typename?: 'AgentDescriptionType', id: string, name: string, isGroup: boolean, describes?: Array<{ __typename?: 'HistoricalPersonType', id: string, identifiable: boolean } | null> | null }>, gifts: Array<{ __typename?: 'GiftDescriptionType', id: string, name: string }>, letters: Array<{ __typename?: 'LetterDescriptionType', id: string, name: string }>, spaces: Array<{ __typename?: 'SpaceDescriptionType', id: string, name: string }> }> } };
export type DataEntrySourceDetailQuery = { __typename?: 'Query', source: { __typename?: 'SourceType', id: string, name: string, editionAuthor: string, editionTitle: string, medievalAuthor: string, medievalTitle: string, numOfEpisodes: number, 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, describes?: Array<{ __typename?: 'HistoricalPersonType', id: string, identifiable: boolean } | null> | null }>, gifts: Array<{ __typename?: 'GiftDescriptionType', id: string, name: string }>, letters: Array<{ __typename?: 'LetterDescriptionType', id: string, name: string }>, spaces: Array<{ __typename?: 'SpaceDescriptionType', id: string, name: string }> }> } };

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

Expand Down Expand Up @@ -1196,6 +1197,10 @@ export const DataEntrySourceDetailDocument = gql`
summary
book
chapter
contributors {
id
fullName
}
page
agents {
id
Expand Down
1 change: 1 addition & 0 deletions frontend/generated/schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,7 @@ type UpdateOrCreateSourceMutation {

type UserType {
firstName: String!
fullName: String!
id: ID!
lastName: String!
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
{{ episode.name }}
<br />
<small>
<span>book {{ episode.book }}</span>,
<span>book {{ episode.book }}</span
>,
<span>chapter {{ episode.chapter }}</span>
</small>
</div>
Expand All @@ -23,10 +24,7 @@

<dl>
<dt>Agents</dt>
<dd
*ngIf="episode.agents.length > 0; else noAgents"
class="ms-4"
>
<dd *ngIf="episode.agents.length > 0; else noAgents" class="ms-4">
<ul class="inline-list">
<li
*ngFor="let agent of episode.agents"
Expand All @@ -47,10 +45,7 @@
</ng-template>

<dt>Locations</dt>
<dd
*ngIf="episode.spaces.length > 0; else noSpaces"
class="ms-4"
>
<dd *ngIf="episode.spaces.length > 0; else noSpaces" class="ms-4">
<ul class="inline-list">
<li
*ngFor="let space of episode.spaces"
Expand Down Expand Up @@ -106,5 +101,8 @@
<dd class="ms-4">No objects have been added yet.</dd>
</ng-template>
</dl>
<div class="d-flex justify-content-end">
<lc-contributors [contributors]="episode.contributors" />
</div>
</div>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ describe("EpisodePreviewComponent", () => {
chapter: '',
page: '',
agents: [],
contributors: [],
gifts: [],
letters: [],
spaces: [],
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/app/data-entry/source/source.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ query DataEntrySourceDetail($id: ID!) {
summary
book
chapter
contributors {
id
fullName
}
page
agents {
id
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<small class="contributors text-muted">
<ng-container *ngIf="contributors.length === 0"
>No contributors</ng-container
>
<ng-container *ngIf="contributors.length === 1">Contributor: {{ contributors[0].fullName }}</ng-container>
<ng-container *ngIf="contributors.length === 2">Contributors: {{ contributors[0].fullName }}, {{ contributors[1].fullName }}</ng-container>
<ng-container *ngIf="contributors.length > 2">Contributors: {{ contributors[0].fullName }} et al.</ng-container>
</small>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { ContributorsComponent } from "./contributors.component";
import { UserType } from "generated/graphql";

describe("ContributorsComponent", () => {
let component: ContributorsComponent;
let fixture: ComponentFixture<ContributorsComponent>;

beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ContributorsComponent],
});
fixture = TestBed.createComponent(ContributorsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it("should create", () => {
expect(component).toBeTruthy();
});

it("should have input property 'contributors'", () => {
expect(component.contributors).toBeDefined();
});

it("should render no contributors", () => {
const contributors: Pick<UserType, "id" | "fullName">[] = [];
component.contributors = contributors;
fixture.detectChanges();
const compiled = fixture.nativeElement;
const element: HTMLElement = compiled.querySelector(".contributors");
self.expect(element).toBeDefined();
self.expect(element.textContent).toBe("No contributors");
});

it("should render one contributor", () => {
const contributors: Pick<UserType, "id" | "fullName">[] = [
{ id: "1", fullName: "Anakin Skywalker" },
];
component.contributors = contributors;
fixture.detectChanges();
const compiled = fixture.nativeElement;
const element: HTMLElement = compiled.querySelector(".contributors");
self.expect(element).toBeDefined();
self.expect(element.textContent).toBe("Contributor: Anakin Skywalker");
});

it("should render two contributors", () => {
const contributors: Pick<UserType, "id" | "fullName">[] = [
{ id: "1", fullName: "Anakin Skywalker" },
{ id: "2", fullName: "Jabba the Hutt" },
];
component.contributors = contributors;
fixture.detectChanges();
const compiled = fixture.nativeElement;
const element: HTMLElement = compiled.querySelector(".contributors");
self.expect(element).toBeDefined();
self.expect(element.textContent).toBe("Contributors: Anakin Skywalker, Jabba the Hutt");
});

it("should render three contributors", () => {
const contributors: Pick<UserType, "id" | "fullName">[] = [
{ id: "1", fullName: "Anakin Skywalker" },
{ id: "2", fullName: "Jabba the Hutt" },
{ id: "3", fullName: "Queen Amidala" },
];
component.contributors = contributors;
fixture.detectChanges();
const compiled = fixture.nativeElement;
const element: HTMLElement = compiled.querySelector(".contributors");
self.expect(element).toBeDefined();
self.expect(element.textContent).toBe("Contributors: Anakin Skywalker et al.");
});
});
12 changes: 12 additions & 0 deletions frontend/src/app/shared/contributors/contributors.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Component, Input } from "@angular/core";
import { UserType } from "generated/graphql";

@Component({
selector: "lc-contributors",
templateUrl: "./contributors.component.html",
styleUrls: ["./contributors.component.scss"],
})
export class ContributorsComponent {
@Input({ required: true })
public contributors: Pick<UserType, "id" | "fullName">[] = [];
}
Loading

0 comments on commit ec2f19c

Please sign in to comment.