Merge branch 'main' of gitschool:ThomasRubini/SAE-A2-TruthInquiry
@ -35,11 +35,27 @@ def npc(npc_id):
|
|||||||
answer_list = [answer.TEXT for answer in answer_type.TEXT_LOCALE.TEXTS]
|
answer_list = [answer.TEXT for answer in answer_type.TEXT_LOCALE.TEXTS]
|
||||||
npc_answers.append(answer_list)
|
npc_answers.append(answer_list)
|
||||||
|
|
||||||
|
reactions = [{
|
||||||
|
"id": reaction.TRAIT.TRAIT_ID,
|
||||||
|
"name": reaction.TRAIT.NAME_LOCALE.get_text(DEFAULT_LANG).TEXT,
|
||||||
|
"url": "/api/v1/getReaction?uuid="+reaction.REACTION_UUID
|
||||||
|
} for reaction in npc_obj.REACTIONS]
|
||||||
|
|
||||||
|
reactions_to_add = []
|
||||||
|
for trait in db.session.query(Trait).all():
|
||||||
|
if trait.TRAIT_ID not in [reaction.TRAIT.TRAIT_ID for reaction in npc_obj.REACTIONS]:
|
||||||
|
reactions_to_add.append({
|
||||||
|
"id": trait.TRAIT_ID,
|
||||||
|
"name": trait.NAME_LOCALE.get_text(DEFAULT_LANG).TEXT
|
||||||
|
})
|
||||||
|
|
||||||
npc_dict = {
|
npc_dict = {
|
||||||
"id": npc_obj.NPC_ID,
|
"id": npc_obj.NPC_ID,
|
||||||
"name": npc_obj.NAME_LOCALE.get_text(DEFAULT_LANG).TEXT,
|
"name": npc_obj.NAME_LOCALE.get_text(DEFAULT_LANG).TEXT,
|
||||||
"img": npc_obj.NPC_ID,
|
"img": npc_obj.NPC_ID,
|
||||||
"answers": npc_answers,
|
"answers": npc_answers,
|
||||||
|
"reactions": reactions,
|
||||||
|
"reactions_to_add": reactions_to_add,
|
||||||
}
|
}
|
||||||
|
|
||||||
return flask.render_template("admin/npc.html", npc=npc_dict)
|
return flask.render_template("admin/npc.html", npc=npc_dict)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
import flask
|
import flask
|
||||||
from sqlalchemy import select, delete, or_
|
from sqlalchemy import select, delete, and_
|
||||||
|
|
||||||
from truthinquiry.ext.database.models import *
|
from truthinquiry.ext.database.models import *
|
||||||
from truthinquiry.ext.database.fsa import db
|
from truthinquiry.ext.database.fsa import db
|
||||||
@ -176,3 +176,36 @@ def delete_npc():
|
|||||||
db.session.execute(delete(Npc).where(Npc.NPC_ID==input_npc_id))
|
db.session.execute(delete(Npc).where(Npc.NPC_ID==input_npc_id))
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
@routes_api_admin.route("/setReaction", methods=["GET", "POST"])
|
||||||
|
@require_admin(api=True)
|
||||||
|
def setReaction():
|
||||||
|
input_npc_id = flask.request.values["npc_id"]
|
||||||
|
input_trait_id = flask.request.values["trait_id"]
|
||||||
|
|
||||||
|
row = db.session.execute(
|
||||||
|
select(Reaction)
|
||||||
|
.where(and_(
|
||||||
|
Reaction.NPC_ID==input_npc_id,
|
||||||
|
Reaction.TRAIT_ID==input_trait_id
|
||||||
|
))
|
||||||
|
).first()
|
||||||
|
|
||||||
|
reaction = None if row == None else row[0]
|
||||||
|
|
||||||
|
|
||||||
|
if len(flask.request.files) == 0: # want to delete
|
||||||
|
if reaction:
|
||||||
|
db.session.delete(reaction)
|
||||||
|
else:
|
||||||
|
return {"msg": "No such reaction"} # Not an error because this can be intentional
|
||||||
|
else:
|
||||||
|
input_reaction_file = flask.request.files['file']
|
||||||
|
if not reaction:
|
||||||
|
reaction = Reaction(None, input_npc_id, input_trait_id)
|
||||||
|
db.session.add(reaction)
|
||||||
|
reaction.IMG = input_reaction_file.read()
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
return {}
|
||||||
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 998 KiB After Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 331 KiB After Width: | Height: | Size: 194 KiB |
Before Width: | Height: | Size: 478 KiB After Width: | Height: | Size: 269 KiB |
Before Width: | Height: | Size: 500 KiB After Width: | Height: | Size: 276 KiB |
Before Width: | Height: | Size: 1.0 MiB After Width: | Height: | Size: 349 KiB |
@ -1,4 +1,7 @@
|
|||||||
function createOrUpdateNpc() {
|
|
||||||
|
const reactionsDelta = {}
|
||||||
|
|
||||||
|
async function createOrUpdateNpc() {
|
||||||
const data = {};
|
const data = {};
|
||||||
data["id"] = document.querySelector("#npc_id").value;
|
data["id"] = document.querySelector("#npc_id").value;
|
||||||
data["name"] = document.querySelector("#npc_name").value;
|
data["name"] = document.querySelector("#npc_name").value;
|
||||||
@ -16,9 +19,36 @@ function createOrUpdateNpc() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
makeAPIRequest("admin/setNpc", {"npc": data, "lang": "FR"}, {"content": "json"}).then(() => {
|
await makeAPIRequest("admin/setNpc", {"npc": data, "lang": "FR"}, {"content": "json"});
|
||||||
|
|
||||||
|
await uploadReactionsDelta();
|
||||||
|
|
||||||
alert("Opération effectuée avec succès");
|
alert("Opération effectuée avec succès");
|
||||||
});
|
}
|
||||||
|
|
||||||
|
async function uploadReactionsDelta() {
|
||||||
|
let requests = [];
|
||||||
|
|
||||||
|
|
||||||
|
for(const [traitId, reactionNode] of Object.entries(reactionsDelta)){
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("npc_id", npc_id.value);
|
||||||
|
formData.append("trait_id", traitId);
|
||||||
|
|
||||||
|
if(reactionNode === null) formData.append("file", "null");
|
||||||
|
else{
|
||||||
|
const file = reactionNode.querySelector(".img_input").files[0]
|
||||||
|
formData.append("file", file ? file : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
requests.push(makeAPIRequest("admin/setReaction", formData, {"content": "form"}));
|
||||||
|
}
|
||||||
|
|
||||||
|
for(request of requests){
|
||||||
|
await request;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteNpc() {
|
async function deleteNpc() {
|
||||||
@ -31,3 +61,52 @@ async function deleteNpc() {
|
|||||||
alert("Opération effectuée avec succès");
|
alert("Opération effectuée avec succès");
|
||||||
document.location = "/admin";
|
document.location = "/admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function changeReaction(inputNode){
|
||||||
|
const parentNode = inputNode.parentNode;
|
||||||
|
const imgNode = parentNode.querySelector('img');
|
||||||
|
const traitId = parentNode.querySelector('.trait_id').value;
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e)=>{
|
||||||
|
imgNode.src = e.target.result
|
||||||
|
}
|
||||||
|
reader.readAsDataURL(inputNode.files[0]);
|
||||||
|
|
||||||
|
reactionsDelta[traitId] = parentNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteReaction(node){
|
||||||
|
const reactionNode = node.parentNode;
|
||||||
|
const traitId = reactionNode.querySelector(".trait_id").value;
|
||||||
|
const reactionName = reactionNode.querySelector("p").innerText;
|
||||||
|
|
||||||
|
reactionNode.parentNode.removeChild(reactionNode);
|
||||||
|
|
||||||
|
const option = document.createElement("option");
|
||||||
|
option.value = traitId
|
||||||
|
option.innerText = reactionName
|
||||||
|
|
||||||
|
reactions_to_add.appendChild(option);
|
||||||
|
|
||||||
|
reactionsDelta[traitId] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addReaction(selectNode){
|
||||||
|
const selectedOptionNode = selectNode.selectedOptions[0];
|
||||||
|
|
||||||
|
const traitId = selectedOptionNode.value;
|
||||||
|
const reactionName = selectedOptionNode.innerText;
|
||||||
|
|
||||||
|
selectNode.removeChild(selectedOptionNode);
|
||||||
|
|
||||||
|
const newReaction = reactions.querySelector("div").cloneNode(true);
|
||||||
|
newReaction.querySelector("img").src = "";
|
||||||
|
newReaction.querySelector(".img_input").value = null;
|
||||||
|
newReaction.querySelector(".trait_id").value = traitId
|
||||||
|
newReaction.querySelector("p").innerText = reactionName
|
||||||
|
|
||||||
|
reactions.appendChild(newReaction);
|
||||||
|
|
||||||
|
reactionsDelta[traitId] = newReaction;
|
||||||
|
}
|
@ -17,6 +17,8 @@ async function makeAPIRequest(endpoint, body, options={}) {
|
|||||||
if (options["content"] === 'json') {
|
if (options["content"] === 'json') {
|
||||||
fetchOptions["headers"]["Content-Type"] = 'application/json'
|
fetchOptions["headers"]["Content-Type"] = 'application/json'
|
||||||
fetchOptions["body"] = JSON.stringify(body)
|
fetchOptions["body"] = JSON.stringify(body)
|
||||||
|
} else if (options["content"] === 'form') {
|
||||||
|
fetchOptions["body"] = body;
|
||||||
} else {
|
} else {
|
||||||
fetchOptions["body"] = new URLSearchParams(body);
|
fetchOptions["body"] = new URLSearchParams(body);
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,28 @@
|
|||||||
<p class="info_item">Image du personnage</p>
|
<p class="info_item">Image du personnage</p>
|
||||||
<img class="character_image" alt="{{'Image du personnage' + (' ' + npc.get('name') if npc.get('name') else '')}}" src="{{'/static/images/no_photography_white.svg' if npc.get('img') == None else '/api/v1/getNpcImage?npcid=' + npc.get('img')|string}}">
|
<img class="character_image" alt="{{'Image du personnage' + (' ' + npc.get('name') if npc.get('name') else '')}}" src="{{'/static/images/no_photography_white.svg' if npc.get('img') == None else '/api/v1/getNpcImage?npcid=' + npc.get('img')|string}}">
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section id="reactions">
|
||||||
|
<h2 class="section_title">Réactions</h2>
|
||||||
|
|
||||||
|
{%for reaction in npc.get("reactions")%}
|
||||||
|
<div>
|
||||||
|
<p> {{reaction.get('name')}} </p>
|
||||||
|
<img src="{{reaction.get('url')}}" style="max-width: 100; max-height: 100px">
|
||||||
|
<input class="img_input", type="file" accept="image/png, image/jpeg" onchange="changeReaction(this)">
|
||||||
|
<input class="trait_id", type="hidden" value="{{reaction.get('trait_id')}}">
|
||||||
|
<button onclick="deleteReaction(this)">Delete reaction</button>
|
||||||
|
</div>
|
||||||
|
{%endfor%}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<select id="reactions_to_add" onchange="addReaction(this)">
|
||||||
|
<option value="" default></option>
|
||||||
|
{%for reaction_to_add in npc.get("reactions_to_add") or []%}
|
||||||
|
<option value="{{reaction_to_add.get('trait_id')}}">{{reaction_to_add.get('name')}}</option>
|
||||||
|
{%endfor%}
|
||||||
|
</select>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2 class="section_title">Réponses aux questions lors de l'interrogation</h2>
|
<h2 class="section_title">Réponses aux questions lors de l'interrogation</h2>
|
||||||
<div class="answer_groups">
|
<div class="answer_groups">
|
||||||
|
@ -28,6 +28,12 @@
|
|||||||
<li class="license_item">
|
<li class="license_item">
|
||||||
Roboto Mono, police créée par Christian Robertson sous license <a href="https://www.apache.org/licenses/LICENSE-2.0.html" title="Voir la license Apache 2.0 dans un nouvel onglet" target="_blank" class="legal_link">Apache 2.0</a>
|
Roboto Mono, police créée par Christian Robertson sous license <a href="https://www.apache.org/licenses/LICENSE-2.0.html" title="Voir la license Apache 2.0 dans un nouvel onglet" target="_blank" class="legal_link">Apache 2.0</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="license_item">
|
||||||
|
Park Lane NF, police créée par Nick Curtis sous license <a href="https://www.1001fonts.com/licenses/ffc.html" title="Voir la license 1001Fonts Free For Commercial Use dans un nouvel onglet" target="_blank" class="legal_link">FFC</a>
|
||||||
|
</li>
|
||||||
|
<li class="license_item">
|
||||||
|
Ironick, police créée par Nick Curtis sous license <a href="https://www.1001fonts.com/licenses/ffc.html" title="Voir la license 1001Fonts Free For Commercial Use dans un nouvel onglet" target="_blank" class="legal_link">FFC</a>
|
||||||
|
</li>
|
||||||
<li class="license_item">
|
<li class="license_item">
|
||||||
Material Icons, icônes créés par Google sous license <a href="https://www.apache.org/licenses/LICENSE-2.0.html" title="Voir la license Apache 2.0 dans un nouvel onglet" target="_blank" class="legal_link">Apache 2.0</a>
|
Material Icons, icônes créés par Google sous license <a href="https://www.apache.org/licenses/LICENSE-2.0.html" title="Voir la license Apache 2.0 dans un nouvel onglet" target="_blank" class="legal_link">Apache 2.0</a>
|
||||||
</li>
|
</li>
|
||||||
@ -61,6 +67,9 @@
|
|||||||
<li class="license_item">
|
<li class="license_item">
|
||||||
Flask-APScheduler, bibliothèque Python créée par Vinicius Chiele sous license <a href="https://www.apache.org/licenses/LICENSE-2.0.html" title="Voir la license Apache 2.0 dans un nouvel onglet" target="_blank" class="legal_link">Apache 2.0</a>
|
Flask-APScheduler, bibliothèque Python créée par Vinicius Chiele sous license <a href="https://www.apache.org/licenses/LICENSE-2.0.html" title="Voir la license Apache 2.0 dans un nouvel onglet" target="_blank" class="legal_link">Apache 2.0</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="license_item">
|
||||||
|
Image d'ecran titre, libre utilisation personnelle et commerciale. <a href="https://www.freepik.com/free-vector/gradient-art-deco-background_20216164.htm">Source et licenses.</a>
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<footer>
|
<footer>
|
||||||
<div class="footer_links">
|
<div class="footer_links">
|
||||||
|