Skip to content

Commit f0fe304

Browse files
committed
Add table showing number of lines per character per scene
1 parent 65e9e7a commit f0fe304

3 files changed

Lines changed: 281 additions & 51 deletions

File tree

client/src/views/show/config/ConfigCharacters.vue

Lines changed: 62 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -5,58 +5,67 @@
55
>
66
<b-row>
77
<b-col>
8-
<h5>Character List</h5>
9-
<b-table
10-
id="character-table"
11-
:items="CHARACTER_LIST"
12-
:fields="characterFields"
13-
:per-page="rowsPerPage"
14-
:current-page="currentPage"
15-
show-empty
16-
>
17-
<template #head(btn)="data">
18-
<b-button
19-
v-b-modal.new-character
20-
variant="outline-success"
8+
<b-tabs content-class="mt-3">
9+
<b-tab
10+
title="Characters"
11+
active
12+
>
13+
<b-table
14+
id="character-table"
15+
:items="CHARACTER_LIST"
16+
:fields="characterFields"
17+
:per-page="rowsPerPage"
18+
:current-page="currentPage"
19+
show-empty
2120
>
22-
New Character
23-
</b-button>
24-
</template>
25-
<template #cell(cast_member)="data">
26-
<template v-if="data.item.cast_member">
27-
{{ data.item.cast_member.first_name }} {{ data.item.cast_member.last_name }}
28-
</template>
29-
<template v-else>
30-
<b-link @click="openEditForm(data)">
31-
Set Cast Member
32-
</b-link>
33-
</template>
34-
</template>
35-
<template #cell(btn)="data">
36-
<b-button-group>
37-
<b-button
38-
variant="warning"
39-
@click="openEditForm(data)"
40-
>
41-
Edit
42-
</b-button>
43-
<b-button
44-
variant="danger"
45-
@click="deleteCharacter(data)"
46-
>
47-
Delete
48-
</b-button>
49-
</b-button-group>
50-
</template>
51-
</b-table>
52-
<b-pagination
53-
v-show="CHARACTER_LIST.length > rowsPerPage"
54-
v-model="currentPage"
55-
:total-rows="CHARACTER_LIST.length"
56-
:per-page="rowsPerPage"
57-
aria-controls="character-table"
58-
class="justify-content-center"
59-
/>
21+
<template #head(btn)="data">
22+
<b-button
23+
v-b-modal.new-character
24+
variant="outline-success"
25+
>
26+
New Character
27+
</b-button>
28+
</template>
29+
<template #cell(cast_member)="data">
30+
<template v-if="data.item.cast_member">
31+
{{ data.item.cast_member.first_name }} {{ data.item.cast_member.last_name }}
32+
</template>
33+
<template v-else>
34+
<b-link @click="openEditForm(data)">
35+
Set Cast Member
36+
</b-link>
37+
</template>
38+
</template>
39+
<template #cell(btn)="data">
40+
<b-button-group>
41+
<b-button
42+
variant="warning"
43+
@click="openEditForm(data)"
44+
>
45+
Edit
46+
</b-button>
47+
<b-button
48+
variant="danger"
49+
@click="deleteCharacter(data)"
50+
>
51+
Delete
52+
</b-button>
53+
</b-button-group>
54+
</template>
55+
</b-table>
56+
<b-pagination
57+
v-show="CHARACTER_LIST.length > rowsPerPage"
58+
v-model="currentPage"
59+
:total-rows="CHARACTER_LIST.length"
60+
:per-page="rowsPerPage"
61+
aria-controls="character-table"
62+
class="justify-content-center"
63+
/>
64+
</b-tab>
65+
<b-tab title="Line Counts">
66+
<character-line-stats />
67+
</b-tab>
68+
</b-tabs>
6069
</b-col>
6170
</b-row>
6271
<b-modal
@@ -176,9 +185,11 @@
176185
<script>
177186
import { required } from 'vuelidate/lib/validators';
178187
import { mapGetters, mapActions } from 'vuex';
188+
import CharacterLineStats from '@/vue_components/show/config/characters/CharacterLineStats.vue';
179189
180190
export default {
181191
name: 'ConfigCharacters',
192+
components: { CharacterLineStats },
182193
data() {
183194
return {
184195
rowsPerPage: 15,
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
<template>
2+
<b-container
3+
class="mx-0 px-0"
4+
fluid
5+
>
6+
<b-row>
7+
<b-col>
8+
<div
9+
v-if="!loaded"
10+
class="text-center center-spinner"
11+
>
12+
<b-spinner
13+
style="width: 10rem; height: 10rem;"
14+
variant="info"
15+
/>
16+
</div>
17+
<template v-else-if="sortedScenes.length > 0">
18+
<b-table
19+
:items="tableData"
20+
:fields="tableFields"
21+
responsive
22+
show-empty
23+
sticky-header="65vh"
24+
>
25+
<template #thead-top="data">
26+
<b-tr>
27+
<b-th colspan="1">
28+
<span class="sr-only">Character</span>
29+
</b-th>
30+
<template v-for="act in sortedActs">
31+
<b-th
32+
v-if="numScenesPerAct(act.id) > 0"
33+
:key="act.id"
34+
variant="primary"
35+
:colspan="numScenesPerAct(act.id)"
36+
class="act-header"
37+
>
38+
{{ act.name }}
39+
</b-th>
40+
</template>
41+
</b-tr>
42+
</template>
43+
<template
44+
v-for="scene in sortedScenes"
45+
#[getHeaderName(scene.id)]="data"
46+
>
47+
{{ scene.name }}
48+
</template>
49+
<template #cell(Character)="data">
50+
{{ CHARACTER_BY_ID(data.item.Character).name }}
51+
</template>
52+
<template
53+
v-for="scene in sortedScenes"
54+
#[getCellName(scene.id)]="data"
55+
>
56+
<template
57+
v-if="getLineCountForCharacter(data.item.Character, scene.act, scene.id) > 0"
58+
>
59+
{{ getLineCountForCharacter(data.item.Character, scene.act, scene.id) }}
60+
</template>
61+
</template>
62+
</b-table>
63+
</template>
64+
<b v-else>Unable to get mic allocations. Ensure act and scene ordering is set.</b>
65+
</b-col>
66+
</b-row>
67+
</b-container>
68+
</template>
69+
70+
<script>
71+
import { mapActions, mapGetters } from 'vuex';
72+
import { makeURL } from '@/js/utils';
73+
import log from 'loglevel';
74+
75+
export default {
76+
name: 'CharacterLineStats',
77+
data() {
78+
return {
79+
loaded: false,
80+
characterStats: {},
81+
};
82+
},
83+
async mounted() {
84+
await this.GET_ACT_LIST();
85+
await this.GET_SCENE_LIST();
86+
await this.getCharacterStats();
87+
this.loaded = true;
88+
},
89+
methods: {
90+
numScenesPerAct(actId) {
91+
return this.sortedScenes.filter((scene) => scene.act === actId).length;
92+
},
93+
getHeaderName(sceneId) {
94+
return `head(${sceneId})`;
95+
},
96+
getCellName(sceneId) {
97+
return `cell(${sceneId})`;
98+
},
99+
async getCharacterStats() {
100+
const response = await fetch(`${makeURL('/api/v1/show/character/stats')}`);
101+
if (response.ok) {
102+
this.characterStats = await response.json();
103+
} else {
104+
log.error('Unable to get character stats!');
105+
}
106+
},
107+
getLineCountForCharacter(characterId, actId, sceneId) {
108+
if (!Object.keys(this.characterStats).includes('line_counts')) {
109+
return 0;
110+
}
111+
const lineCounts = this.characterStats.line_counts;
112+
if (Object.keys(lineCounts).includes(characterId.toString())) {
113+
const characterCounts = this.characterStats.line_counts[characterId];
114+
if (Object.keys(characterCounts).includes(actId.toString())) {
115+
const actCounts = characterCounts[actId];
116+
if (Object.keys(actCounts).includes(sceneId.toString())) {
117+
return actCounts[sceneId];
118+
}
119+
}
120+
}
121+
return 0;
122+
},
123+
...mapActions(['GET_ACT_LIST', 'GET_SCENE_LIST']),
124+
},
125+
computed: {
126+
tableData() {
127+
if (!this.loaded) {
128+
return [];
129+
}
130+
return this.CHARACTER_LIST.map((character) => ({
131+
Character: character.id,
132+
}), this);
133+
},
134+
tableFields() {
135+
return ['Character', ...this.sortedScenes.map((scene) => (scene.id.toString()))];
136+
},
137+
sortedActs() {
138+
if (this.CURRENT_SHOW.first_act_id == null) {
139+
return [];
140+
}
141+
let currentAct = this.ACT_BY_ID(this.CURRENT_SHOW.first_act_id);
142+
if (currentAct == null) {
143+
return [];
144+
}
145+
const acts = [];
146+
while (currentAct != null) {
147+
acts.push(currentAct);
148+
currentAct = this.ACT_BY_ID(currentAct.next_act);
149+
}
150+
return acts;
151+
},
152+
sortedScenes() {
153+
if (this.CURRENT_SHOW.first_act_id == null) {
154+
return [];
155+
}
156+
157+
let currentAct = this.ACT_BY_ID(this.CURRENT_SHOW.first_act_id);
158+
if (currentAct == null || currentAct.first_scene == null) {
159+
return [];
160+
}
161+
162+
const scenes = [];
163+
while (currentAct != null) {
164+
let currentScene = this.SCENE_BY_ID(currentAct.first_scene);
165+
while (currentScene != null) {
166+
scenes.push(currentScene);
167+
currentScene = this.SCENE_BY_ID(currentScene.next_scene);
168+
}
169+
currentAct = this.ACT_BY_ID(currentAct.next_act);
170+
}
171+
return scenes;
172+
},
173+
...mapGetters(['ACT_BY_ID', 'SCENE_BY_ID', 'CURRENT_SHOW', 'CHARACTER_BY_ID', 'CHARACTER_LIST']),
174+
},
175+
};
176+
</script>

server/controllers/api/show/characters.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from collections import defaultdict
2+
13
from tornado import escape
24

5+
from models.script import Script, ScriptRevision, ScriptLine
36
from models.show import Show, Cast, Character, CharacterGroup
47
from rbac.role import Role
58
from schemas.schemas import CharacterSchema, CharacterGroupSchema
@@ -154,6 +157,46 @@ async def delete(self):
154157
await self.finish({'message': '404 show not found'})
155158

156159

160+
@ApiRoute('show/character/stats', ApiVersion.V1)
161+
class CharacterStatsController(BaseAPIController):
162+
async def get(self):
163+
current_show = self.get_current_show()
164+
show_id = current_show['id']
165+
166+
with self.make_session() as session:
167+
show: Show = session.query(Show).get(show_id)
168+
if show:
169+
script: Script = session.query(Script).filter(Script.show_id == show.id).first()
170+
171+
if script.current_revision:
172+
revision: ScriptRevision = session.query(ScriptRevision).get(
173+
script.current_revision)
174+
else:
175+
self.set_status(400)
176+
await self.finish({'message': 'Script does not have a current revision'})
177+
return
178+
179+
line_counts = defaultdict(lambda: defaultdict(lambda: defaultdict(int)))
180+
for line_association in revision.line_associations:
181+
line: ScriptLine = line_association.line
182+
if line.stage_direction:
183+
continue
184+
for line_part in line.line_parts:
185+
if line_part.line_part_cuts is not None:
186+
continue
187+
if line_part.character_id:
188+
line_counts[line_part.character_id][line.act_id][line.scene_id] += 1
189+
elif line_part.character_group_id:
190+
for character in line_part.character_group.characters:
191+
line_counts[character.id][line.act_id][line.scene_id] += 1
192+
193+
self.set_status(200)
194+
await self.finish({'line_counts': line_counts})
195+
else:
196+
self.set_status(404)
197+
await self.finish({'message': '404 show not found'})
198+
199+
157200
@ApiRoute('show/character/group', ApiVersion.V1)
158201
class CharacterGroupController(BaseAPIController):
159202

0 commit comments

Comments
 (0)