Changement de Streamlit vers Flask
This commit is contained in:
parent
761f3d10aa
commit
6a4cff83f2
21 changed files with 361 additions and 133 deletions
|
@ -1 +1,2 @@
|
|||
BACKEND_URL=
|
||||
BACKEND_URL=
|
||||
FLASK_DEBUG=
|
|
@ -1,40 +0,0 @@
|
|||
# .gitlab-ci.yml
|
||||
|
||||
stages:
|
||||
- dockerize
|
||||
- deployment
|
||||
|
||||
build-push-docker-image-job:
|
||||
stage: dockerize
|
||||
# Specify a Docker image to run the job in.
|
||||
image: docker:20-dind
|
||||
# Specify an additional image 'docker:dind' ("Docker-in-Docker") that
|
||||
# will start up the Docker daemon when it is brought up by a runner.
|
||||
before_script:
|
||||
- docker login -u "$DOCKER_REGISTRY_USER" -p "$DOCKER_REGISTRY_PASSWORD" $DOCKER_REGISTRY_URL # Instructs GitLab to login to its registry
|
||||
services:
|
||||
- name: docker:20-dind
|
||||
alias: docker
|
||||
command: ["--tls=false"]
|
||||
script:
|
||||
- echo "Building..." # MAKE SURE NO SPACE ON EITHER SIDE OF = IN THE FOLLOWING LINE
|
||||
- export CONTAINER_FULL_IMAGE_NAME_WITH_TAG=$IMAGE_NAME_WITH_REGISTRY_PREFIX/my-build-image:$COMMIT_HASH
|
||||
- docker build --network=host -f ./Dockerfile --pull -t built-image-name .
|
||||
- docker tag built-image-name "$CONTAINER_FULL_IMAGE_NAME_WITH_TAG"
|
||||
- docker push "$CONTAINER_FULL_IMAGE_NAME_WITH_TAG"
|
||||
- echo "$CONTAINER_FULL_IMAGE_NAME_WITH_TAG"
|
||||
- echo "Deploying on CapRover..."
|
||||
- docker run --network=host caprover/cli-caprover:2.2.3 caprover deploy --caproverUrl "$CAPROVER_URL" --caproverPassword "$CAPROVER_PASSWORD" -a "$CAPROVER_APP" -i "$CONTAINER_FULL_IMAGE_NAME_WITH_TAG"
|
||||
only:
|
||||
- main
|
||||
variables:
|
||||
DOCKER_DRIVER: overlay2
|
||||
DOCKER_REGISTRY_USER: ${CI_REGISTRY_USER}
|
||||
DOCKER_REGISTRY_PASSWORD: ${CI_REGISTRY_PASSWORD}
|
||||
DOCKER_REGISTRY_URL: ${CI_REGISTRY}
|
||||
IMAGE_NAME_WITH_REGISTRY_PREFIX: ${CI_REGISTRY_IMAGE}
|
||||
COMMIT_HASH: ${CI_COMMIT_SHA}
|
||||
CAPROVER_URL: ${CAPROVER_URL}
|
||||
CAPROVER_PASSWORD: ${CAPROVER_PASSWORD}
|
||||
CAPROVER_APP: ${CAPROVER_APP}
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
steps:
|
||||
- name: docker
|
||||
image: plugins/docker
|
||||
commands:
|
||||
- docker login docker.io -u $${DOCKERHUB_USERNAME} -p $${DOCKERHUB_PASSWORD}
|
||||
- docker build --rm=true -f Dockerfile -t $${CI_COMMIT_REF} .
|
||||
- docker tag $${CI_COMMIT_REF} $${DOCKERHUB_USERNAME}/$${CI_REPO_NAME}:latest
|
||||
- docker push $${DOCKERHUB_USERNAME}/$${CI_REPO_NAME}:latest
|
||||
secrets: [ dockerhub_username, dockerhub_password ]
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- name: caprover
|
||||
image: plugins/docker
|
||||
commands:
|
||||
- docker login docker.io -u $${DOCKERHUB_USERNAME} -p $${DOCKERHUB_PASSWORD}
|
||||
- docker run --network=host caprover/cli-caprover:2.2.3 caprover deploy --caproverUrl "$${CAPROVER_URL}" --caproverPassword "$${CAPROVER_PASSWORD}" -a "$${CI_REPO_NAME}" -i docker.io/$${DOCKERHUB_USERNAME}/$${CI_REPO_NAME}
|
||||
secrets: [ dockerhub_username, dockerhub_password, caprover_url, caprover_password ]
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
15
Dockerfile
15
Dockerfile
|
@ -1,6 +1,6 @@
|
|||
FROM python:3.10-slim
|
||||
FROM python:3.13-slim-bookworm
|
||||
|
||||
EXPOSE 8051
|
||||
EXPOSE 5000
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
@ -10,9 +10,14 @@ RUN pip install -r requirements.txt
|
|||
|
||||
# Copy the app's code
|
||||
COPY config.py ./
|
||||
COPY main.py ./
|
||||
COPY app.py ./
|
||||
COPY ressources ./ressources
|
||||
COPY templates ./templates
|
||||
COPY static ./static
|
||||
|
||||
# Set the environment variable for Flask
|
||||
ENV FLASK_APP=app.py
|
||||
ENV FLASK_RUN_HOST=0.0.0.0
|
||||
|
||||
# Set the entrypoint to run the app
|
||||
ENTRYPOINT [ "streamlit", "run" ]
|
||||
CMD [ "main.py", "--server.port=8051", "--server.headless", "true", "--server.fileWatcherType", "none", "--browser.gatherUsageStats", "false"]
|
||||
CMD ["flask", "run", "--port=5000"]
|
35
app.py
Normal file
35
app.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
from flask import Flask, render_template, request, jsonify
|
||||
from flaskext.markdown import Markdown
|
||||
from markupsafe import Markup
|
||||
import requests
|
||||
import os
|
||||
from config import settings
|
||||
|
||||
app = Flask(__name__)
|
||||
Markdown(app)
|
||||
|
||||
def correct_text(text):
|
||||
url = f"{settings.BACKEND_URL}/corriger"
|
||||
response = requests.post(url, json={"text": text})
|
||||
if response.status_code != 200:
|
||||
return {"error": f"Erreur lors de la requête au serveur: {response.status_code}"}
|
||||
else:
|
||||
return {"text": response.json()["text"]}
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
with open("ressources/header.md", "r") as f:
|
||||
header = f.read()
|
||||
with open("ressources/footer.md", "r") as f:
|
||||
footer = f.read()
|
||||
return render_template('index.html', header=header, footer=footer)
|
||||
|
||||
@app.route('/correct', methods=['POST'])
|
||||
def correct():
|
||||
text = request.form['text']
|
||||
result = correct_text(text)
|
||||
return jsonify(result)
|
||||
|
||||
if __name__ == '__main__':
|
||||
debug_mode = os.getenv('FLASK_DEBUG', 'False').lower() == 'true'
|
||||
app.run(host='localhost', debug=debug_mode)
|
18
config.py
18
config.py
|
@ -1,17 +1,9 @@
|
|||
from typing import Optional
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic_settings import BaseSettings
|
||||
from dotenv import find_dotenv
|
||||
|
||||
LOGGER_NAME = "point-median-frontend"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
BACKEND_URL: str = "http://localhost:8000"
|
||||
|
||||
class Config:
|
||||
env_file = find_dotenv()
|
||||
load_dotenv()
|
||||
|
||||
class Settings:
|
||||
BACKEND_URL = os.getenv("BACKEND_URL")
|
||||
|
||||
settings = Settings()
|
||||
|
|
|
@ -1,4 +1,27 @@
|
|||
docker stop point-median-frontend
|
||||
docker rm point-median-frontend
|
||||
# Ce programme sert à lancer le job_dispatcher dans un docker localement pour tester
|
||||
docker run -p 8051:8051 --name point-median-frontend --env-file .env --network host local/point-median-frontend
|
||||
#!/bin/bash
|
||||
|
||||
# Name of the Docker network and application
|
||||
NETWORK_NAME="point-median-network"
|
||||
APP_NAME="point-median-frontend"
|
||||
|
||||
# Create the Docker network if it doesn't exist
|
||||
if ! docker network inspect $NETWORK_NAME >/dev/null 2>&1; then
|
||||
echo "Creating Docker network: $NETWORK_NAME"
|
||||
docker network create $NETWORK_NAME
|
||||
else
|
||||
echo "Docker network $NETWORK_NAME already exists"
|
||||
fi
|
||||
|
||||
# Stop and remove the existing container if it exists
|
||||
docker stop $APP_NAME >/dev/null 2>&1
|
||||
docker rm $APP_NAME >/dev/null 2>&1
|
||||
# Run the new container
|
||||
echo "Starting $APP_NAME container"
|
||||
docker run -d \
|
||||
-p 5000:5000 \
|
||||
--name $APP_NAME \
|
||||
--env-file .env \
|
||||
--network $NETWORK_NAME \
|
||||
local/$APP_NAME
|
||||
|
||||
echo "Container started. You can access the application at http://localhost:5000"
|
||||
|
|
34
main.py
34
main.py
|
@ -1,34 +0,0 @@
|
|||
import streamlit as st
|
||||
import requests
|
||||
from config import settings
|
||||
import streamlit.components.v1 as components
|
||||
|
||||
|
||||
def correct_text(text):
|
||||
url = f"{settings.BACKEND_URL}/corriger"
|
||||
response = requests.post(url, json={"text": text})
|
||||
if response.status_code != 200:
|
||||
st.error("Erreur lors de la requête au serveur: {response.status_code}")
|
||||
return ""
|
||||
else:
|
||||
return response.json()["text"]
|
||||
|
||||
|
||||
def main():
|
||||
st.title("Application Point Médian")
|
||||
|
||||
with open("ressources/header.md", "r") as f:
|
||||
st.markdown(f.read(), unsafe_allow_html=False)
|
||||
|
||||
text = st.text_area("Entre le texte à corriger", placeholder="Écris ton texte ici", height=200)
|
||||
if st.button("Corriger"):
|
||||
corrected_text = correct_text(text)
|
||||
st.text_area("Texte corrigé:", value=corrected_text, height=200)
|
||||
with open("ressources/formulaire_courriel.html", "r") as f:
|
||||
components.html(f.read(), height=350, scrolling=True)
|
||||
with open("ressources/footer.md", "r") as f:
|
||||
st.markdown(f.read(), unsafe_allow_html=False)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
|
@ -1,5 +1,5 @@
|
|||
pydantic~=2.3.0
|
||||
pydantic-settings==2.0.3
|
||||
streamlit~=1.26.0
|
||||
requests~=2.31.0
|
||||
python-dotenv~=1.0.0
|
||||
Flask==2.3.2
|
||||
Flask-Markdown==0.3
|
||||
MarkupSafe==2.1.3
|
||||
requests
|
||||
python-dotenv
|
|
@ -3,15 +3,10 @@
|
|||
<div>
|
||||
<h3>Abonne-toi pour ne rien manquer</h3>
|
||||
<input type="hidden" name="nonce"/>
|
||||
<p><label for="emaillistmonk">Courriel: </label> <input type="email" id="emaillistmonk" name="email"
|
||||
required placeholder="E-mail"/></p>
|
||||
<p><label for="nomlistmonk">Nom: </label> <input type="text" id="nomlistmonk" name="name"
|
||||
placeholder="Nom (facultatif)"/></p>
|
||||
|
||||
<p>
|
||||
<input id="4acf1" type="checkbox" name="l" checked value="4acf17dd-b2d6-4970-9e5b-25cacd9b3f31"/>
|
||||
<label for="4acf1">Les mises à jour de l'application Point médian</label>
|
||||
</p>
|
||||
<p><label for="emaillistmonk">Courriel : </label> <input type="email" id="emaillistmonk" name="email"
|
||||
required placeholder="E-mail"/></p>
|
||||
<p><label for="nomlistmonk">Nom : </label> <input type="text" id="nomlistmonk" name="name"
|
||||
placeholder="Nom (facultatif)"/></p>
|
||||
<p>
|
||||
<input id="a74b6" type="checkbox" name="l" checked value="a74b62d0-14e0-410c-aa86-517ee1e2e7bd"/>
|
||||
<label for="a74b6">💌 La cyberlettre 💌</label>
|
||||
|
@ -20,7 +15,6 @@
|
|||
<input id="32641" type="checkbox" name="l" checked value="32641cf3-06ab-41bb-87b5-6fcd0886906a"/>
|
||||
<label for="32641">Mon podcast Aires Communes</label>
|
||||
</p>
|
||||
|
||||
<p><input type="submit" value="S'abonner"/></p>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
Cette application convertit le texte entré dans la boîte en forme inclusive abrégée avec le point régulier . vers le point médian ·.
|
||||
Les séparateurs / et - et les formes avec les parenthèses sont aussi convertis.
|
||||
Cette application convertit le texte entré dans la boîte en forme inclusive abrégée avec le point régulier `.` vers le point médian `·`.
|
||||
|
||||
Les séparateurs `/` et `-` et les formes avec les parenthèses sont aussi convertis.
|
||||
|
|
BIN
static/fonts/Cinzel/Cinzel-Bold.ttf
Normal file
BIN
static/fonts/Cinzel/Cinzel-Bold.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Cinzel/Cinzel-Regular.ttf
Normal file
BIN
static/fonts/Cinzel/Cinzel-Regular.ttf
Normal file
Binary file not shown.
BIN
static/fonts/Cinzel/Cinzel-SemiBold.ttf
Normal file
BIN
static/fonts/Cinzel/Cinzel-SemiBold.ttf
Normal file
Binary file not shown.
BIN
static/fonts/FiraSans/FiraSans-Light.ttf
Normal file
BIN
static/fonts/FiraSans/FiraSans-Light.ttf
Normal file
Binary file not shown.
BIN
static/fonts/FiraSans/FiraSans-Medium.ttf
Normal file
BIN
static/fonts/FiraSans/FiraSans-Medium.ttf
Normal file
Binary file not shown.
BIN
static/fonts/FiraSans/FiraSans-Regular.ttf
Normal file
BIN
static/fonts/FiraSans/FiraSans-Regular.ttf
Normal file
Binary file not shown.
BIN
static/fonts/FiraSans/FiraSans-SemiBold.ttf
Normal file
BIN
static/fonts/FiraSans/FiraSans-SemiBold.ttf
Normal file
Binary file not shown.
25
static/script.js
Normal file
25
static/script.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
$(document).ready(function() {
|
||||
$('#correct-button').click(function() {
|
||||
var text = $('#input-text').val();
|
||||
$.ajax({
|
||||
url: '/correct',
|
||||
method: 'POST',
|
||||
data: { text: text },
|
||||
success: function(response) {
|
||||
if (response.error) {
|
||||
alert(response.error);
|
||||
} else {
|
||||
$('#output-text').val(response.text);
|
||||
}
|
||||
},
|
||||
error: function() {
|
||||
alert('Une erreur est survenue lors de la correction du texte.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Load email form
|
||||
$.get('ressources/formulaire_courriel.html', function(data) {
|
||||
$('#email-form').html(data);
|
||||
});
|
||||
});
|
214
static/style.css
Normal file
214
static/style.css
Normal file
|
@ -0,0 +1,214 @@
|
|||
:root {
|
||||
--bg-color: #001233;
|
||||
--text-color: #c5c5c5;
|
||||
--accent-color: #daa520;
|
||||
--secondary-color: #791cf8;
|
||||
--tertiary-color: #daa520;
|
||||
--link-color: #954df9;
|
||||
--link-hover-color: #daa520;
|
||||
--code-bg-color: #1d1d1d;
|
||||
--code-text-color: #c1bdbd;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fira Sans';
|
||||
src: url(/static/fonts/FiraSans/FiraSans-Light.ttf) format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fira Sans';
|
||||
src: url(/static/fonts/FiraSans/FiraSans-Regular.ttf) format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fira Sans';
|
||||
src: url(/static/fonts/FiraSans/FiraSans-Medium.ttf) format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Fira Sans';
|
||||
src: url(/static/fonts/FiraSans/FiraSans-SemiBold.ttf) format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
src: url(/static/fonts/Cinzel/Cinzel-Regular.ttf) format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
src: url(/static/fonts/Cinzel/Cinzel-SemiBold.ttf) format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Cinzel';
|
||||
src: url(/static/fonts/Cinzel/Cinzel-Bold.ttf) format("truetype");
|
||||
font-weight: 700;
|
||||
font-style: normal
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
.container {
|
||||
width: 80%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Adjust the existing body styles */
|
||||
body {
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
font-family: 'Fira Sans', sans-serif;
|
||||
font-weight: 300;
|
||||
font-size: 18px;
|
||||
line-height: 1.6;
|
||||
background-image: radial-gradient(circle at 10% 20%, #8a2be20d 0%, transparent 20%), radial-gradient(circle at 90% 80%, #32cd320d 0%, transparent 20%);
|
||||
}
|
||||
|
||||
/* Adjust padding for elements inside the container */
|
||||
.container > * {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 600
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 800;
|
||||
color: var(--accent-color);
|
||||
text-shadow: 2px 2px 4px #00000080;
|
||||
letter-spacing: 1px;
|
||||
font-size: 2.5em
|
||||
}
|
||||
|
||||
h2, h3 {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 600;
|
||||
color: var(--accent-color);
|
||||
text-shadow: 1px 1px 3px #0000004d;
|
||||
letter-spacing: .5px
|
||||
}
|
||||
|
||||
h4, h5 {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-weight: 400;
|
||||
color: var(--accent-color);
|
||||
text-shadow: 1px 1px 3px #0000004d;
|
||||
letter-spacing: .5px
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75em
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5em
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25em
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--link-color);
|
||||
text-decoration: none;
|
||||
transition: all .3s ease
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--link-hover-color);
|
||||
text-decoration: underline
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 15px 20px;
|
||||
margin: 15px;
|
||||
background-color: var(--code-bg-color);
|
||||
color: var(--code-text-color);
|
||||
border: 1px solid var(--accent-color);
|
||||
border-radius: 5px;
|
||||
font-family: 'Fira Sans', monospace;
|
||||
font-size: 16px;
|
||||
resize: vertical;
|
||||
line-height: 1.5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--secondary-color);
|
||||
box-shadow: 0 0 0 2px rgba(121, 28, 248, 0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 12px 24px;
|
||||
background-color: var(--secondary-color);
|
||||
color: var(--text-color);
|
||||
border: none;
|
||||
border-radius: 30px;
|
||||
cursor: pointer;
|
||||
font-family: 'Fira Sans', sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
margin: 10px 0;
|
||||
display: inline-block;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: var(--tertiary-color);
|
||||
color: var(--bg-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
button:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px rgba(218, 165, 32, 0.4);
|
||||
}
|
||||
|
||||
.markdown-content {
|
||||
margin-bottom: 20px;
|
||||
}
|
31
templates/index.html
Normal file
31
templates/index.html
Normal file
|
@ -0,0 +1,31 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Application Point Médian</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Application Point Médian</h1>
|
||||
|
||||
<div class="markdown-content">
|
||||
{{ header | markdown }}
|
||||
</div>
|
||||
|
||||
<textarea id="input-text" placeholder="Écris ton texte ici" rows="10"></textarea>
|
||||
<button id="correct-button">Corriger</button>
|
||||
<textarea id="output-text" readonly rows="10"></textarea>
|
||||
|
||||
<div id="email-form"></div>
|
||||
|
||||
<div class="markdown-content">
|
||||
{{ footer | markdown }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||
</body>
|
||||
</html>
|
Loading…
Reference in a new issue