Script python pour propose des évts en ligne de commande!
This commit is contained in:
parent
8532c88097
commit
f140d7c7bd
@ -32,27 +32,3 @@ $(document).on 'page:receive', ->
|
|||||||
$(document).on 'page:load', ->
|
$(document).on 'page:load', ->
|
||||||
# Reload polyfill when turbolinks loads a new page
|
# Reload polyfill when turbolinks loads a new page
|
||||||
$(this).updatePolyfill()
|
$(this).updatePolyfill()
|
||||||
|
|
||||||
$(document).on 'page:receive', ->
|
|
||||||
# Delete existing tinymce editors, very important in the turbolinks context!
|
|
||||||
tinymce.remove()
|
|
||||||
|
|
||||||
$(document).ready ->
|
|
||||||
tinyMCE.init
|
|
||||||
selector: 'textarea',
|
|
||||||
menubar : false,
|
|
||||||
schema: 'html5',
|
|
||||||
add_unload_trigger: true,
|
|
||||||
browser_spellcheck: true,
|
|
||||||
toolbar: [
|
|
||||||
' bold italic strikethrough
|
|
||||||
| bullist numlist outdent indent
|
|
||||||
| alignleft aligncenter alignright alignjustify
|
|
||||||
| link media insertdatetime charmap table
|
|
||||||
| undo redo
|
|
||||||
| searchreplace
|
|
||||||
| code visualblocks preview fullscreen'
|
|
||||||
],
|
|
||||||
plugins: 'lists, advlist, autolink, link, charmap, paste, print, preview,
|
|
||||||
table, fullscreen, searchreplace, media, insertdatetime, visualblocks,
|
|
||||||
visualchars, wordcount, contextmenu, code'
|
|
||||||
|
@ -1,3 +1,26 @@
|
|||||||
$(document).ready ->
|
$(document).ready ->
|
||||||
$('form.region_selector select').change ->
|
$('form.region_selector select').change ->
|
||||||
this.form.submit()
|
this.form.submit()
|
||||||
|
|
||||||
|
tinyMCE.init
|
||||||
|
selector: 'textarea#event_description',
|
||||||
|
menubar : false,
|
||||||
|
schema: 'html5',
|
||||||
|
add_unload_trigger: true,
|
||||||
|
browser_spellcheck: true,
|
||||||
|
toolbar: [
|
||||||
|
' bold italic strikethrough
|
||||||
|
| bullist numlist outdent indent
|
||||||
|
| alignleft aligncenter alignright alignjustify
|
||||||
|
| link media insertdatetime charmap table
|
||||||
|
| undo redo
|
||||||
|
| searchreplace
|
||||||
|
| code visualblocks preview fullscreen'
|
||||||
|
],
|
||||||
|
plugins: 'lists, advlist, autolink, link, charmap, paste, print, preview,
|
||||||
|
table, fullscreen, searchreplace, media, insertdatetime, visualblocks,
|
||||||
|
visualchars, wordcount, contextmenu, code'
|
||||||
|
|
||||||
|
$(document).on 'page:receive', ->
|
||||||
|
# Delete existing tinymce editors, very important in the turbolinks context!
|
||||||
|
tinymce.remove()
|
||||||
|
1
public/adl-submit-latest-version
Normal file
1
public/adl-submit-latest-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.0
|
387
public/adl-submit.py
Executable file
387
public/adl-submit.py
Executable file
@ -0,0 +1,387 @@
|
|||||||
|
#!/usr/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
# Copyright (C) 2005 Thomas Petazzoni <thomas.petazzoni@enix.org>
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU General Public License as published by
|
||||||
|
# the Free Software Foundation; version 2 of the License.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful,
|
||||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
# GNU General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU General Public License
|
||||||
|
# along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
|
||||||
|
import xml.dom.minidom
|
||||||
|
import getopt
|
||||||
|
import sys
|
||||||
|
import pycurl
|
||||||
|
import StringIO
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import locale
|
||||||
|
|
||||||
|
locale.setlocale(locale.LC_ALL, ('fr_FR', 'utf-8'))
|
||||||
|
|
||||||
|
eventFields = [ "title", "start-date", "end-date", "start-hour",
|
||||||
|
"end-hour", "description", "city", "region",
|
||||||
|
"url", "contact", "submitter", "tags" ]
|
||||||
|
|
||||||
|
regions = {
|
||||||
|
u'Alsace' : 1,
|
||||||
|
u'Aquitaine' : 2,
|
||||||
|
u'Auvergne' : 3,
|
||||||
|
u'Basse-Normandie' : 4,
|
||||||
|
u'Bourgogne' : 5,
|
||||||
|
u'Bretagne' : 6,
|
||||||
|
u'Centre' : 7,
|
||||||
|
u'Champagne-Ardenne' : 8,
|
||||||
|
u'Corse' : 9,
|
||||||
|
u'Franche-Comté' : 10,
|
||||||
|
u'Haute-Normandie' : 11,
|
||||||
|
u'Île-de-France' : 12,
|
||||||
|
u'Languedoc-Roussillon' : 13,
|
||||||
|
u'Limousin' : 14,
|
||||||
|
u'Lorraine' : 15,
|
||||||
|
u'Midi-Pyrénées' : 16,
|
||||||
|
u'Nord-Pas-de-Calais' : 17,
|
||||||
|
u'Pays' : 18,
|
||||||
|
u'Picardie' : 19,
|
||||||
|
u'Poitou-Charentes' : 20,
|
||||||
|
u'Provence-Alpes-Côte-d\'Azur' : 21,
|
||||||
|
u'Rhône-Alpes' : 22,
|
||||||
|
u'Guadeloupe' : 23,
|
||||||
|
u'Guyane' : 24,
|
||||||
|
u'Martinique' : 25,
|
||||||
|
u'Réunion' : 26,
|
||||||
|
u'Autre' : 27
|
||||||
|
}
|
||||||
|
|
||||||
|
baseUrl = "http://www.agendadulibre.org"
|
||||||
|
#baseUrl = "http://localhost:3000/"
|
||||||
|
|
||||||
|
def Usage():
|
||||||
|
print u"""Soumettre un évènement dans l'Agenda du Libre
|
||||||
|
|
||||||
|
Options:
|
||||||
|
--file event.xml Fichier XML décrivant l'évènement.
|
||||||
|
--test-output test.html Fichier de sortie HTML de test
|
||||||
|
--start-date YYYY-MM-DD Date de début de l'évènement.
|
||||||
|
--end-date YYYY-MM-DD Date de fin de l'évènement.
|
||||||
|
--start-hour HH:MM Heure de début de l'évènement.
|
||||||
|
--end-hour HH:MM Heure de fin de l'évènement.
|
||||||
|
--title chaine Titre de l'évènement.
|
||||||
|
--description chaine-html Description de l'évènement.
|
||||||
|
--city chaine Ville de l'évènement.
|
||||||
|
--region entier Région de l'évènement.
|
||||||
|
--url chaine URL décrivant l'évènement.
|
||||||
|
--contact chaine E-mail de contact.
|
||||||
|
--tags chaine Liste des tags.
|
||||||
|
|
||||||
|
Exemple de fichier XML:
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<event>
|
||||||
|
<title>Permanence Logiciels Libres</title>
|
||||||
|
<start-hour>18:00</start-hour>
|
||||||
|
<end-hour>21:00</end-hour>
|
||||||
|
<description><![CDATA[
|
||||||
|
|
||||||
|
<p><a href="http://www.gulliver.eu.org">Gulliver</a> organise chaque
|
||||||
|
semaine une permanence <i>Logiciels Libres</i> ouverte à tous,
|
||||||
|
membre de l'association ou non.</p>
|
||||||
|
|
||||||
|
<p>Durant cette permanence, vous pourrez trouver des réponses aux
|
||||||
|
questions que vous vous posez au sujet du Logiciel Libre, ainsi que
|
||||||
|
de l'aide pour résoudre vos problèmes d'installation, de
|
||||||
|
configuration et d'utilisation de Logiciels Libres. N'hésitez pas
|
||||||
|
à apporter votre ordinateur, afin que les autres participants
|
||||||
|
puissent vous aider.</p>
|
||||||
|
|
||||||
|
<p>Une connexion Internet est disponible sur place, ainsi que les
|
||||||
|
mises à jour pour les distributions GNU/Linux les plus
|
||||||
|
courantes.</p>
|
||||||
|
|
||||||
|
<p>Cette permanence a lieu à la <a
|
||||||
|
href=\"http://www.grand-cordel.com/\">MJC du Grand Cordel</a>, 18
|
||||||
|
rue des Plantes à Rennes.</p>
|
||||||
|
|
||||||
|
]]></description>
|
||||||
|
<city>Rennes</city>
|
||||||
|
<region>Bretagne</region>
|
||||||
|
<url>http://www.gulliver.eu.org</url>
|
||||||
|
<contact>contact@gulliver.eu.org</contact>
|
||||||
|
<tags>gulliver permanence</tags>
|
||||||
|
</event>
|
||||||
|
|
||||||
|
Valeurs des champs:
|
||||||
|
Le fichier XML peut contenir des champs dont le nom est semblable
|
||||||
|
à celui des options, à savoir start-date, end-date,
|
||||||
|
start-hour, end-hour, title, description, city, region, url et
|
||||||
|
contact. Si un champ est défini à la fois dans le fichier XML
|
||||||
|
et sur la ligne de commande, alors c'est la valeur donnée sur la
|
||||||
|
ligne de commande qui l'emporte. Entre le fichier XML et la ligne de
|
||||||
|
commande, tous les champs doivent être définis, sinon l'ajout
|
||||||
|
de l'évènement sera refusé. Le seul champ qui peut être
|
||||||
|
vide est end-date, auquel cas il sera positionné à la même
|
||||||
|
valeur que start-date.
|
||||||
|
|
||||||
|
Remplacements:
|
||||||
|
Si la chaîne $month est trouvée dans la description, elle sera
|
||||||
|
automatiquement remplacée par le nom du mois de la date de début
|
||||||
|
de l'évènement.
|
||||||
|
Si la chaîne $date est trouvée dans la description, elle sera
|
||||||
|
automatiquement remplacée par la date de début de l'évènement
|
||||||
|
(numéro du jour dans le mois puis nom du mois).
|
||||||
|
|
||||||
|
Exemple d'utilisation:
|
||||||
|
./adl-submit.py --file event.xml --start-date 2005-12-10
|
||||||
|
|
||||||
|
Ajoutera l'évènement décrit dans le fichier event.xml
|
||||||
|
(donné ci-dessous) pour la date du 10 décembre 2005. Puisque
|
||||||
|
le champ end-date n'est pas spécifié, alors il vaudra la
|
||||||
|
même chose que start-date, c'est à dire le 10 décembre
|
||||||
|
2005.
|
||||||
|
|
||||||
|
Pour vérifier que le formatage est correct avant l'envoi,
|
||||||
|
on pourra utiliser:
|
||||||
|
|
||||||
|
./adl-submit.py --file event.xml --start-date 2005-12-10
|
||||||
|
--test-output test.html
|
||||||
|
|
||||||
|
et regarder le fichier test.html avec un navigateur Web.
|
||||||
|
"""
|
||||||
|
sys.exit (1)
|
||||||
|
|
||||||
|
def HandleXmlFile(file, values):
|
||||||
|
dom = xml.dom.minidom.parse(file)
|
||||||
|
for node in dom.getElementsByTagName("event")[0].childNodes:
|
||||||
|
if node.nodeType == node.ELEMENT_NODE:
|
||||||
|
val = node.childNodes[0]
|
||||||
|
for field in eventFields:
|
||||||
|
if node.nodeName == field:
|
||||||
|
values[field] = val.data
|
||||||
|
|
||||||
|
def HandleParamValue(param, val, values):
|
||||||
|
for field in eventFields:
|
||||||
|
if param == "--" + field:
|
||||||
|
values[field] = val
|
||||||
|
|
||||||
|
def ParseOptions(options):
|
||||||
|
getoptOptions = map (lambda elt: elt + "=", eventFields)
|
||||||
|
getoptOptions.append ("file=")
|
||||||
|
getoptOptions.append ("help")
|
||||||
|
getoptOptions.append("test-output=")
|
||||||
|
eventFieldValues = {}
|
||||||
|
|
||||||
|
testOutputFile = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
opts, args = getopt.getopt(options, "", getoptOptions)
|
||||||
|
except getopt.GetoptError:
|
||||||
|
print u"Option inconnue."
|
||||||
|
Usage()
|
||||||
|
|
||||||
|
if opts == []:
|
||||||
|
Usage()
|
||||||
|
|
||||||
|
for param, val in opts:
|
||||||
|
if param == "--help":
|
||||||
|
Usage()
|
||||||
|
|
||||||
|
if param == "--file":
|
||||||
|
HandleXmlFile(val, eventFieldValues)
|
||||||
|
|
||||||
|
if param == "--test-output":
|
||||||
|
testOutputFile = val
|
||||||
|
|
||||||
|
for param, val in opts:
|
||||||
|
HandleParamValue (param, val, eventFieldValues)
|
||||||
|
|
||||||
|
return (eventFieldValues, testOutputFile)
|
||||||
|
|
||||||
|
def getAuthToken(baseUrl):
|
||||||
|
|
||||||
|
curl = pycurl.Curl()
|
||||||
|
|
||||||
|
contents = StringIO.StringIO()
|
||||||
|
curl.setopt(curl.WRITEFUNCTION, contents.write)
|
||||||
|
|
||||||
|
curl.setopt(curl.URL, baseUrl)
|
||||||
|
curl.setopt(pycurl.COOKIEJAR, '/tmp/cookie.txt')
|
||||||
|
curl.setopt(pycurl.COOKIEFILE, '/tmp/cookie.txt')
|
||||||
|
|
||||||
|
curl.perform()
|
||||||
|
|
||||||
|
m = re.search(r'(<meta content="(.*?)" name="csrf-token" />)', contents.getvalue())
|
||||||
|
m = re.search(r'"(.*?)"', m.group())
|
||||||
|
return m.group().strip('"')
|
||||||
|
|
||||||
|
def SubmitEvent(event, testOutputFile):
|
||||||
|
|
||||||
|
if not event.has_key ("end-date") and event.has_key('start-date'):
|
||||||
|
event ["end-date"] = event ["start-date"]
|
||||||
|
|
||||||
|
if not event.has_key("submitter") and event.has_key('contact'):
|
||||||
|
event ['submitter'] = event['contact']
|
||||||
|
|
||||||
|
for field in eventFields:
|
||||||
|
if not event.has_key(field):
|
||||||
|
print u"Le champ '%s' n'est pas renseigné" % field
|
||||||
|
return
|
||||||
|
|
||||||
|
if re.compile(r'^[^\<\>]*$').search (event['title']) is None:
|
||||||
|
print u"Problème de formatage dans le titre: '%s'. Les tags HTML ne sont pas autorisés." % event['title']
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
startDate = time.strptime(event['start-date'], "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
print u"Problème de formatage dans la date de début: '%s'. Elle doit être de la forme AAAA-MM-JJ" % event['start-date']
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
endDate = time.strptime(event['end-date'], "%Y-%m-%d")
|
||||||
|
except ValueError:
|
||||||
|
print u"Problème de formatage dans la date de fin: '%s'. Elle doit être de la forme AAAA-MM-JJ" % event['end-date']
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
startHour = time.strptime(event['start-hour'], "%H:%M")
|
||||||
|
except ValueError:
|
||||||
|
print u"Problème de formatage dans l'heure de début: '%s'. Elle doit être de la forme: HH:MM" % event['start-hour']
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
endHour = time.strptime(event['end-hour'], "%H:%M")
|
||||||
|
except ValueError:
|
||||||
|
print u"Problème de formatage dans l'heure de fin: '%s'. Elle doit être de la forme HH:MM" % event['start-hour']
|
||||||
|
return
|
||||||
|
|
||||||
|
for tag in event['tags'].split(' '):
|
||||||
|
if len(tag) < 3:
|
||||||
|
print u"Le tag '%s' est trop petit, minimum de 3 caractères" % tag
|
||||||
|
return
|
||||||
|
|
||||||
|
startDate = (startDate[0], startDate[1], startDate[2], startHour[3],
|
||||||
|
startHour[4], startDate[5], startDate[6], startDate[7], startDate[8])
|
||||||
|
endDate = (endDate[0], endDate[1], endDate[2], endHour[3],
|
||||||
|
endHour[4], endDate[5], endDate[6], endDate[7], endDate[8])
|
||||||
|
|
||||||
|
if time.mktime(startDate) <= time.time():
|
||||||
|
print u"ERREUR: La date de début de l'évènement est dans le passé."
|
||||||
|
return
|
||||||
|
|
||||||
|
if time.mktime(endDate) <= time.time():
|
||||||
|
print u"ERREUR: La date de fin de l'évènement est dans le passé."
|
||||||
|
return
|
||||||
|
|
||||||
|
if time.mktime(endDate) < time.mktime(startDate):
|
||||||
|
print u"ERREUR: La date de fin de l'évènement est avant la date de début."
|
||||||
|
return
|
||||||
|
|
||||||
|
if re.compile(r'^[^\<\>]*$').search (event['city']) is None:
|
||||||
|
print u"ERREUR: Problème de formatage dans le nom de la ville: '%s'. Les tags HTML sont interdits." % event['city']
|
||||||
|
return
|
||||||
|
|
||||||
|
if regions.has_key(event['region']) is False:
|
||||||
|
print u"ERREUR: La région '%s' n'existe pas." % event['region']
|
||||||
|
print u"Les régions existantes sont:"
|
||||||
|
for name in regions:
|
||||||
|
print u" - " + name
|
||||||
|
return
|
||||||
|
|
||||||
|
if re.compile(r'^http://.*$').search (event['url']) is None and re.compile(r'^https://.*$').search (event['url']) is None:
|
||||||
|
print u"ERREUR: Problème de formatage dans l'URL: '%s'. Elle doit commencer par http:// ou https://." % event['url']
|
||||||
|
return
|
||||||
|
|
||||||
|
if re.compile(r'^([A-Za-z0-9_\.\-]*)@([A-Za-z0-9_\-]*)\.([A-Za-z0-9_\.\-]*)$').search (event['contact']) is None:
|
||||||
|
print u"ERREUR: Problème de formatage dans l'adresse e-mail." % event ['contact']
|
||||||
|
return
|
||||||
|
|
||||||
|
if re.compile(r'^([A-Za-z0-9_\.\-]*)@([A-Za-z0-9_\-]*)\.([A-Za-z0-9_\.\-]*)$').search (event['submitter']) is None:
|
||||||
|
print u"ERREUR: Problème de formatage dans l'adresse e-mail." % event ['submitter']
|
||||||
|
return
|
||||||
|
|
||||||
|
monthstr = unicode(time.strftime("%B", startDate), 'utf-8')
|
||||||
|
datestr = unicode(time.strftime("%d %B", startDate), 'utf-8')
|
||||||
|
|
||||||
|
event['description'] = event['description'].replace("$month", monthstr)
|
||||||
|
event['description'] = event['description'].replace("$date", datestr)
|
||||||
|
|
||||||
|
curl = pycurl.Curl()
|
||||||
|
|
||||||
|
contents = StringIO.StringIO()
|
||||||
|
curl.setopt(curl.WRITEFUNCTION, contents.write)
|
||||||
|
|
||||||
|
if testOutputFile:
|
||||||
|
curl.setopt (curl.URL, baseUrl + 'events?visu=true')
|
||||||
|
else:
|
||||||
|
curl.setopt (curl.URL, baseUrl + 'events')
|
||||||
|
|
||||||
|
curl.setopt(curl.HTTPPOST, [('authenticity_token', str(getAuthToken(baseUrl+'events/new'))),
|
||||||
|
('event[title]', event['title'].encode('utf-8')),
|
||||||
|
('event[start_time(3i)]', str(startDate[2])),
|
||||||
|
('event[start_time(2i)]', str(startDate[1])),
|
||||||
|
('event[start_time(1i)]', str(startDate[0])),
|
||||||
|
('event[start_time(4i)]', str(startDate[3])),
|
||||||
|
('event[start_time(5i)]', str(startDate[4])),
|
||||||
|
('event[end_time(3i)]', str(endDate[2])),
|
||||||
|
('event[end_time(2i)]', str(endDate[1])),
|
||||||
|
('event[end_time(1i)]', str(endDate[0])),
|
||||||
|
('event[end_time(4i)]', str(endHour[3])),
|
||||||
|
('event[end_time(5i)]', str(endHour[4])),
|
||||||
|
('event[description]', event['description'].encode('utf-8')),
|
||||||
|
('event[city]', event['city'].encode('utf-8')),
|
||||||
|
('event[region]', str(regions[event['region']])),
|
||||||
|
('event[locality]', str(0)),
|
||||||
|
('event[url]', event['url'].encode('utf-8')),
|
||||||
|
('event[contact]', event['contact'].encode('utf-8')),
|
||||||
|
('event[submitter]', event['submitter'].encode('utf-8')),
|
||||||
|
('event[tags]', event['tags'].encode('utf-8'))])
|
||||||
|
|
||||||
|
curl.setopt(pycurl.COOKIEJAR, '/tmp/cookie.txt')
|
||||||
|
curl.setopt(pycurl.COOKIEFILE, '/tmp/cookie.txt')
|
||||||
|
curl.perform()
|
||||||
|
|
||||||
|
if testOutputFile:
|
||||||
|
if curl.getinfo(curl.HTTP_CODE) != 200:
|
||||||
|
print u"Erreur lors de la récupération de la sortie HTML"
|
||||||
|
sys.exit(0)
|
||||||
|
fp = open(testOutputFile, "wb")
|
||||||
|
s = contents.getvalue()
|
||||||
|
s = re.sub(r'href="([A-Za-z0-9]*).css"', r'href="'+baseUrl+'\1.css"', s)
|
||||||
|
fp.write(s)
|
||||||
|
fp.close()
|
||||||
|
|
||||||
|
else:
|
||||||
|
if curl.getinfo(curl.HTTP_CODE) != 302:
|
||||||
|
print u"Erreur lors de la soumission de l'évènement"
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print u"Évènement soumis avec succès. Il sera prochainement validé par un modérateur."
|
||||||
|
|
||||||
|
|
||||||
|
if (len(sys.argv) == 1) and sys.argv[1] == "--help":
|
||||||
|
Usage()
|
||||||
|
|
||||||
|
(event, testOutputFile) = ParseOptions(sys.argv[1:])
|
||||||
|
|
||||||
|
# Check that we are running the latest version of the adl-submit
|
||||||
|
# script
|
||||||
|
if not testOutputFile:
|
||||||
|
contents = StringIO.StringIO()
|
||||||
|
curl = pycurl.Curl()
|
||||||
|
curl.setopt(curl.WRITEFUNCTION, contents.write)
|
||||||
|
curl.setopt (curl.URL, baseUrl + './adl-submit-latest-version')
|
||||||
|
curl.perform()
|
||||||
|
if curl.getinfo(curl.HTTP_CODE) == 200:
|
||||||
|
if float(contents.getvalue()) != float('3.0'):
|
||||||
|
print u"Votre script n'est plus à jour, merci de télécharger la nouvelle version à l'adresse"
|
||||||
|
print u"%sadl-submit.py" % baseUrl
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
SubmitEvent(event, testOutputFile)
|
Loading…
Reference in New Issue
Block a user