Widget pour choisir ou créer un lieu fonctionnel
This commit is contained in:
parent
3b030aef70
commit
7e8384f086
9 changed files with 406 additions and 35 deletions
|
@ -482,3 +482,51 @@ div.as-results {
|
||||||
#map_addlieu {
|
#map_addlieu {
|
||||||
height: 500px;
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.window {
|
||||||
|
display:none;
|
||||||
|
position:fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
display:block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-bg {
|
||||||
|
background: #000;
|
||||||
|
opacity: 0.7;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.window-content {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 50vh;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
background: #eee;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.lieu-ui {
|
||||||
|
.map {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -552,3 +552,50 @@ div.as-results ul li.as-message {
|
||||||
#map_addlieu {
|
#map_addlieu {
|
||||||
height: 500px;
|
height: 500px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* line 486, ../../sass/screen.scss */
|
||||||
|
.window {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
/* line 496, ../../sass/screen.scss */
|
||||||
|
.window.visible {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/* line 500, ../../sass/screen.scss */
|
||||||
|
.window .window-bg {
|
||||||
|
background: #000;
|
||||||
|
opacity: 0.7;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
/* line 511, ../../sass/screen.scss */
|
||||||
|
.window .window-content {
|
||||||
|
position: relative;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
margin-top: 50vh;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1;
|
||||||
|
background: #eee;
|
||||||
|
max-width: 600px;
|
||||||
|
width: 90%;
|
||||||
|
max-height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* line 528, ../../sass/screen.scss */
|
||||||
|
.lieu-ui .map {
|
||||||
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
194
avisstage/static/js/select_lieu.js
Normal file
194
avisstage/static/js/select_lieu.js
Normal file
|
@ -0,0 +1,194 @@
|
||||||
|
function SelectLieuWidget(STATIC_ROOT, target, callback) {
|
||||||
|
var map, input, autocomplete;
|
||||||
|
var $el = $(target);
|
||||||
|
var ui_el = $el.find('.lieu-ui');
|
||||||
|
var form_el = $el.find('form');
|
||||||
|
var content_el = $el.find('.window-content');
|
||||||
|
var ui_ready = false;
|
||||||
|
var lieux_db = {};
|
||||||
|
|
||||||
|
form_el.detach();
|
||||||
|
form_el.on("submit", nouveauLieu);
|
||||||
|
|
||||||
|
function initUI(){
|
||||||
|
$.each(ui_el.children(), function(i, item){$(item).remove();});
|
||||||
|
|
||||||
|
var map_el = $("<div>", {class: "map"});
|
||||||
|
input = $("<input>",
|
||||||
|
{type:"text",
|
||||||
|
placeholder:"Chercher un établissement..."});
|
||||||
|
|
||||||
|
ui_el.append(input);
|
||||||
|
ui_el.append(map_el);
|
||||||
|
|
||||||
|
// Affiche la carte
|
||||||
|
map = L.map(map_el[0]).setView([48.8422411,2.3430553], 13);
|
||||||
|
var layer = new L.StamenTileLayer("terrain");
|
||||||
|
map.addLayer(layer);
|
||||||
|
|
||||||
|
// Autocomplete
|
||||||
|
autocomplete = new google.maps.places.Autocomplete(input[0]);
|
||||||
|
autocomplete.setTypes(["geocode", "establishment"]);
|
||||||
|
autocomplete.addListener('place_changed', handlePlaceSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.showWidget = function() {
|
||||||
|
$el.addClass("visible").removeClass("ajout");
|
||||||
|
if(!ui_ready)
|
||||||
|
initUI();
|
||||||
|
form_el.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closeWidget = function() {
|
||||||
|
$el.removeClass("visible");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icones
|
||||||
|
function makeIcon(couleur){
|
||||||
|
return L.icon({
|
||||||
|
iconUrl: STATIC_ROOT + 'images/marker-'+couleur+'.png',
|
||||||
|
iconSize: [36, 46],
|
||||||
|
iconAnchor: [18, 45],
|
||||||
|
popupAnchor: [0, -48]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
var greenIcon = makeIcon('green');
|
||||||
|
var redIcon = makeIcon('red');
|
||||||
|
var blueIcon = makeIcon('blue');
|
||||||
|
|
||||||
|
// Callback de l'autocomplete
|
||||||
|
function handlePlaceSearch() {
|
||||||
|
var place = autocomplete.getPlace();
|
||||||
|
if (!place.geometry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log(place);
|
||||||
|
|
||||||
|
if (lieux_db.suggestion !== undefined) {
|
||||||
|
lieux_db.suggestion.marqueur.remove();
|
||||||
|
lieux_db.suggestion = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Processing du lieu
|
||||||
|
var data = {};
|
||||||
|
$.each(place.address_components, function(i, obj) {
|
||||||
|
for (var j=0; j<obj.types.length; j++) {
|
||||||
|
switch(obj.types[j]) {
|
||||||
|
case "locality":
|
||||||
|
data.ville = obj.long_name;
|
||||||
|
break;
|
||||||
|
case "country":
|
||||||
|
data.pays = obj.short_name;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
data.nom = place.name;
|
||||||
|
data.coord = {lng: place.geometry.location.lng(),
|
||||||
|
lat: place.geometry.location.lat()};
|
||||||
|
data.orig_coord = data.coord;
|
||||||
|
data.fromSuggestion = true;
|
||||||
|
|
||||||
|
lieux_db.suggestion = data;
|
||||||
|
|
||||||
|
map.panTo(data.coord);
|
||||||
|
lieuSurCarte(data);
|
||||||
|
|
||||||
|
// Affichage des suggestions
|
||||||
|
$.getJSON("/api/v1/lieu/", {"format":"json",
|
||||||
|
"lat":location.lat,
|
||||||
|
"lng":location.lng}, showPropositions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Callback suggestions
|
||||||
|
function showPropositions(data) {
|
||||||
|
// TODO gérer les appels concurrents
|
||||||
|
$.each(data.objects, function(i, item) {
|
||||||
|
lieuSurCarte(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function lieuSurCarte(data) {
|
||||||
|
// data : des données sur un lieu, sérialisé comme par tastypie
|
||||||
|
if(data.marqueur !== undefined)
|
||||||
|
data.marqueur.remove();
|
||||||
|
|
||||||
|
var icone = blueIcon;
|
||||||
|
var fromSuggestion = false;
|
||||||
|
|
||||||
|
// Si c'est un résultat d'autocomplete
|
||||||
|
if(data.fromSuggestion === true) {
|
||||||
|
icone = redIcon;
|
||||||
|
fromSuggestion = true;
|
||||||
|
}
|
||||||
|
var marqueur = L.marker(data.coord,
|
||||||
|
{icon: icone, draggable: fromSuggestion});
|
||||||
|
|
||||||
|
data.marqueur = marqueur;
|
||||||
|
var desc = $("<div>").append($("<h3>").text(data.nom))
|
||||||
|
.append($("<p>").text(data.ville+", "+data.pays));
|
||||||
|
var activeBtn = $("<a>", {href:"javascript:void(0);"})
|
||||||
|
.prop("_lieustage_data", data)
|
||||||
|
.on("click", choixLieuStage);
|
||||||
|
|
||||||
|
if(!fromSuggestion) {
|
||||||
|
activeBtn.text("Choisir ce lieu");
|
||||||
|
} else {
|
||||||
|
var resetBtn = $("<a>", {href:"javascript:void(0);"})
|
||||||
|
.text("Réinitialiser la position")
|
||||||
|
.prop("_lieustage_data", data)
|
||||||
|
.on("click", resetOrigLieu);
|
||||||
|
desc.append($("<p>").append(resetBtn))
|
||||||
|
activeBtn.text("Créer un nouveau lieu ici");
|
||||||
|
}
|
||||||
|
desc.append($("<p>").append(activeBtn));
|
||||||
|
|
||||||
|
marqueur.bindPopup(desc[0]).addTo(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetOrigLieu() {
|
||||||
|
var data = this._lieustage_data;
|
||||||
|
data.marqueur.setLatLng(data.orig_coord);
|
||||||
|
map.panTo(data.orig_coord);
|
||||||
|
}
|
||||||
|
|
||||||
|
function choixLieuStage() {
|
||||||
|
var choix = this._lieustage_data;
|
||||||
|
if(!choix.fromSuggestion)
|
||||||
|
callback(choix);
|
||||||
|
else
|
||||||
|
showForm(choix);
|
||||||
|
}
|
||||||
|
|
||||||
|
function showForm(choix) {
|
||||||
|
$el.addClass("ajout");
|
||||||
|
content_el.append(form_el);
|
||||||
|
form_el.find("#id_nom").val(choix.nom);
|
||||||
|
form_el.find("#id_ville").val(choix.ville);
|
||||||
|
form_el.find("#id_pays").val(choix.pays);
|
||||||
|
form_el.find("#id_coord_0").val(choix.coord.lat);
|
||||||
|
form_el.find("#id_coord_1").val(choix.coord.lng);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nouveauLieu() {
|
||||||
|
var coord = lieux_db.suggestion.marqueur.getLatLng();
|
||||||
|
form_el.find("#id_coord_0").val(coord.lat);
|
||||||
|
form_el.find("#id_coord_1").val(coord.lng);
|
||||||
|
$.post(form_el.attr("action")+"?format=json",
|
||||||
|
form_el.serialize(),
|
||||||
|
onLieuCreated);
|
||||||
|
form_el.detach();
|
||||||
|
content_el.append($("<p>").text("Envoi en cours..."));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLieuCreated(data) {
|
||||||
|
console.log(data);
|
||||||
|
if(data.success = false)
|
||||||
|
content_el.append(form_el);
|
||||||
|
else {
|
||||||
|
lieux_db.suggestion.id = data.id;
|
||||||
|
callback(lieux_db.suggestion);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,18 @@
|
||||||
{% extends "avisstage/base.html" %}
|
{% extends "avisstage/base.html" %}
|
||||||
{% load staticfiles %}
|
{% load staticfiles avisstage_tags %}
|
||||||
|
|
||||||
{% block extra_head %}
|
{% block extra_head %}
|
||||||
<link href="{% static "jquery-autosuggest/css/autoSuggest-upshot.css" %}"
|
<script type="text/javascript" src="//maps.googleapis.com/maps/api/js?libraries=places&key=AIzaSyDd4innPShfHcW8KDJB833vZHZSsqt-ACw"></script>
|
||||||
type="text/css" media="all" rel="stylesheet" />
|
<script type="text/javascript" src="{% static "js/leaflet.js" %}"></script>
|
||||||
<script type="text/javascript"
|
<script type="text/javascript" src="{% static "js/leaflet-gplaces-autocomplete.js" %}"></script>
|
||||||
src="{% static "jquery-autosuggest/js/jquery.autoSuggest.minified.js" %}"> </script>
|
<script type="text/javascript" src="//maps.stamen.com/js/tile.stamen.js?v1.3.0"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript" src="{% static "jquery-autosuggest/js/jquery.autoSuggest.minified.js" %}"> </script>
|
||||||
$( function() {
|
<link rel="stylesheet" type="text/css" href="{% static "css/leaflet.css" %}" />
|
||||||
$( ".datepicker" ).datepicker({ dateFormat: 'dd/mm/yy' });
|
|
||||||
} );
|
|
||||||
</script>
|
|
||||||
<script type="text/javascript" src="{% static "js/tinymce/tinymce.min.js" %}"></script>
|
<script type="text/javascript" src="{% static "js/tinymce/tinymce.min.js" %}"></script>
|
||||||
|
<script type="text/javascript" src="{% static "js/select_lieu.js" %}"></script>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(function() {
|
$(function() {
|
||||||
|
$(".datepicker").datepicker({ dateFormat: 'dd/mm/yy' });
|
||||||
// Process rich text fields
|
// Process rich text fields
|
||||||
var txtr = $("textarea.tinymce");
|
var txtr = $("textarea.tinymce");
|
||||||
$.each(txtr, function(i, item) {
|
$.each(txtr, function(i, item) {
|
||||||
|
@ -31,7 +30,7 @@
|
||||||
language: "fr_FR",
|
language: "fr_FR",
|
||||||
});
|
});
|
||||||
|
|
||||||
// process select multiple fields
|
// Process select multiple fields
|
||||||
var slts = $("select[multiple]");
|
var slts = $("select[multiple]");
|
||||||
var NULL_VAL = " ";
|
var NULL_VAL = " ";
|
||||||
$.each(slts, function(i, item) {
|
$.each(slts, function(i, item) {
|
||||||
|
@ -66,6 +65,21 @@
|
||||||
$item.remove();
|
$item.remove();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Widget du choix du lieu
|
||||||
|
var lieu_select = new SelectLieuWidget("{{ STATIC_URL|escapejs }}",
|
||||||
|
$("#lieu_widget"), lieuChoisi);
|
||||||
|
$("#stage-addlieu").prop("_lieustage_data", "new")
|
||||||
|
.on("click", lieu_select.showWidget);
|
||||||
|
var lieu_focus;
|
||||||
|
function clickLieu() {
|
||||||
|
lieu_focus = this;
|
||||||
|
lieu_select.showWidget();
|
||||||
|
}
|
||||||
|
function lieuChoisi(lieu) {
|
||||||
|
// TODO
|
||||||
|
lieu_select.closeWidget();
|
||||||
|
}
|
||||||
|
|
||||||
// À l'envoi du formulaire
|
// À l'envoi du formulaire
|
||||||
$("#stageform").submit(function() {
|
$("#stageform").submit(function() {
|
||||||
$.each(txtr, function(i, item) {
|
$.each(txtr, function(i, item) {
|
||||||
|
@ -109,6 +123,7 @@
|
||||||
{% for fform in avis_lieu_formset %}
|
{% for fform in avis_lieu_formset %}
|
||||||
{{ fform.lieu }}
|
{{ fform.lieu }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
<a href="javascript:void(0);" id="stage-addlieu">Ajouter un lieu</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -130,7 +145,9 @@
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{{ avis_lieu_formset.management_form }}
|
{{ avis_lieu_formset.management_form }}
|
||||||
|
<div id="avis_lieu_container">
|
||||||
{% for fform in avis_lieu_formset %}
|
{% for fform in avis_lieu_formset %}
|
||||||
|
<div class="avis_lieu">
|
||||||
<h2>Commentaire sur le lieu</h2>
|
<h2>Commentaire sur le lieu</h2>
|
||||||
{{ fform.non_field_errors }}
|
{{ fform.non_field_errors }}
|
||||||
{% for field in fform.hidden_fields %}
|
{% for field in fform.hidden_fields %}
|
||||||
|
@ -152,7 +169,9 @@
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
<div id="avis_lieu_vide">
|
<div id="avis_lieu_vide">
|
||||||
{% with avis_lieu_formset.empty_form as fform %}
|
{% with avis_lieu_formset.empty_form as fform %}
|
||||||
<h2>Commentaire sur le lieu</h2>
|
<h2>Commentaire sur le lieu</h2>
|
||||||
|
@ -180,4 +199,7 @@
|
||||||
</div>
|
</div>
|
||||||
<input type="submit" value="Enregistrer" />
|
<input type="submit" value="Enregistrer" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{% lieu_widget %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
33
avisstage/templates/avisstage/templatetags/widget_lieu.html
Normal file
33
avisstage/templates/avisstage/templatetags/widget_lieu.html
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
{% load staticfiles %}
|
||||||
|
|
||||||
|
<div id="lieu_widget" class="window">
|
||||||
|
<div class="window-bg"></div>
|
||||||
|
<div class="window-content">
|
||||||
|
<a class="window-closer"></a>
|
||||||
|
<h2>Choisir un lieu</h2>
|
||||||
|
<div class="lieu-ui">
|
||||||
|
</div>
|
||||||
|
<div class="lieu-form">{% load staticfiles %}
|
||||||
|
<form action="{% url 'avisstage:lieu_ajout' %}" method="post" id="lieu_ajout">
|
||||||
|
<h1>Ajouter un lieu</h1>
|
||||||
|
{% csrf_token %}
|
||||||
|
{% for field in form.hidden_fields %}
|
||||||
|
{{ field }}
|
||||||
|
{% endfor %}
|
||||||
|
{% for field in form.visible_fields %}
|
||||||
|
{{ field.errors }}
|
||||||
|
<div class="field">
|
||||||
|
<label for="{{ field.id_for_label }}">{{ field.label }}</label>
|
||||||
|
<div class="input">
|
||||||
|
{{ field }}
|
||||||
|
{% if field.help_text %}
|
||||||
|
<p class="help_text">{{ field.help_text }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<input type="submit" />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
0
avisstage/templatetags/__init__.py
Normal file
0
avisstage/templatetags/__init__.py
Normal file
10
avisstage/templatetags/avisstage_tags.py
Normal file
10
avisstage/templatetags/avisstage_tags.py
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
from django import template
|
||||||
|
|
||||||
|
from avisstage.forms import LieuForm
|
||||||
|
|
||||||
|
register = template.Library()
|
||||||
|
|
||||||
|
@register.inclusion_tag('avisstage/templatetags/widget_lieu.html')
|
||||||
|
def lieu_widget():
|
||||||
|
form = LieuForm()
|
||||||
|
return {"form": form}
|
|
@ -8,6 +8,7 @@ from django import forms
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from braces.views import LoginRequiredMixin
|
from braces.views import LoginRequiredMixin
|
||||||
|
from django.http import JsonResponse
|
||||||
|
|
||||||
from avisstage.models import Normalien, Stage, Lieu, AvisLieu, AvisStage
|
from avisstage.models import Normalien, Stage, Lieu, AvisLieu, AvisStage
|
||||||
from avisstage.forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm
|
from avisstage.forms import StageForm, LieuForm, AvisStageForm, AvisLieuForm
|
||||||
|
@ -94,6 +95,22 @@ class LieuAjout(CreateView, LoginRequiredMixin):
|
||||||
form_class = LieuForm
|
form_class = LieuForm
|
||||||
template_name = 'avisstage/formulaires/lieu.html'
|
template_name = 'avisstage/formulaires/lieu.html'
|
||||||
|
|
||||||
|
def form_valid(self, form):
|
||||||
|
if self.request.GET.get("format", "") == "json":
|
||||||
|
self.object = form.save()
|
||||||
|
return JsonResponse({"success": True,
|
||||||
|
"id": self.object.id})
|
||||||
|
else:
|
||||||
|
super(LieuAjout, self).form_valid(form)
|
||||||
|
|
||||||
|
def form_invalid(self, form):
|
||||||
|
if self.request.GET.get("format", "") == "json":
|
||||||
|
return JsonResponse({"success": False,
|
||||||
|
"errors": form.errors})
|
||||||
|
else:
|
||||||
|
super(LieuAjout, self).form_valid(form)
|
||||||
|
|
||||||
|
|
||||||
def recherche(request):
|
def recherche(request):
|
||||||
return render(request, 'avisstage/recherche.html')
|
return render(request, 'avisstage/recherche.html')
|
||||||
|
|
||||||
|
|
|
@ -7,8 +7,8 @@ class LatLonWidget(forms.MultiWidget):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, attrs=None, date_format=None, time_format=None):
|
def __init__(self, attrs=None, date_format=None, time_format=None):
|
||||||
widgets = (forms.TextInput(attrs=attrs),
|
widgets = (forms.HiddenInput(attrs=attrs),
|
||||||
forms.TextInput(attrs=attrs))
|
forms.HiddenInput(attrs=attrs))
|
||||||
super(LatLonWidget, self).__init__(widgets, attrs)
|
super(LatLonWidget, self).__init__(widgets, attrs)
|
||||||
|
|
||||||
def decompress(self, value):
|
def decompress(self, value):
|
||||||
|
|
Loading…
Add table
Reference in a new issue