698 lines
28 KiB
Svelte
698 lines
28 KiB
Svelte
<script>
|
|
import { user } from '$lib/stores/user';
|
|
import CorrectionPieChart from '$lib/components/CorrectionPieChart.svelte';
|
|
import ListInputResponses from '$lib/components/ListInputResponses.svelte';
|
|
import QuestionForm from '$lib/components/QuestionForm.svelte';
|
|
import StartStopLiveSurvey from '$lib/components/StartStopLiveSurvey.svelte';
|
|
import SurveyAdmin from '$lib/components/SurveyAdmin.svelte';
|
|
import SurveyBadge from '$lib/components/SurveyBadge.svelte';
|
|
import { getSurvey } from '$lib/surveys';
|
|
import { getQuestion, getQuestions, Question } from '$lib/questions';
|
|
import { getUsers } from '$lib/users';
|
|
|
|
export let data;
|
|
let survey;
|
|
let req_questions;
|
|
|
|
$: {
|
|
survey = data.survey;
|
|
updateQuestions();
|
|
if (survey.direct !== null) {
|
|
wsconnect();
|
|
}
|
|
}
|
|
|
|
async function updateSurvey() {
|
|
survey = await getSurvey(survey.id);
|
|
updateQuestions();
|
|
if (survey.direct !== null) {
|
|
wsconnect();
|
|
}
|
|
}
|
|
|
|
function updateQuestions() {
|
|
req_questions = getQuestions(survey.id);
|
|
}
|
|
|
|
function deleteQuestion(question) {
|
|
edit_question = null;
|
|
question.delete();
|
|
}
|
|
|
|
let ws = null;
|
|
let ws_up = false;
|
|
let wsstats = null;
|
|
let current_question = null;
|
|
let edit_question = null;
|
|
let responses = {};
|
|
let corrected = false;
|
|
let next_corrected = false;
|
|
let with_stats = false;
|
|
let timer = 20;
|
|
let timer_end = null;
|
|
let timer_remain = 0;
|
|
let timer_cancel = null;
|
|
|
|
function updTimer() {
|
|
const now = new Date().getTime();
|
|
if (now > timer_end) {
|
|
timer_remain = 0;
|
|
clearInterval(timer_cancel);
|
|
timer_cancel = null;
|
|
} else {
|
|
timer_remain = Math.floor((timer_end - now) / 100)/10;
|
|
}
|
|
}
|
|
|
|
let users = {};
|
|
function updateUsers() {
|
|
getUsers().then((usr) => {
|
|
const tmp = { };
|
|
for (const u of usr) {
|
|
tmp[u.id.toString()] = u;
|
|
}
|
|
users = tmp;
|
|
});
|
|
}
|
|
updateUsers();
|
|
|
|
let scroll_states = { };
|
|
let scroll_mean = 0;
|
|
$: {
|
|
let mean = 0;
|
|
for (const k in scroll_states) {
|
|
mean += scroll_states[k];
|
|
}
|
|
scroll_mean = mean / Object.keys(scroll_states).length;
|
|
}
|
|
|
|
let responsesbyid = { };
|
|
$: {
|
|
const tmp = { };
|
|
for (const response in responses) {
|
|
if (!tmp[response]) tmp[response] = [];
|
|
for (const r in responses[response]) {
|
|
tmp[response].push(responses[response][r]);
|
|
}
|
|
}
|
|
responsesbyid = tmp;
|
|
}
|
|
|
|
let graph_data = {labels:[], datasets:[]};
|
|
async function reset_graph_data(questionid) {
|
|
if (questionid) {
|
|
const labels = [];
|
|
const flabels = [];
|
|
|
|
let question = null;
|
|
for (const q of await req_questions) {
|
|
if (q.id == current_question) {
|
|
question = q;
|
|
}
|
|
}
|
|
|
|
if (question) {
|
|
for (const p of await question.getProposals()) {
|
|
flabels.push(p.id.toString());
|
|
labels.push(p.label);
|
|
}
|
|
}
|
|
|
|
graph_data = {
|
|
labels,
|
|
flabels,
|
|
datasets: [
|
|
{
|
|
values: labels.map(() => 0)
|
|
}
|
|
]
|
|
}
|
|
}
|
|
|
|
if (current_question && responses[current_question] && graph_data.labels.length != 0) {
|
|
const values = graph_data.datasets[0].values.map(() => 0);
|
|
|
|
for (const u in responses[current_question]) {
|
|
const res = responses[current_question][u];
|
|
for (const r of res.split(',')) {
|
|
let idx = graph_data.flabels.indexOf(r);
|
|
values[idx] += 1;
|
|
}
|
|
}
|
|
|
|
graph_data.datasets[0].values = values;
|
|
}
|
|
}
|
|
|
|
let asks = [];
|
|
function wsconnect() {
|
|
if (ws !== null) return;
|
|
|
|
ws = new WebSocket((window.location.protocol == 'https:'?'wss://':'ws://') + window.location.host + `/api/surveys/${data.sid}/ws-admin`);
|
|
|
|
ws.addEventListener("open", () => {
|
|
ws_up = true;
|
|
ws.send('{"action":"get_responses"}');
|
|
ws.send('{"action":"get_stats"}');
|
|
ws.send('{"action":"get_asks"}');
|
|
});
|
|
|
|
ws.addEventListener("close", (e) => {
|
|
ws_up = false;
|
|
console.log('Socket is closed. Reconnect will be attempted in 1 second.');
|
|
setTimeout(function() {
|
|
ws = null;
|
|
updateSurvey();
|
|
}, 1500);
|
|
});
|
|
|
|
ws.addEventListener("error", (err) => {
|
|
ws_up = false;
|
|
console.log('Socket closed due to error.', err);
|
|
});
|
|
|
|
ws.addEventListener("message", (message) => {
|
|
const data = JSON.parse(message.data);
|
|
if (data.action && data.action == "new_question") {
|
|
current_question = data.question;
|
|
corrected = data.corrected == true;
|
|
if (timer_cancel) {
|
|
clearInterval(timer_cancel);
|
|
timer_cancel = null;
|
|
}
|
|
if (data.timer) {
|
|
timer_end = new Date().getTime() + data.timer;
|
|
timer_cancel = setInterval(updTimer, 250);
|
|
} else {
|
|
timer_end = null;
|
|
}
|
|
reset_graph_data(data.question);
|
|
} else if (data.action && data.action == "stats") {
|
|
wsstats = data.stats;
|
|
} else if (data.action && data.action == "new_response") {
|
|
if (!responses[data.question]) responses[data.question] = { };
|
|
responses[data.question][data.user] = data.value;
|
|
|
|
reset_graph_data();
|
|
} else if (data.action && data.action == "new_ask") {
|
|
asks.push({"id": data.question, "content": data.value, "userid": data.user});
|
|
asks = asks;
|
|
} else if (data.action && data.action == "myscroll" && wsstats && wsstats.users) {
|
|
scroll_states[data.user] = parseFloat(data.value);
|
|
for (const k in wsstats.users) {
|
|
if (wsstats.users[k].id == data.user) {
|
|
wsstats.users[k].myscroll = scroll_states[data.user];
|
|
}
|
|
}
|
|
} else if (data.action && data.action == "end") {
|
|
ws.close();
|
|
updateSurvey();
|
|
} else {
|
|
current_question = null;
|
|
timer_end = null;
|
|
if (timer_cancel) {
|
|
clearInterval(timer_cancel);
|
|
timer_cancel = null;
|
|
}
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
|
|
{#if $user && $user.is_admin}
|
|
<StartStopLiveSurvey
|
|
{survey}
|
|
class="ms-1 float-end"
|
|
on:update={() => updateSurvey()}
|
|
on:end={() => { if (confirm("Sûr ?")) ws.send('{"action":"end"}') }}
|
|
/>
|
|
<a href="surveys/{survey.id}/responses" class="btn btn-success ms-1 float-end" title="Voir les réponses"><i class="bi bi-files"></i></a>
|
|
{/if}
|
|
<div class="d-flex align-items-center">
|
|
<h2>
|
|
<a href="surveys/" class="text-muted" style="text-decoration: none"><</a>
|
|
{survey.title}
|
|
<small class="text-muted">
|
|
Administration
|
|
</small>
|
|
{#if asks.length}
|
|
<a href="surveys/{data.sid}/admin#questions_part">
|
|
<i class="bi bi-patch-question-fill text-danger"></i>
|
|
</a>
|
|
{/if}
|
|
</h2>
|
|
{#if survey.direct !== null}
|
|
<div
|
|
class="badge rounded-pill ms-2"
|
|
class:bg-success={ws_up}
|
|
class:bg-danger={!ws_up}
|
|
>
|
|
{#if ws_up}Connecté{:else}Déconnecté{/if}
|
|
</div>
|
|
{:else}
|
|
<SurveyBadge
|
|
class="mx-2"
|
|
{survey}
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
{#if survey.direct === null}
|
|
<SurveyAdmin
|
|
{survey}
|
|
on:saved={updateSurvey}
|
|
/>
|
|
{:else}
|
|
{#await req_questions}
|
|
<div class="text-center">
|
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
|
<span>Chargement des questions …</span>
|
|
</div>
|
|
{:then questions}
|
|
<div class="card my-3">
|
|
<table class="table table-hover table-striped mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th>
|
|
Question
|
|
{#if timer_end}
|
|
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
disabled
|
|
value={timer_remain}
|
|
>
|
|
<span class="input-group-text">s</span>
|
|
</div>
|
|
{:else}
|
|
<div class="input-group input-group-sm float-end" style="max-width: 150px;">
|
|
<input
|
|
type="number"
|
|
class="form-control"
|
|
bind:value={timer}
|
|
placeholder="Valeur du timer"
|
|
>
|
|
<span class="input-group-text">s</span>
|
|
</div>
|
|
{/if}
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-info ms-1"
|
|
on:click={updateQuestions}
|
|
title="Rafraîchir les questions"
|
|
>
|
|
<i class="bi bi-arrow-counterclockwise"></i>
|
|
</button>
|
|
</th>
|
|
<th>
|
|
Réponses
|
|
</th>
|
|
<th>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-primary"
|
|
disabled={!current_question || !ws_up}
|
|
on:click={() => { ws.send('{"action":"pause"}')} }
|
|
title="Passer sur une scène sans question"
|
|
>
|
|
<i class="bi bi-pause-fill"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
class:btn-outline-success={!next_corrected}
|
|
class:btn-success={next_corrected}
|
|
on:click={() => { next_corrected = !next_corrected } }
|
|
title="La prochaine question est affichée corrigée"
|
|
>
|
|
<i class="bi bi-eye"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
class:btn-outline-success={!with_stats}
|
|
class:btn-success={with_stats}
|
|
on:click={() => { with_stats = !with_stats } }
|
|
title="La prochaine correction sera affichée avec les statistiques"
|
|
>
|
|
<i class="bi bi-bar-chart-fill"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-outline-danger"
|
|
on:click={() => { fetch('api/cache', {method: 'DELETE'}) } }
|
|
title="Vider les caches"
|
|
>
|
|
<i class="bi bi-bandaid-fill"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-info mt-1"
|
|
on:click={() => { edit_question = new Question({ id_survey: survey.id }) } }
|
|
title="Ajouter une question"
|
|
>
|
|
<i class="bi bi-plus"></i>
|
|
</button>
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{#each questions as question (question.id)}
|
|
<tr>
|
|
<td>
|
|
{#if responses[question.id]}
|
|
<a href="surveys/{data.sid}/admin#q{question.id}_res">
|
|
{question.title}
|
|
</a>
|
|
{:else}
|
|
{question.title}
|
|
{/if}
|
|
</td>
|
|
<td>
|
|
{#if responses[question.id]}
|
|
{Object.keys(responses[question.id]).length}
|
|
{:else}
|
|
0
|
|
{/if}
|
|
{#if wsstats}/ {wsstats.nb_clients}{/if}
|
|
</td>
|
|
<td>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm"
|
|
class:btn-primary={!next_corrected}
|
|
class:btn-success={next_corrected}
|
|
disabled={(question.id === current_question && next_corrected == corrected) || !ws_up}
|
|
on:click={() => { ws.send('{"action":"new_question", "corrected": ' + next_corrected + (with_stats?', "stats": {}':'') + ', "timer": 0, "question":' + question.id + '}')} }
|
|
>
|
|
<i class="bi bi-play-fill"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-danger"
|
|
disabled={question.id === current_question || !ws_up}
|
|
on:click={() => { ws.send('{"action":"new_question", "corrected": ' + next_corrected + (with_stats?', "stats": {}':'') + ', "timer": ' + timer * 1000 + ',"question":' + question.id + '}')} }
|
|
>
|
|
<i class="bi bi-stopwatch-fill"></i>
|
|
</button>
|
|
<a
|
|
href="/surveys/{survey.id}/responses/{question.id}"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
type="button"
|
|
class="btn btn-sm btn-success"
|
|
>
|
|
<i class="bi bi-files"></i>
|
|
</a>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-info"
|
|
disabled={question.id === current_question}
|
|
on:click={() => { getQuestion(question.id).then((q) => {edit_question = q})} }
|
|
>
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/await}
|
|
{#if edit_question !== null}
|
|
<QuestionForm
|
|
{survey}
|
|
edit
|
|
question={edit_question}
|
|
on:delete={() => deleteQuestion(edit_question)}
|
|
/>
|
|
{/if}
|
|
|
|
<hr>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-info ms-1 float-end"
|
|
on:click={() => { ws.send('{"action":"get_asks", "value": ""}'); asks = []; }}
|
|
title="Rafraîchir les réponses"
|
|
>
|
|
<i class="bi bi-arrow-counterclockwise"></i>
|
|
<i class="bi bi-question-diamond"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-light ms-1 float-end"
|
|
on:click={() => { ws.send('{"action":"get_asks", "value": "unanswered"}'); asks = []; }}
|
|
title="Rafraîchir les réponses, en rapportant les réponses déjà répondues"
|
|
>
|
|
<i class="bi bi-arrow-counterclockwise"></i>
|
|
<i class="bi bi-question-diamond"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-success float-end"
|
|
title="Tout marqué comme répondu"
|
|
on:click={() => { ws.send('{"action":"mark_answered", "value": "all"}'); asks = [] }}
|
|
>
|
|
<i class="bi bi-check-all"></i>
|
|
</button>
|
|
<h3 id="questions_part">
|
|
Questions
|
|
{#if asks.length}
|
|
<small class="text-muted">
|
|
{asks.length} question{#if asks.length > 1}s{/if}
|
|
</small>
|
|
{/if}
|
|
</h3>
|
|
{#if asks.length}
|
|
{#each asks as ask (ask.id)}
|
|
<div class="card mb-3">
|
|
<div class="card-body">
|
|
<p class="card-text">
|
|
{ask.content}
|
|
</p>
|
|
</div>
|
|
<div class="card-footer">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-success float-end"
|
|
title="Marqué comme répondu"
|
|
on:click={() => { ws.send('{"action":"mark_answered", "question": ' + ask.id + '}'); asks = asks.filter((e) => e.id != ask.id) }}
|
|
>
|
|
<i class="bi bi-check"></i>
|
|
</button>
|
|
Par
|
|
<a href="users/{ask.userid}" target="_blank" rel="noreferrer">
|
|
{#if users && users[ask.userid]}
|
|
{users[ask.userid].login}
|
|
{:else}
|
|
{ask.userid}
|
|
{/if}
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
{:else}
|
|
<div class="text-center text-muted">
|
|
Pas de question pour l'instant.
|
|
</div>
|
|
{/if}
|
|
|
|
<hr>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-info ms-1 float-end"
|
|
on:click={() => { ws.send('{"action":"get_responses"}') }}
|
|
title="Rafraîchir les réponses"
|
|
>
|
|
<i class="bi bi-arrow-counterclockwise"></i>
|
|
<i class="bi bi-card-checklist"></i>
|
|
</button>
|
|
<h3>
|
|
Réponses
|
|
</h3>
|
|
{#if Object.keys(responses).length}
|
|
{#each Object.keys(responses) as q, qid (qid)}
|
|
{#await req_questions then questions}
|
|
{#each questions as question}
|
|
{#if question.id == q}
|
|
<h4 id="q{question.id}_res">
|
|
{question.title}
|
|
</h4>
|
|
{#if question.kind == 'ucq'}
|
|
{#await question.getProposals()}
|
|
<div class="text-center">
|
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
|
<span>Chargement des propositions …</span>
|
|
</div>
|
|
{:then proposals}
|
|
{#if current_question == question.id}
|
|
<CorrectionPieChart
|
|
{question}
|
|
{proposals}
|
|
data={graph_data}
|
|
/>
|
|
{:else}
|
|
<CorrectionPieChart
|
|
{question}
|
|
/>
|
|
{/if}
|
|
<div class="card mb-4">
|
|
<table class="table table-sm table-striped table-hover mb-0">
|
|
<tbody>
|
|
{#each proposals as proposal (proposal.id)}
|
|
<tr>
|
|
<td>
|
|
{proposal.label}
|
|
</td>
|
|
<td>
|
|
{responsesbyid[q].filter((e) => e == proposal.id.toString()).length}/{responsesbyid[q].length}
|
|
</td>
|
|
<td>
|
|
{Math.trunc(responsesbyid[q].filter((e) => e == proposal.id.toString()).length / responsesbyid[q].length * 1000)/10} %
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/await}
|
|
{:else if question.kind == 'mcq'}
|
|
{#await question.getProposals()}
|
|
<div class="text-center">
|
|
<div class="spinner-border text-primary mx-3" role="status"></div>
|
|
<span>Chargement des propositions …</span>
|
|
</div>
|
|
{:then proposals}
|
|
{#if current_question == question.id}
|
|
<CorrectionPieChart
|
|
{question}
|
|
{proposals}
|
|
data={graph_data}
|
|
/>
|
|
{:else}
|
|
<CorrectionPieChart
|
|
{question}
|
|
/>
|
|
{/if}
|
|
<div class="card mb-4">
|
|
<table class="table table-sm table-striped table-hover mb-0">
|
|
<tbody>
|
|
{#each proposals as proposal (proposal.id)}
|
|
<tr>
|
|
<td>
|
|
{proposal.label}
|
|
</td>
|
|
<td>
|
|
{responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length}/{responsesbyid[q].length}
|
|
</td>
|
|
<td>
|
|
{Math.trunc(responsesbyid[q].filter((e) => e.indexOf(proposal.id.toString()) >= 0).length / responsesbyid[q].length * 1000)/10} %
|
|
</td>
|
|
</tr>
|
|
{/each}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{/await}
|
|
{:else if question.kind && question.kind.startsWith('list')}
|
|
<ListInputResponses
|
|
responses={responses[q]}
|
|
{users}
|
|
/>
|
|
{:else}
|
|
<div class="card mb-4">
|
|
<ul class="list-group list-group-flush">
|
|
{#each Object.keys(responses[q]) as user, rid (rid)}
|
|
<li class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
|
|
<span>
|
|
{responses[q][user]}
|
|
</span>
|
|
<a href="users/{user}" target="_blank" rel="noreferrer" class="badge bg-dark rounded-pill">
|
|
{#if users && users[user]}
|
|
{users[user].login}
|
|
{:else}
|
|
{user}
|
|
{/if}
|
|
</a>
|
|
</li>
|
|
{/each}
|
|
</ul>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
{/each}
|
|
{/await}
|
|
{/each}
|
|
{/if}
|
|
|
|
<hr>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-info ms-1 float-end"
|
|
on:click={() => { ws.send('{"action":"get_stats"}') }}
|
|
title="Rafraîchir les stats"
|
|
>
|
|
<i class="bi bi-arrow-counterclockwise"></i>
|
|
<i class="bi bi-123"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-primary ms-1 float-end"
|
|
title="Rafraîchir la liste des utilisateurs"
|
|
on:click={updateUsers}
|
|
>
|
|
<i class="bi bi-arrow-clockwise"></i>
|
|
<i class="bi bi-people"></i>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-warning ms-1 float-end"
|
|
on:click={() => { scroll_states = {}; ws.send('{"action":"where_are_you"}')} }
|
|
title="Rapporter l'avancement"
|
|
>
|
|
<i class="bi bi-geo-fill"></i>
|
|
</button>
|
|
<h3 id="users">
|
|
Connectés
|
|
{#if wsstats}
|
|
<small class="text-muted">{wsstats.nb_clients} utilisateurs</small>
|
|
{/if}
|
|
{#if scroll_mean}
|
|
<small
|
|
class:text-danger={scroll_mean >= 0 && scroll_mean < 0.2}
|
|
class:text-warning={scroll_mean >= 0.2 && scroll_mean < 0.6}
|
|
class:text-info={scroll_mean >= 0.6 && scroll_mean < 0.9}
|
|
class:text-success={scroll_mean >= 0.9}
|
|
>Avancement global : {Math.trunc(scroll_mean*10000)/100} %</small>
|
|
{/if}
|
|
</h3>
|
|
{#if wsstats && wsstats.users}
|
|
<div class="row row-cols-5 py-3">
|
|
{#each wsstats.users as user, lid (lid)}
|
|
<div class="col">
|
|
<div class="card">
|
|
<img alt="{user.login}" src="//photos.cri.epita.fr/thumb/{user.login}" class="card-img-top">
|
|
<div class="card-footer text-center text-truncate p-0">
|
|
<a href="users/{user.login}" target="_blank" rel="noreferrer">
|
|
{user.login}
|
|
</a>
|
|
</div>
|
|
{#if user.myscroll != null}
|
|
<div
|
|
class="card-footer py-0 px-1"
|
|
class:bg-danger={user.myscroll >= 0 && user.myscroll < 0.2}
|
|
class:bg-warning={user.myscroll >= 0.2 && user.myscroll < 0.6}
|
|
class:bg-info={user.myscroll >= 0.6 && user.myscroll < 0.9}
|
|
class:bg-success={user.myscroll >= 0.9}
|
|
>
|
|
Avancement : {Math.trunc(user.myscroll*10000)/100} %
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/if}
|