Compare commits

..

11 Commits

Author SHA1 Message Date
5921d27f1e runit.sh update
Signed-off-by: philippe lhardy <philippe.lhardy@astrolabe.coop>
2026-03-12 14:45:17 +01:00
philippe lhardy
6095bd26ce exmples mieuxvoter 2025-10-07 13:17:33 +02:00
philippe lhardy
34154c7c4d temporary expected error : works only on my system (absolute directories) 2025-10-07 11:18:33 +02:00
philippe lhardy
23d5368f40 artlog script 2025-10-07 11:02:55 +02:00
8d011bd818 README update 2025-10-07 10:53:08 +02:00
c718ddfd7c missing sample 2025-10-07 10:52:01 +02:00
6a2ccb5d2b run docker dev nextcloud
- missing clone info
- missing setup of certificates (mkcert and so one ... )
2025-09-29 16:15:08 +02:00
ad09c3ec72 update pending 2025-09-28 09:50:55 +02:00
8d65ab03cf rename into english 2025-08-20 17:21:23 +02:00
9159d2a082 fr -> en code 2025-08-20 16:49:41 +02:00
faedb4c80c support mariadb 2025-08-20 11:32:01 +02:00
37 changed files with 1316 additions and 464 deletions

View File

@@ -29,7 +29,7 @@ C'est la description de wikipedia https://fr.wikipedia.org/wiki/Jugement_majorit
(https://en.wikipedia.org/wiki/Majority_judgment) (https://en.wikipedia.org/wiki/Majority_judgment)
L'implémentaion est très basique et miminale : L'implémentation est très basique et miminale :
|langage|code| |langage|code|
|-----|---------------------| |-----|---------------------|
@@ -170,3 +170,12 @@ https://framagit.org/framasoft/framadate
C'est un intégration de framadate dans yunohost. C'est un intégration de framadate dans yunohost.
## Mieux Voter
https://app.mieuxvoter.fr/
Il est possible d'importer un csv.
https://github.com/MieuxVoter/majority-judgment-web-app
exemples sous samples/mieuxvoter

View File

@@ -1,4 +1,4 @@
# Glue to use majortiy judgement algorithm in various polls applications # Glue to use majority judgement algorithm in various polls applications
Initial trigger of this project is implementation of this in Nextcloud poll app https://github.com/nextcloud/polls/issues/3472, project pushed by https://www.astrolabe.coop/ Initial trigger of this project is implementation of this in Nextcloud poll app https://github.com/nextcloud/polls/issues/3472, project pushed by https://www.astrolabe.coop/
@@ -14,7 +14,6 @@ https://en.wikipedia.org/wiki/Majority_judgment
After some research most of implementation is done in https://github.com/MieuxVoter After some research most of implementation is done in https://github.com/MieuxVoter
Sorry for inconvenience but currently only french version is available. Sorry for inconvenience but currently only french version is available.
LISEZMOI.md LISEZMOI.md
@@ -50,4 +49,11 @@ dev done on Ubuntu 24.04.2 LTS
``` ```
cd code cd code
./check_csv.sh ../samples/nextcloud_poll_export/poll1.csv ./check_csv.sh ../samples/nextcloud_poll_export/poll1.csv
``` ```
```
./check_csv.sh ../samples/nextcloud_poll_export/poll1.csv >../samples/0.json
```
et tester

View File

@@ -1,4 +1,37 @@
#!/bin/bash #!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
#
csv_file="$1" toolsdir=../lib
python3 convert_nextcloud_poll_csv.py "$csv_file" 'Très Bien' 'Bien' 'Assez Bien' 'Passable' 'Insuffisant' 'A Rejeter' pushd $toolsdir >/dev/null
toolsdir=$(pwd)
source metascript.sh
popd >/dev/null
while [[ $# > 0 ]]
do
case "$1" in
*)
if [[ -z $csv_file ]]
then
csv_file="$1"
else
parsemetaarg "$1"
fi
;;
esac
shift
done
metarun=metarun
if [[ -z $csv_file ]]
then
log_error "Missing csv_file argument"
exit 1
fi
enforcefile "$csv_file" exists
$metarun python3 convert_nextcloud_poll_csv.py "$csv_file" 'Très Bien' 'Bien' 'Assez Bien' 'Passable' 'Insuffisant' 'A Rejeter'

View File

@@ -3,41 +3,41 @@ import sys
import csv import csv
# using non modified nextcloud standard text poll # using non modified nextcloud standard text poll
nom_mentions=['yes','maybe','','no'] mentions_name=['yes','maybe','','no']
if len(sys.argv) > 1: if len(sys.argv) > 1:
csv_file=sys.argv[1] csv_file=sys.argv[1]
if len(sys.argv) > 2: if len(sys.argv) > 2:
nom_mentions=[] mentions_name=[]
for i in range(2,len(sys.argv)): for i in range(2,len(sys.argv)):
nom_mentions.append(sys.argv[i]) mentions_name.append(sys.argv[i])
else: else:
print('missing file argument') print('missing file argument')
exit(1) exit(1)
def mention_index(mention): def mention_index(mention):
return nom_mentions.index(mention) return mentions_name.index(mention)
verbose=False verbose=False
mentions=len(nom_mentions) mentions=len(mentions_name)
default_mention=mentions-1 default_mention=mentions-1
warnings=[] warnings=[]
candidats={} candidates={}
candidats_alias=[] candidates_alias=[]
a_vote={} a_vote={}
votes=[] votes=[]
with open(csv_file,'r') as csv_fd: with open(csv_file,'r') as csv_fd:
vote_reader = csv.reader(csv_fd, delimiter=',') vote_reader = csv.reader(csv_fd, delimiter=',')
for row in vote_reader: for row in vote_reader:
if len(candidats_alias) == 0: if len(candidates_alias) == 0:
for i in range(2,len(row)): for i in range(2,len(row)):
candidat_alias=row[i] candidate_alias=row[i]
candidats_alias.append(candidat_alias) candidates_alias.append(candidate_alias)
# no other name given # no other name given
if not candidat_alias in candidats: if not candidate_alias in candidates:
candidats[candidat_alias]=candidat_alias candidates[candidate_alias]=candidate_alias
else: else:
# check if not duplicated # check if not duplicated
participant=row[0] participant=row[0]
@@ -47,20 +47,20 @@ with open(csv_file,'r') as csv_fd:
a_vote[participant]=row[1] a_vote[participant]=row[1]
vote={} vote={}
for i in range(2,len(row)): for i in range(2,len(row)):
candidat_index=i-2 candidate_index=i-2
candidat_alias=candidats_alias[candidat_index] candidate_alias=candidates_alias[candidate_index]
mention=row[i] mention=row[i]
vote[candidat_alias]=mention_index(mention) vote[candidate_alias]=mention_index(mention)
votes.append(vote) votes.append(vote)
default_vote={} default_vote={}
if verbose: if verbose:
print(','.join(candidats)) print(','.join(candidates))
print(','.join(nom_mentions)) print(','.join(mentions_name))
result = {'candidats':candidats,'votants':{'decompte':len(a_vote)},'mentions':nom_mentions,'votation':{'votes':votes}} result = {'candidates':candidates,'voters':{'count':len(a_vote)},'mentions':mentions_name,'votation':{'votes':votes}}
print(json.dumps(result)) print(json.dumps(result))
sys.exit(0) sys.exit(0)

View File

@@ -1,124 +0,0 @@
import json
import sys
from majority_judgment import majority_judgment
verbose=False
def collect_votes(votes,candidats,default_mention,warnings):
collect={}
for vote in votes:
# missing candidat in vote picks the worse one
for candidat in candidats:
if not candidat in vote:
vote[candidat]=default_mention
for candidat,mention in vote.items():
if mention > default_mention or mention < 0:
mention = default_mention
warnings.append("index de mention invalide, soit la liste des mentions est erronnée soit la mention dans le vote est erronée")
if candidat in collect:
if mention in collect[candidat]:
collect[candidat][mention]=collect[candidat][mention]+1
else:
collect[candidat][mention]=1
else:
collect[candidat]={mention:1}
return collect
def arrange_votes_par_candidat(votes,candidats,default_mention):
vpc={}
for candidat in candidats:
vpc[candidat]=[vote[candidat] if candidat in vote else default_mention for vote in votes]
return vpc
def jugement_majoritaire(poll):
verbose=False
crosscheck=True
votants=poll['votants']['decompte']
candidats=poll['candidats']
nombre_candidats=len(candidats)
# l'ordre des mentions est de la meilleure à la pire (reject)
nom_mentions=poll['mentions']
mentions=len(nom_mentions)
default_mention=mentions-1
votes=poll["votation"]["votes"]
if verbose:
print('candidats:' + str(candidats))
print(votes)
warnings=[]
collect=collect_votes(votes,list(candidats),default_mention,warnings)
if verbose:
print(collect)
votant_median_check= votants / 2 if votants % 2 == 0 else ((votants-1) / 2) +1
votant_median=votants // 2 + votants % 2
if votant_median_check != votant_median:
print('[ERROR] le nombre median de votants semble erroné. contactez le developpeur de ce code.' + str(votant_median_check))
merite={}
mention_mediane={}
merite_pourcent={}
votes_par_candidat=arrange_votes_par_candidat(votes,candidats,mentions-1)
# cumul : du meilleur au pire
# range_mentions = range(len(mentions))
# cumul du pire au meilleur
range_mentions = range(mentions-1,-1,-1)
for candidat in list(candidats):
vote=collect[candidat]
cumul=0
m=[0 for i in range(mentions)]
mention_m=None
pourcent=[0 for i in range(mentions)]
for mention in range_mentions:
if mention in vote:
cumul=cumul+vote[mention]
pourcent[mention]= ( 100 * vote[mention] ) / votants
if cumul >= votant_median and mention_m is None:
mention_m=mention
m[mention]=cumul
if cumul < votants:
print('[ERROR] le cumul des votes doit correspondre au nombre de votants. contactez le developpeur de ce code.')
mention_mediane[candidat]=mention_m
merite[candidat]=m
merite_pourcent[candidat]=pourcent
if verbose:
print(merite)
print(mention_mediane)
# les gagnants sont ceux qui ont la mention mediane minimale ( la meilleure )
found=[]
for mention in range(0,mentions):
for candidat,merite_median in mention_mediane.items():
if merite_median == mention:
found.append({candidat:candidats[candidat]})
if len(found) > 0:
break
if verbose:
print(found)
print(nom_mentions[mention])
# expanded=expand_collect(collect,list(candidats),mentions)
if verbose:
print(votes_par_candidat)
result = {'resultat':{'mention':nom_mentions[mention],'candidats':found},'merite':merite,'profil':merite_pourcent,"warnings":warnings}
if crosscheck:
# cross check with a well known implementation
result["crosscheck"]=majority_judgment(votes_par_candidat, reverse=True)
return result

View File

@@ -0,0 +1,124 @@
import json
import sys
from majority_judgment import majority_judgment
verbose=False
def collect_votes(votes,candidates,default_mention,warnings):
collect={}
for vote in votes:
# missing candidate in vote picks the worse one
for candidate in candidates:
if not candidate in vote:
vote[candidate]=default_mention
for candidate,mention in vote.items():
if mention > default_mention or mention < 0:
mention = default_mention
warnings.append("index de mention invalide, soit la liste des mentions est erronnée soit la mention dans le vote est erronée")
if candidate in collect:
if mention in collect[candidate]:
collect[candidate][mention]=collect[candidate][mention]+1
else:
collect[candidate][mention]=1
else:
collect[candidate]={mention:1}
return collect
def arrange_votes_by_candidate(votes,candidates,default_mention):
vpc={}
for candidate in candidates:
vpc[candidate]=[vote[candidate] if candidate in vote else default_mention for vote in votes]
return vpc
def majority_judgment_run(poll):
verbose=False
crosscheck=True
voters=poll['voters']['count']
candidates=poll['candidates']
nombre_candidates=len(candidates)
# l'ordre des mentions est de la meilleure à la pire (reject)
nom_mentions=poll['mentions']
mentions=len(nom_mentions)
default_mention=mentions-1
votes=poll["votation"]["votes"]
if verbose:
print('candidates:' + str(candidates))
print(votes)
warnings=[]
collect=collect_votes(votes,list(candidates),default_mention,warnings)
if verbose:
print(collect)
voter_median_check= voters / 2 if voters % 2 == 0 else ((voters-1) / 2) +1
voter_median=voters // 2 + voters % 2
if voter_median_check != voter_median:
print('[ERROR] le nombre median de voters semble erroné. contactez le developpeur de ce code.' + str(voter_median_check))
merit={}
mention_mediane={}
merit_pourcent={}
votes_by_candidate=arrange_votes_by_candidate(votes,candidates,mentions-1)
# cumul : du meilleur au pire
# range_mentions = range(len(mentions))
# cumul du pire au meilleur
range_mentions = range(mentions-1,-1,-1)
for candidate in list(candidates):
vote=collect[candidate]
cumul=0
m=[0 for i in range(mentions)]
mention_m=None
pourcent=[0 for i in range(mentions)]
for mention in range_mentions:
if mention in vote:
cumul=cumul+vote[mention]
pourcent[mention]= ( 100 * vote[mention] ) / voters
if cumul >= voter_median and mention_m is None:
mention_m=mention
m[mention]=cumul
if cumul < voters:
print('[ERROR] cumulated votes should match voters. Contact this code developer.')
mention_mediane[candidate]=mention_m
merit[candidate]=m
merit_pourcent[candidate]=pourcent
if verbose:
print(merit)
print(mention_mediane)
# les gagnants sont ceux qui ont la mention mediane minimale ( la meilleure )
found=[]
for mention in range(0,mentions):
for candidate,merit_median in mention_mediane.items():
if merit_median == mention:
found.append({candidate:candidates[candidate]})
if len(found) > 0:
break
if verbose:
print(found)
print(nom_mentions[mention])
# expanded=expand_collect(collect,list(candidates),mentions)
if verbose:
print(votes_by_candidatee)
result = {'result':{'mention':nom_mentions[mention],'candidates':found},'merit':merit,'profil':merit_pourcent,"warnings":warnings}
if crosscheck:
# cross check with a well known implementation
result["crosscheck"]=majority_judgment(votes_by_candidate, reverse=True)
return result

View File

@@ -21,9 +21,9 @@ if ($verbose) {
var_dump($poll); var_dump($poll);
} }
$votants=$poll['votants']['decompte']; $voters=$poll['voters']['count'];
$candidats=$poll['candidats']; $candidates=$poll['candidates'];
$nombre_candidats=count($candidats); $nombre_candidates=count($candidates);
# l'ordre des mentions est de la meilleure à la pire (reject) # l'ordre des mentions est de la meilleure à la pire (reject)
$nom_mentions=$poll['mentions']; $nom_mentions=$poll['mentions'];
@@ -33,22 +33,22 @@ $default_mention=$mentions-1;
$votes=$poll["votation"]["votes"]; $votes=$poll["votation"]["votes"];
if ($verbose) { if ($verbose) {
print("candidats\n"); print("candidates\n");
var_dump($candidats); var_dump($candidates);
print("votes:\n"); print("votes:\n");
var_dump($votes); var_dump($votes);
} }
$warnings=[]; $warnings=[];
$default_vote=[]; $default_vote=[];
foreach ($candidats as $candidat => $nom_candidat ) { foreach ($candidates as $candidat => $nom_candidat ) {
$default_vote[$candidat]=$default_mention; $default_vote[$candidat]=$default_mention;
} }
$collect=[]; $collect=[];
foreach ($votes as $vote) { foreach ($votes as $vote) {
# missing candidat in vote is the worts one # missing candidat in vote is the worts one
foreach ($candidats as $candidat => $nom_candidat ) { foreach ($candidates as $candidat => $nom_candidat ) {
if (! array_key_exists($candidat,$vote)) { if (! array_key_exists($candidat,$vote)) {
$vote[$candidat]=$default_mention; $vote[$candidat]=$default_mention;
} }
@@ -78,24 +78,24 @@ if ($verbose) {
print(json_encode($collect)); print(json_encode($collect));
} }
$votant_median_check= ($votants % 2 == 0) ? intdiv($votants,2) : intdiv(($votants-1),2) + 1; $voter_median_check= ($voters % 2 == 0) ? intdiv($voters,2) : intdiv(($voters-1),2) + 1;
$votant_median=intdiv($votants, 2) + $votants % 2; $voter_median=intdiv($voters, 2) + $voters % 2;
if ( $votant_median_check != $votant_median ) { if ( $voter_median_check != $voter_median ) {
print('[ERROR] le nombre median de votants (' . $votant_median_check . '/' . $votant_median . ') semble erroné. contactez le developpeur de ce code.'); print('[ERROR] le nombre median de voters (' . $voter_median_check . '/' . $voter_median . ') semble erroné. contactez le developpeur de ce code.');
} }
$merite=[]; $merit=[];
$mention_mediane=[]; $mention_mediane=[];
$merite_pourcent=[]; $merit_pourcent=[];
# cumul : du meilleur au pire # cumul : du meilleur au pire
# range_mentions = range(count(mentions)) # range_mentions = range(count(mentions))
# cumul du pire au meilleur # cumul du pire au meilleur
$range_mentions = $mentions; $range_mentions = $mentions;
foreach ($candidats as $candidat => $nom_candidat) { foreach ($candidates as $candidat => $nom_candidat) {
$vote=$collect[$candidat]; $vote=$collect[$candidat];
$cumul=0; $cumul=0;
for ($i=0; $i < $range_mentions; $i ++) { for ($i=0; $i < $range_mentions; $i ++) {
@@ -106,24 +106,24 @@ foreach ($candidats as $candidat => $nom_candidat) {
for ($mention=$range_mentions -1; $mention >=0; $mention --) { for ($mention=$range_mentions -1; $mention >=0; $mention --) {
if ( array_key_exists($mention,$vote) ) { if ( array_key_exists($mention,$vote) ) {
$cumul=$cumul+$vote[$mention]; $cumul=$cumul+$vote[$mention];
$pourcent[$mention]= ( 100 * $vote[$mention] ) / $votants; $pourcent[$mention]= ( 100 * $vote[$mention] ) / $voters;
} }
if ($cumul >= $votant_median and $mention_m == null ) { if ($cumul >= $voter_median and $mention_m == null ) {
$mention_m=$mention; $mention_m=$mention;
} }
$m[$mention]=$cumul; $m[$mention]=$cumul;
} }
if ($cumul < $votants) { if ($cumul < $voters) {
print("[ERROR] le cumul des votes " . $cumul . " doit correspondre au nombre de votants. contactez le developpeur de ce code."); print("[ERROR] votes cumulated " . $cumul . " should match voters. Contact this code developer.");
} }
$mention_mediane[$candidat]=$mention_m; $mention_mediane[$candidat]=$mention_m;
$merite[$candidat]=$m; $merit[$candidat]=$m;
$merite_pourcent[$candidat]=$pourcent; $merit_pourcent[$candidat]=$pourcent;
} }
if ($verbose) { if ($verbose) {
print("\nmerite:\n"); print("\nmerit:\n");
print(json_encode($merite)); print(json_encode($merit));
print("\nmention_mediane:\n"); print("\nmention_mediane:\n");
print(json_encode($mention_mediane)); print(json_encode($mention_mediane));
} }
@@ -132,10 +132,10 @@ if ($verbose) {
$found=[]; $found=[];
for ($mention = 0; $mention < $mentions; $mention ++) { for ($mention = 0; $mention < $mentions; $mention ++) {
foreach ($mention_mediane as $candidat => $merite_median) foreach ($mention_mediane as $candidat => $merit_median)
if ($merite_median == $mention) { if ($merit_median == $mention) {
$item=[]; $item=[];
$item[$candidat]=$candidats[$candidat]; $item[$candidat]=$candidates[$candidat];
array_push($found,$item); array_push($found,$item);
} }
if (count($found) > 0) { if (count($found) > 0) {
@@ -148,7 +148,7 @@ if ($verbose) {
print($nom_mentions[$mention]); print($nom_mentions[$mention]);
} }
$result = ['resultat'=> ['mention'=>$nom_mentions[$mention],'candidats' => $found],'merite' => $merite,'profil' => $merite_pourcent,"warnings" => $warnings]; $result = ['result'=> ['mention'=>$nom_mentions[$mention],'candidates' => $found],'merit' => $merit,'profil' => $merit_pourcent,"warnings" => $warnings];
print(json_encode($result)); print(json_encode($result));

View File

@@ -1,6 +1,6 @@
import json import json
import sys import sys
from jugement_majoritaire import jugement_majoritaire from majority_judgment_method import majority_judgment_run
if len(sys.argv) > 1: if len(sys.argv) > 1:
poll_file=sys.argv[1] poll_file=sys.argv[1]
@@ -16,6 +16,6 @@ with open(poll_file,'r') as poll_fd:
if verbose: if verbose:
print(poll) print(poll)
result = jugement_majoritaire(poll) result = majority_judgment_run(poll)
print(json.dumps(result)) print(json.dumps(result))

View File

@@ -1,9 +1,7 @@
# contexte # contexte
Choix des noms de mentions Choix des noms de mentions
https://fr.wikipedia.org/wiki/Jugement_majoritaire https://fr.wikipedia.org/wiki/Jugement_majoritaire
vs vs
https://en.wikipedia.org/wiki/Jugement_majoritaire https://en.wikipedia.org/wiki/Jugement_majoritaire

1
lib Symbolic link
View File

@@ -0,0 +1 @@
nextcloud_devenv/lib

View File

@@ -1,8 +1,16 @@
# Nextcloud Dev En # Nextcloud Dev Environment for Poll
Two options
one created manualy with a simple podman and a local database
one fully under podman, done on nextcloud conference
# Option 1 / simple podman
Create a dev env for nextcloud Create a dev env for nextcloud
setup databse : setup database :
``` ```
./setupdatabase.sh ./setupdatabase.sh
@@ -29,7 +37,7 @@ puis
./sync_poll.sh ./sync_poll.sh
``` ```
il reste encore a compiler avec les dependances composer et le build npm vuejs il reste encore a compiler avec les dépendances composer et le build npm vuejs
``` ```
./enter_www_data.sh ./enter_www_data.sh
@@ -63,3 +71,50 @@ utiliser la base de donnee postgres et non le SQLLite ?
./migrate_db_sqlite2postgres.sh ./migrate_db_sqlite2postgres.sh
# Option 2 : nextcloud-docker-dev
setup
```
git clone git@github.com:juliusknorr/nextcloud-docker-dev.git
git git@github.com:nextcloud/server.git
```
check nextcloud-docker-dev.env then
```
cp nextcloud-docker-dev.env nextcloud-docker-dev.git/.env
```
https setup, local private PKI.
missing setup cert and setupcert host ... mkcert compilation + install
update-certs update-hosts
https://nextcloud-dev.l0g.eu
then will be done as sudo ( RootLess or podman limitation ? )
```
./runit.sh dryrun up
```
start
```
./runit.sh up
```
stop
```
./runit.sh down
```
Pour les mails : https://mail-dev.l0g.eu/#
un MailHog est installé !

View File

@@ -11,5 +11,13 @@ flavor=dev
database_name=nextcloud_$flavor database_name=nextcloud_$flavor
username=nextcloud_$flavor username=nextcloud_$flavor
# postgres
db_port=5432 db_port=5432
db_hostname=127.0.0.1 db_hostname=127.0.0.1
dbtype_nextcloude=pgsql
# mariadb/mysql
db_port=3306
db_hostname=127.0.0.1
dbtype_nextcloud=mysql

View File

@@ -5,25 +5,44 @@
# [[ -f $log_functions ]] || { echo "[FATAL] Missing $log_functions" >&2 ; exit 1 ;} # [[ -f $log_functions ]] || { echo "[FATAL] Missing $log_functions" >&2 ; exit 1 ;}
# source $log_functions # source $log_functions
metalog_color_start() {
if [[ -n $metalog_color ]]
then
echo -en "${metalog_color}"
fi
}
metalog_color_stop() {
if [[ -n $metalog_color ]]
then
echo -en "\033[0m"
fi
}
log_any() log_any()
{ {
priority=$1 priority=$1
shift shift
metalog_color_start
echo "[$priority] $@" >&2 echo "[$priority] $@" >&2
metalog_color_stop
} }
log_fatal() log_fatal()
{ {
local metalog_color=$metalog_color_error
log_any FATAL "$*" log_any FATAL "$*"
} }
log_error() log_error()
{ {
local metalog_color=$metalog_color_error
log_any ERROR "$*" log_any ERROR "$*"
} }
log_warn() log_warn()
{ {
local metalog_color=$metalog_color_warning
log_any WARN "$*" log_any WARN "$*"
} }
@@ -32,59 +51,36 @@ log_info()
log_any INFO "$*" log_any INFO "$*"
} }
log_success()
{
local metalog_color=$metalog_color_success
log_info "$*"
}
log_debug() log_debug()
{ {
local metalog_color=$metalog_color_info
[[ -n $debug ]] && log_any DEBUG "$*" [[ -n $debug ]] && log_any DEBUG "$*"
} }
deferpipe()
{
cat
echo "# <previous line> | $@"
}
autoquoteargs()
{
echo -n "$1"
shift
while [[ $# > 0 ]]
do
if [[ "$1" =~ [\ \$] ]]
then
echo -n " '$1'"
else
echo -n " $1"
fi
shift
done
echo
}
echoarray()
{
declare -a arr=("${@}")
declare -i len=${#arr[@]}
# Show passed array
for ((n = 0; n < len; n++))
do
echo -en " \"${arr[$n]}\""
done
echo
}
include_source()
{
if [[ -f $1 ]]
then
source $1
else
log_fatal "Missing $1 script"
exit 1
fi
}
verbose() verbose()
{ {
[[ -n $verbose ]] && log_any $verbose $@ [[ -n $verbose ]] && log_any $verbose $@
} }
metalog_no_colors()
{
metalog_color_info=
metalog_color_success=
metalog_color_error=
metalog_color_warning=
}
# default colors
metalog_default_colors()
{
metalog_color_info="\033[38;5;79m"
metalog_color_success="\033[1;32m"
metalog_color_error="\033[1;31m"
metalog_color_warning="\033[1;34m"
}

View File

@@ -1,30 +1,83 @@
#!/bin/bash #!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# metascript to include # metascript to include
# #
# usual way if no parameters used by outer script : # to generate file header for caller script do :
# # bash <relative_path_to>/metascript.sh header
# source $(dirname "$0")/metascript.sh # ex:
# # bash lib/metascript.sh header
# defaultmetainit $@
# metascript_version=v1.1.0
# if called directly
if [[ "$0" =~ ^(.*)/metascript.sh$ ]]
then
prefix=${BASH_REMATCH[1]}
case $1 in
header)
cat <<EOF
#!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
# header generated by $0 $1
# metascript_version=$metascript_version
toolsdir=$prefix
pushd \$toolsdir >/dev/null
toolsdir=\$(pwd)
source metascript.sh
popd >/dev/null
metarun=metarun
# change default to sudo if needed
# metasudo=sudo
while [[ \$# > 0 ]]
do
case "\$1" in
*)
parsemetaarg "\$1"
;;
esac
shift
done
usage
EOF
;;
version)
echo $metascript_version
;;
*)
echo "[WARNING] only header and version arguments are supported. (prefix=$prefix), this script is intended to be included not to be directly called." >&2
exit 1
esac
exit 0
fi
if [[ "$metascript_included" == "yes" ]]
then
echo "[WARNING] metascript already included ($0)" >&2
else
metascript_included=yes metascript_included=yes
# assume all tools are in lib/ # at this step $(pwd) should be this of metascript.sh
toolsdir=$(dirname $(readlink -f $0))/lib # while $0 is this of caller script using metascript
# all tools resources are relative to this directory
# project directory if [[ -z $toolsdir ]]
# relative then
# toolsparentdir=$(realpath --relative-to "$(pwd)" $(readlink -f $0))/ # assume all tools are in lib/
# absolute toolsdir=$(dirname $(readlink -f $0))/lib
toolsparentdir=$(realpath $(readlink -f $0))/ # all tools resources are relative to this directory
fi
if [[ -z $log_functions ]] if [[ -z $log_functions ]]
then then
log_functions=$toolsdir/log_functions.sh log_functions=$toolsdir/log_functions.sh
[[ -f $log_functions ]] || { echo "[FATAL] Missing $log_functions" >&2 ; exit 1 ;} [[ -f $log_functions ]] || { echo "[FATAL] Missing $log_functions , toolsdir=$toolsdir script=$0" >&2 ; exit 1 ;}
source $log_functions source $log_functions
fi fi
@@ -33,28 +86,100 @@ metascript_usage()
cat <<EOF >&2 cat <<EOF >&2
metascript commands : metascript commands :
help|usage help or usage of this {$0} tool help|usage help or usage of this {$0} tool. stop after usage display.
dryrun|show|showdoc display what should/will be done dryrun|show|showdoc display what should/will be done
defersource= script file defining defer() non sandard function defersource= script file defining defer() non standard function
defer= defer function to use, default is showdoc defer= defer function to use, default is showdoc
toolsresourcesdir= where to pick resource toolsresourcesdir= where to pick resource
default to parent of script $toolsresourcesdir default to parent of script $toolsresourcesdir
mostly used with dryrun mostly used with dryrun
apply default : will actual do work without defer apply default : will actual do work without defer
text_format=md set text format output to markdown
EOF EOF
} }
md_output_init() {
md_quote='```'
}
usage() usage()
{ {
echo "[WARNING} no specific usage function for {$0}, to improve by developer" >&2 log_warn "no specific usage function for {$0}, to improve by developer"
echo >&2 echo >&2
metascript_usage metascript_usage
} }
showinfo() showinfo()
{ {
echo $@ echo "$@"
}
metarun()
{
$defer $metasudo "$@"
return 0
}
metasudo_auto()
{
if [[ $EUID -eq 0 ]]
then
log_info "already effective user id as root, don't require sudo"
else
metasudo=sudo
fi
}
autoquoteargs()
{
echo -n "$1"
shift
while [[ $# > 0 ]]
do
if [[ "$1" =~ [\ \$] ]]
then
echo -n " '$1'"
else
echo -n " $1"
fi
shift
done
}
echoarray()
{
declare -a arr=("${@}")
declare -i len=${#arr[@]}
# Show passed array
for ((n = 0; n < len; n++))
do
echo -en " \"${arr[$n]}\""
done
echo
}
start_quote() {
[[ -n $md_quote ]] && echo "$md_quote"
}
end_quote() {
[[ -n $md_quote ]] && echo "$md_quote"
}
deferpipe()
{
cat
echo "# <previous line> | $@"
}
include_source()
{
local script="$1"
enforce var script
source "$script"
} }
showdoc() showdoc()
@@ -62,12 +187,12 @@ showdoc()
if [[ $1 =~ ^tools/ ]] if [[ $1 =~ ^tools/ ]]
then then
# assumes it handles ENV_METASCRIPT_DEFER # assumes it handles ENV_METASCRIPT_DEFER
$@ "$@"
else else
echo '```' start_quote
autoquoteargs $@ autoquoteargs "$@"
echo echo
echo '```' end_quote
fi fi
} }
@@ -77,57 +202,149 @@ redirectto()
if [[ -n $defer ]] if [[ -n $defer ]]
then then
echo "Copy to $tofile" echo "Copy to $tofile"
echo '```' start_quote
cat cat
echo '```' end_quote
else else
cat > $tofile cat > $tofile
fi fi
} }
execredirectfrom() redirectappendto()
{ {
tofile=$1 tofile=$1
shift
if [[ -n $defer ]] if [[ -n $defer ]]
then then
echo '```' echo "Append to $tofile"
autoquoteargs $@ start_quote
echo ' < '"$tofile" cat
echo '```' end_quote
else else
$@ < $tofile cat >> $tofile
fi
}
execredirectfrom()
{
fromfile="$1"
shift
enforcefile "$fromfile" exists
if [[ -n $defer ]]
then
start_quote
autoquoteargs "$@"
echo ' < '"$fromfile"
end_quote
else
"$@" < $fromfile
fi fi
} }
execredirectto() execredirectto()
{ {
tofile=$1 tofile="$1"
shift shift
if [[ -n $defer ]] if [[ -n $defer ]]
then then
echo '```' start_quote
autoquoteargs $@ autoquoteargs "$@"
echo ' > '"$tofile" echo ' > '"$tofile"
echo '```' end_quote
else else
$@ > $tofile "$@" > $tofile
fi fi
} }
defer_exec_set_var()
{
tovar="$1"
shift
cat <<EOF
$md_quote
$tovar=\$($(autoquoteargs "$@"))
$md_quote
EOF
}
execredirecttoroot()
{
tofile="$1"
shift
if [[ -n $defer ]]
then
start_quote
autoquoteargs "$@"
echo ' | sudo tee '"$tofile"' >/dev/null'
end_quote
else
"$@" | sudo tee $tofile >/dev/null
fi
}
execredirectas_to()
{
while [[ $# > 0 ]]
do
case $1 in
user=*)
if [[ "$1" =~ ^user=([a-z]+[-_a-z0-9]*)$ ]]
then
asuser=${BASH_REMATCH[1]}
else
log_fatal "execredirecttouser missing user= in '$@'"
[[ -n $defer ]] || exit 1
fi
;;
chroot=*)
aschroot=${1/chroot=}
;;
*)
tofile=$1
shift
break
esac
shift
done
local command=()
if [[ -n $aschroot ]]
then
command=(sudo chroot)
if [[ -n $asuser ]]
then
# no group yet
command+=(--userspec=$asuser:$asuser)
fi
command+=($aschroot)
elif [[ -n $asuser ]]
then
command=(sudo -u $asuser)
fi
command+=(tee $tofile)
if [[ -n $defer ]]
then
start_quote
autoquoteargs "$@"
echo ' | '"${command[@]}"' >/dev/null'
end_quote
else
"$@" | "${command[@]}" >/dev/null
fi
}
pipeto() pipeto()
{ {
if [[ -n $defer ]] if [[ -n $defer ]]
then then
echo '```' echo "$md_quote"
echo -n 'cat << EOF| ' echo -n 'cat << EOF| '
autoquoteargs $@ autoquoteargs "$@"
echo echo
cat cat
echo "EOF" echo "EOF"
echo '```' echo "$md_quote"
else else
cat | $@ cat | "$@"
fi fi
} }
@@ -217,129 +434,6 @@ check_missing_dest_dir()
fi fi
} }
sed_substitute_expr()
{
local from="$1"
shift
local to="$1"
shift
local s='/'
if [[ $from =~ [\\] ]]
then
# escape char \ should be doubled
from=${from//\\/\\\\}
fi
if [[ $from =~ $s ]]
then
# echo "[ERROR] character $s is prohibited due to sed usage" >&2
from=${from//$s/\\$s}
fi
if [[ $from =~ \[ ]]
then
from=${from//\[/\\\[}
fi
if [[ $from =~ \* ]]
then
from=${from//\*/\\\*}
fi
if [[ $from =~ ^(.*)\$$ ]]
then
from=${BASH_REMATCH[1]}'\$'
fi
if [[ $from =~ ^\^(.*)$ ]]
then
from='\^'${BASH_REMATCH[1]}
fi
if [[ $to =~ [\\] ]]
then
# escape char \ should be doubled
to=${to//\\/\\\\}
fi
if [[ $to =~ $s ]]
then
# echo "[ERROR] character $s is prohibited due to sed usage" >&2
# echo "This is a limitation of metascript.sh script, replaced by \$s" >&2
to=${to//$s/\\$s}
fi
if [[ $to =~ [\&] ]]
then
# echo "[ERROR] character & is prohibited due to sed usage" >&2
to=${to//\&/\\\&}
fi
# replace it globaly
echo "s$s$from$s$to${s}g"
}
sedreplacefromto()
{
local from="$1"
local to="$2"
shift 2
local sedexpr="$1"
execredirectto $to sed "$sedexpr" $from
shift
while [[ $# > 0 ]]
do
sedexpr="$1"
$defer sed -i "$sedexpr" $to
shift
done
}
replacefromto()
{
local from="$1"
local to="$2"
shift 2
if [[ -n $defer ]]
then
$defer "replace $@ from '$from' into '$to'"
else
local sedexpr=$(sed_substitute_expr "$1" "$2")
execredirectto $to sed "$sedexpr" $from
shift 2
while [[ $# > 0 ]]
do
sedexpr=$(sed_substitute_expr "$1" "$2")
$defer sed -i "$sedexpr" $to
shift 2
done
fi
}
sedreplacein()
{
local file=$1
shift
while [[ $# > 0 ]]
do
$defer sed -i "$1" $file
shift
done
}
replacein()
{
local infile=$1
shift
if [[ -n $defer ]]
then
$defer "replace $@ into '$infile'"
else
while [[ $# > 0 ]]
do
sedexpr=$(sed_substitute_expr "$1" "$2")
$defer sed -i "$sedexpr" $infile
shift 2
done
fi
}
parsemetaarg() parsemetaarg()
{ {
case $1 in case $1 in
@@ -355,8 +449,15 @@ parsemetaarg()
dryrun|show|showdoc) dryrun|show|showdoc)
defer=showdoc defer=showdoc
;; ;;
debug)
set -ex
;;
metasudo=*)
metasudo=${1/metasudo=}
;;
help|usage) help|usage)
usage usage
exit 0
;; ;;
toolsresourcesdir=*) toolsresourcesdir=*)
toolsresourcesdir=${1/toolsresourcesdir=/} toolsresourcesdir=${1/toolsresourcesdir=/}
@@ -364,6 +465,9 @@ parsemetaarg()
scl_enable=*) scl_enable=*)
scl_args=(scl enable ${1/scl_enable=/} --) scl_args=(scl enable ${1/scl_enable=/} --)
;; ;;
text_format=md)
md_output_init
;;
*) *)
log_error "unrecognized argument '$1'" log_error "unrecognized argument '$1'"
usage usage
@@ -380,28 +484,82 @@ enforcearg()
if [[ -z $value ]] if [[ -z $value ]]
then then
log_error "{0} expect '$var' to be set ex $var=$default" log_error "{$0} expect '$var' to be set ex $var=$default"
usage if [[ -n $defer ]]
exit 1 then
[[ -z $default ]] && default="DEFAULT"
log_warn "in defer/dryrun force $var=$default"
eval "$var=$default"
else
usage
exit 1
fi
fi fi
} }
enforce()
{
objecttype=$1
shift
case $objecttype in
file|dir)
object=$1
shift
constraint=$1
case $constraint in
exists|does_not_exist|create_if_needed|well_named)
enforce${objecttype} "$object" "$@"
;;
*)
log_error "enforce $objecttype '$object' $@"
log_fatal "constraint '$constraint' unsupported. currently known (exists|does_not_exist|create_if_needed) Please fix the code."
exit 1
;;
esac
;;
var|arg)
enforcearg "$@"
;;
user)
enforceuser "$@"
;;
*)
log_error "enforce $objecttype $@"
log_fatal "objectttype $objecttype unsupported"
exit 1
;;
esac
}
enforcefile() enforcefile()
{ {
file="$1" local file="$1"
constraint="$2" local constraint="$2"
if [[ $constraint == well_named ]]
then
if [[ "$file" =~ /$ ]]
then
log_error "'$file' as a file should not end with a '/'"
[[ -n $defer ]] || exit 1
fi
fi
if [[ ! -f "$file" ]] if [[ ! -f "$file" ]]
then then
if [[ $constraint = exists ]] case $constraint in
then exists)
log_error "[ERROR] Missing expected $file" log_error "Missing expected $file"
[[ -n $defer ]] || exit 1 [[ -n $defer ]] || exit 1
fi ;;
create_if_needed)
$metarun touch $file
;;
esac
else else
if [[ $constraint = does_not_exists ]] if [[ $constraint = does_not_exist ]]
then then
log_error "[ERROR] '$file' already exists. Move it away" log_error "'$file' already exists. Move it away"
[[ -n $defer ]] || exit 1 [[ -n $defer ]] || exit 1
fi fi
fi fi
@@ -409,33 +567,53 @@ enforcefile()
enforcedir() enforcedir()
{ {
dir="$1" local dir="$1"
constraint="$2" local constraint="$2"
if [[ $constraint = does_not_exist ]] if [[ $constraint == well_named ]]
then then
if [[ -e $dir ]] if [[ ! "$dir" =~ /$ ]]
then then
log_error "'$dir' already exists. Move it away" log_error "'$dir' should end with a '/'"
[[ -n $defer ]] || exit 1 [[ -n $defer ]] || exit 1
fi fi
fi fi
if [[ $constraint = exists ]] if [[ -e $dir ]]
then then
if [[ ! -d "$dir" ]] case $constraint in
then does_not_exist)
if [[ -e "$dir" ]] if [[ -d "$dir" ]]
then then
log_error "'$dir' already exists but is not a directory as expected" log_error "'$dir' already exists"
[[ -n $defer ]] || exit 1 [[ -n $defer ]] || exit 1
fi else
log_error "[ERROR] Missing expected directory '$dir'" log_error "'$dir' already exists and is not a directory as expected"
[[ -n $defer ]] || exit 1 [[ -n $defer ]] || exit 1
fi fi
;;
create_if_needed|exists)
if [[ ! -d "$dir" ]]
then
log_error "'$dir' already exists and is not a directory as expected"
[[ -n $defer ]] || exit 1
fi
;;
esac
else
case $constraint in
exists)
log_error "Missing expected directory $dir"
[[ -n $defer ]] || exit 1
;;
create_if_needed)
if [[ ! -d "$dir" ]]
then
$metarun mkdir -p "$dir"
fi
;;
esac
fi fi
} }
applymetaargs() applymetaargs()
@@ -528,7 +706,7 @@ check_variable_in()
shift 2 shift 2
local value="" local value=""
local values=$@ local values="$@"
eval value='$'"$var" eval value='$'"$var"
@@ -570,17 +748,222 @@ get_timestamp_second()
echo "$(date +"%Y%m%d%H%M%S")" echo "$(date +"%Y%m%d%H%M%S")"
} }
if [[ -z $ENV_METASCRIPT_RESOURCESDIR ]] todo()
{
log_any TODO "$@"
}
get_resource_var() {
local varname="$1"
local default_value="$2"
eval value='$'"$varname"
if [[ -z $value ]]
then
echo "# ($0:metascript.sh:$LINENO) $(date)" >>$collect_context
if [[ -z $default_value ]]
then
log_error "resource $varname does not exists and no non empty default provided"
echo "# $varname=<MISSING>" >>$collect_context
exit 1
fi
log_warn "($0) Using default value '$varname'='$default_value' HARDCODED in script : should be fixed with proper default file resources.var, see traces in $collect_context"
read "$varname" <<<"$default_value"
echo "$varname=$default_value" >>$collect_context
fi
}
setup_resources_var()
{
local resource_var="$1"
enforcefile "$resource_var" exists
if [[ -f $resource_var ]]
then
while read line
do
if [[ $line =~ ^([a-zA-Z0-9_]+)=(.+)$ ]]
then
varname=${BASH_REMATCH[1]}
value=${BASH_REMATCH[2]}
read "$varname" <<<"$value"
elif [[ $line =~ ^# ]]
then
if [[ -n $defer ]]
then
$defer : "$line"
fi
else
log_warn "Invalid syntax in $resource_var '$line' does not match any expected expression"
fi
done <"$resource_var"
fi
}
# allow to check mounted points
mountpoint_get_device()
{
local mount_point="$1"
mount_point=$(readlink -f "$mount_point")
$defer awk "{ if (\$2 == \""$mount_point"\") print \$1 ;}" /proc/mounts
}
enforce_device() {
local device=$1
local constraint=$2
if [[ -n $defer ]]
then
$defer "mountpoint=$(device_get_mountpoints $device)"
else
mountpoint=$(device_get_mountpoints $device)
fi
if [[ -n $mountpoint ]] && [[ $constraint == "unmounted" ]]
then
log_error "$device seen as mounted on $mountpoint"
$defer exit 1
elif [[ -z $mountpoint ]] && [[ $constraint == "mounted" ]]
then
log_error "$device not seen as mounted"
$defer exit 1
fi
}
device_get_mountpoints()
{
local device="$1"
$defer awk "{ if (\$1 == \""$device"\") print \$2 ;}" /proc/mounts
}
mount_if_needed()
{
local mountdevice="$1"
local mountpoint="$2"
local param="$3"
if [[ -n $defer ]]
then
$defer 'device=$(mountpoint_get_device '"$mountpoint"')'
else
device=$(mountpoint_get_device "$mountpoint")
fi
enforcedir "$mountpoint" exists
# device is a device ...
# enforcefile "$mountdevice" exists
if [[ -z $device ]]
then
$metarun mount $param "$mountdevice" "$mountpoint"
elif [[ "$device" == "$mountdevice" ]]
then
log_info "$device already mounted on $mountdevice"
else
log_warn "Another device $device is mounted on $mountpoint, not $mountdevice"
fi
}
umount_if_needed()
{
local mountdevice="$1"
local mountpoint="$2"
local param="$3"
device=$(mountpoint_get_device "$mountpoint")
if [[ -n "$device" ]]
then
if [[ "$device" == "$mountdevice" ]]
then
$metarun umount "$mountpoint"
else
log_warn "Another device $device is mounted on $mountpoint, not $mountdevice"
if [[ $param == '--bind' ]]
then
log_warn "Unmouning ANYWAY ( mount --bind show root device, not mounted directory )"
$metarun umount "$mountpoint"
fi
fi
else
log_warn "no device found mounted for umount_if_needed $@"
fi
}
exec_bg()
{
if [[ -z $defer ]]
then
"$@" &
else
$defer "$@" '&'
fi
}
notice()
{
case "$1" in
git_commit)
log_warn "$1 should be $2"
;;
*)
log_fatal "unrecognized notice '$1'"
exit 2
;;
esac
}
enforceuser()
{
local expecteduser="$1"
if [[ "$USER" != "$expecteduser" ]]
then
log_error "expected user $expecteduser is not current user $USER"
exit 1
fi
}
# collect all hardcoded values.
mkdir -p ~/.artlog
collect_context=~/.artlog/collect_context.var
if [[ -z $toolsresourcesdir ]]
then then
toolsresourcesdir=$toolsparentdir # project directory
else
toolsresourcesdir=$ENV_METASCRIPT_RESOURCESDIR # from current $(pwd) will follow parent dir hierarchy to find resources.var
dir="$(pwd)"
while [[ -n $dir ]] && [[ -d $dir ]] && [[ ! -f $dir/resources.var ]]
do
new_dir=$(dirname "$dir")
if [[ $new_dir == $dir ]]
then
# protect against infinite loop
break
fi
dir="$new_dir"
done
if [[ -f $dir/resources.var ]]
then
toolsresourcesdir=$dir
else
if [[ -z $ENV_METASCRIPT_RESOURCESDIR ]]
then
toolsresourcesdir=$toolsparentdir
else
toolsresourcesdir=$ENV_METASCRIPT_RESOURCESDIR
fi
fi
fi fi
# quick way to give scl patches to fill scl_arg array resources_var=$toolsresourcesdir/resources.var
if [[ -f $toolsparentdir/.scl_env ]] if [[ -f $resources_var ]]
then then
source $toolsparentdir/.scl_env setup_resources_var "$resources_var"
else
log_warn "No $resources_var found"
fi
# quick way to give scl patches to fill scl_arg array
if [[ -f $toolsresourcesdir/.scl_env ]]
then
source $toolsresourcesdir/.scl_env
fi fi
# empty defer means doit # empty defer means doit
@@ -588,6 +971,11 @@ defer=$ENV_METASCRIPT_DEFER
showinfo=showinfo showinfo=showinfo
allparms=$@ allparms="$@"
applymetaargs=applymetaargs applymetaargs=applymetaargs
metarun=$defer
# metascript included
fi

4
nextcloud_devenv/logs.sh Normal file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
nextcloud_storage=/var/wwww/http/data
sudo -u www-data tail -f $nextcloud_storage/nextcloud.log

View File

@@ -15,10 +15,16 @@ log_info "See https://docs.nextcloud.com/server/latest/admin_manual/configuratio
source ./config_db_query.sh source ./config_db_query.sh
# convert localhost of host into container virtual brdige hostname mapping # convert localhost of host into container virtual bridge hostname mapping
#db_hostname=host.docker.internal #db_hostname=host.docker.internal
#db_hostname=host.containers.internal #db_hostname=host.containers.internal
# host loopback mapped address # host loopback mapped address
db_hostname=10.1.1.13 db_hostname=10.1.1.13
# dbtype_nextcloud inherited from config_db_query
$defer ./run_occ.sh db:convert-type --password="$password" --port="$db_port" --all-apps pgsql "$username" "$db_hostname" "$database_name" $defer ./run_occ.sh db:convert-type --password="$password" --port="$db_port" --all-apps "$dbtype_nextcloud" "$username" "$db_hostname" "$database_name"
if [[ $dbtype_nextcloud == mysql ]]
then
$defer ./run_occ.sh config:system:set mysql.utf8mb4 --type boolean --value="true"
fi

View File

@@ -0,0 +1,72 @@
COMPOSE_PROJECT_NAME=master
# Default protocol to use for Nextcloud and other containers
# check the readme for details how to setup https
PROTOCOL=https
# Paths
REPO_PATH_SERVER=/home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/nextcloud_devenv/server
# Specify a path to apps which will be shared between all containers. Useful for apps that support multiple nextcloud versions
# ADDITIONAL_APPS_PATH=/home/alice/nextcloud-docker-dev/workspace/server/apps-shared
# Stable releases root directory
# STABLE_ROOT_PATH=/home/alice/nextcloud-docker-dev/workspace
# Install Nextcloud apps per default
# NEXTCLOUD_AUTOINSTALL_APPS="viewer activity"
# Retry enabling apps for a provided amount of time (can be useful when using the containers in CI)
# NEXTCLOUD_AUTOINSTALL_APPS_WAIT_TIME=0
# Blackfire configuration
# BLACKFIRE_CLIENT_ID=
# BLACKFIRE_CLIENT_TOKEN=
# BLACKFIRE_SERVER_ID=
# BLACKFIRE_SERVER_TOKEN=
# By default the published ports are only accessible at 127.0.0.1 (your localhost).
# Set this to '0.0.0.0' to make them accessible from your whole local network.
# IP_BIND=127.0.0.1
# can be used to run separate setups besides each other
# DOCKER_SUBNET=192.168.15.0/24
# PORTBASE=815
# Main dns names for ssl proxy
# This can be used to append a custom domain name to the container names
DOMAIN_SUFFIX=-dev.l0g.eu
# May be used to set the PHP version. Defaults to 7.2.
# PHP_VERSION=71
# PHP_VERSION=72
# PHP_VERSION=73
# PHP_VERSION=74
# PHP_VERSION=80
PHP_VERSION=81
# May be used to choose database (sqlite, pgsql, mysql)
SQL=mysql
# The mode of the xdebuger extention. This can be a comma separated list of
# the entries none, develop, debug, trace, and profile.
PHP_XDEBUG_MODE=develop
# Docker socket location, use it when you run rootless Docker
# Replace "1000" with the uid used by your Docker engine (default $(id -u))
# DOCKER_SOCKET=/run/user/1000/docker.sock
DOCKER_SOCKET=/var/run/podman/podman.sock
# Nextcloud AppAPI Docker Socket Proxy
# ------------------------------------
# NC_HAPROXY_PASSWORD=some_secure_password
# BIND_ADDRESS=172.17.0.1
# CERT_PATH=./data/ssl/app_api/app_api.pem
# NETWORK_MODE=host
# HAPROXY_PORT=2375
# TIMEOUT_CONNECT=10s
# TIMEOUT_CLIENT=30s
# TIMEOUT_SERVER=30s
# EX_APPS_NET=ipv4@localhost
# EX_APPS_COUNT=50
# ------------------------------------

View File

@@ -0,0 +1,6 @@
#!/bin/bash
./get_dbcontent.sh
./run_occ.sh polls:db:purge
./run_occ.sh app:enable polls
./run_occ.sh polls:db:rebuild

View File

@@ -0,0 +1,7 @@
See README.md Option 2
./runit.sh dryrun up
./runit.sh dryrun down
Please remove dryrun to actualy do it.

54
nextcloud_devenv/runit.sh Executable file
View File

@@ -0,0 +1,54 @@
#!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
#
toolsdir=lib
pushd $toolsdir >/dev/null
toolsdir=$(pwd)
source metascript.sh
popd >/dev/null
usage() {
cat runit.README.md
}
while [[ $# > 0 ]]
do
case "$1" in
up|down)
action=$1
;;
*)
parsemetaarg "$1"
;;
esac
shift
done
if [[ -z $action ]]
then
log_warn "Missing arguments"
usage
exit 0
fi
metarun=metarun
metasudo=sudo
enforcedir nextcloud-docker-dev exists
pushd nextcloud-docker-dev
if [[ $action = down ]]
then
$metarun podman-compose down
fi
services=(nextcloud proxy database-mysql)
if [[ $action == up ]]
then
$metarun podman-compose --env-file $(pwd)/.env $action "${services[@]}"
fi
popd

View File

@@ -20,4 +20,14 @@ GRANT ALL PRIVILEGES ON SCHEMA public TO $username;
EOF EOF
} | pipeto sudo -u postgres psql -f - } | pipeto sudo -u postgres psql -f -
{
cat <<EOF
CREATE USER '$username'@'localhost' IDENTIFIED BY '$password';
CREATE DATABASE IF NOT EXISTS ${database_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
GRANT ALL PRIVILEGES on ${database_name}.* to '$username'@'localhost';
EOF
} | pipeto sudo mariadb -uroot -p
log_info "created database ${database_name} with user $username" log_info "created database ${database_name} with user $username"

1
samples/0.json Normal file
View File

@@ -0,0 +1 @@
{"candidates": {"A": "A", "B": "B", "C": "C"}, "voters": {"count": 4}, "mentions": ["Tr\u00e8s Bien", "Bien", "Assez Bien", "Passable", "Insuffisant", "A Rejeter"], "votation": {"votes": [{"A": 0, "B": 1, "C": 2}, {"A": 0, "B": 1, "C": 2}, {"A": 0, "B": 1, "C": 2}, {"A": 0, "B": 1, "C": 2}]}}

View File

@@ -1,5 +1,5 @@
{ {
"candidats":{ "candidates":{
"A":"albert", "A":"albert",
"B":"Beatrice", "B":"Beatrice",
"C":"Chloé", "C":"Chloé",
@@ -9,8 +9,8 @@
"G":"Gisèle", "G":"Gisèle",
"H":"Hugo" "H":"Hugo"
}, },
"votants":{ "voters":{
"decompte":10 "count":10
}, },
"mentions":[ "mentions":[
"Très Bien", "Très Bien",

View File

@@ -1,5 +1,5 @@
{ {
"candidats":{ "candidates":{
"A":"albert", "A":"albert",
"B":"Beatrice", "B":"Beatrice",
"C":"Chloé", "C":"Chloé",
@@ -9,8 +9,8 @@
"G":"Gisèle", "G":"Gisèle",
"H":"Hugo" "H":"Hugo"
}, },
"votants":{ "voters":{
"decompte":10 "count":10
}, },
"mentions":[ "mentions":[
"Très Bien", "Très Bien",

View File

@@ -1,5 +1,5 @@
{ {
"candidats":{ "candidates":{
"A":"albert", "A":"albert",
"B":"Beatrice", "B":"Beatrice",
"C":"Chloé", "C":"Chloé",
@@ -9,8 +9,8 @@
"G":"Gisèle", "G":"Gisèle",
"H":"Hugo" "H":"Hugo"
}, },
"votants":{ "voters":{
"decompte":11 "count":11
}, },
"mentions":[ "mentions":[
"Très Bien", "Très Bien",

View File

@@ -1,5 +1,5 @@
{ {
"candidats":{ "candidates":{
"A":"albert", "A":"albert",
"B":"Beatrice", "B":"Beatrice",
"C":"Chloé", "C":"Chloé",
@@ -9,8 +9,8 @@
"G":"Gisèle", "G":"Gisèle",
"H":"Hugo" "H":"Hugo"
}, },
"votants":{ "voters":{
"decompte":11 "count":11
}, },
"mentions":[ "mentions":[
"Très Bien", "Très Bien",

View File

@@ -1,5 +1,5 @@
{ {
"candidats":{ "candidates":{
"A":"albert", "A":"albert",
"B":"Beatrice", "B":"Beatrice",
"C":"Chloé", "C":"Chloé",
@@ -9,8 +9,8 @@
"G":"Gisèle", "G":"Gisèle",
"H":"Hugo" "H":"Hugo"
}, },
"votants":{ "voters":{
"decompte":11 "count":11
}, },
"mentions":[ "mentions":[
"Très Bien", "Très Bien",

View File

@@ -1,25 +1,53 @@
#!/bin/bash #!/bin/bash
# SPDX-FileCopyrightText: 2025 artlog@l0g.eu
# SPDX-License-Identifier: AGPL-3.0-or-later
#
if [[ $# > 0 ]] toolsdir=../lib
then pushd $toolsdir >/dev/null
vote=$1 toolsdir=$(pwd)
fi source metascript.sh
popd >/dev/null
if [[ -z $vote ]] while [[ $# > 0 ]]
do
case "$1" in
*.json)
json_file=$1
;;
*)
if [[ $1 =~ [0-9]+ ]]
then
vote=$1
else
parsemetaarg "$1"
fi
;;
esac
shift
done
if [[ -z $vote ]] && [[ -z $json_file ]]
then then
echo "[ERROR] numéro de vote manquant ( ex 1 pour 1.json )" log_error "numéro de vote manquant ( ex 1 pour 1.json )"
exit 1 exit 1
fi fi
json_file=$vote.json if [[ -z $json_file ]]
json=~/clients/artlog/artisanlogiciel.code/artlog_jsontools/build/json then
json_file=$vote.json
fi
enforcefile $json_file exists
json=json
if [[ -x $json ]] if [[ -x $json ]]
then then
json_indent_file=$vote.indent.json json_indent_file=$vote.indent.json
echo "generating $json_indent_file" echo "generating $json_indent_file"
$json indent=spaces:2 -- $json_file > $json_indent_file $json indent=spaces:2 -- $json_file > $json_indent_file
json=$json_file json_file=$json_indent_file
enforcefile $json_file exists
fi fi
code_dir=../code/ code_dir=../code/

16
samples/expected/6.stderr Normal file
View File

@@ -0,0 +1,16 @@
[WARN] No /.resources.var found
Traceback (most recent call last):
File "/home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/samples/../code/parse_sample.py", line 19, in <module>
result = majority_judgment_run(poll)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/code/majority_judgment_method/__init__.py", line 39, in majority_judgment_run
voters=poll['voters']['count']
~~~~^^^^^^^^^^
KeyError: 'voters'
PHP Warning: Undefined array key "voters" in /home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/code/parse_sample.php on line 24
PHP Warning: Trying to access array offset on null in /home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/code/parse_sample.php on line 24
PHP Warning: Undefined array key "candidates" in /home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/code/parse_sample.php on line 25
PHP Fatal error: Uncaught TypeError: count(): Argument #1 ($value) must be of type Countable|array, null given in /home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/code/parse_sample.php:26
Stack trace:
#0 {main}
thrown in /home/plhardy/clients/astrolabe/nextcloud/poll/jugement_majoritaire/code/parse_sample.php on line 26

View File

@@ -0,0 +1,58 @@
{
"ref":"zdslqbbzvk",
"name":"Quel sont les mérites respectifs des participants",
"description":"{\"description\":\"\",\"randomOrder\":true}",
"candidates":[
{
"name":"A",
"description":"",
"image":"",
"id":61445
},
{
"name":"B",
"description":"",
"image":"",
"id":61446
},
{
"name":"C",
"description":"",
"image":"",
"id":61447
}
],
"grades":[
{
"name":"Excellent",
"value":4,
"id":47253
},
{
"name":"Très bien",
"value":3,
"id":47254
},
{
"name":"Bien",
"value":2,
"id":47255
},
{
"name":"Passable",
"value":1,
"id":47256
},
{
"name":"Insuffisant",
"value":0,
"id":47257
}
],
"num_voters":0,
"hide_results":false,
"force_close":true,
"date_end":"2025-10-08T08:00:00Z",
"auth_for_result":false,
"restricted":false
}

View File

@@ -0,0 +1 @@
{"ref":"zdslqbbzvk","name":"Quel sont les mérites respectifs des participants","description":"{\"description\":\"\",\"randomOrder\":true}","candidates":[{"name":"A","description":"","image":"","id":61445},{"name":"B","description":"","image":"","id":61446},{"name":"C","description":"","image":"","id":61447}],"grades":[{"name":"Excellent","value":4,"id":47253},{"name":"Très bien","value":3,"id":47254},{"name":"Bien","value":2,"id":47255},{"name":"Passable","value":1,"id":47256},{"name":"Insuffisant","value":0,"id":47257}],"num_voters":0,"hide_results":false,"force_close":true,"date_end":"2025-10-08T08:00:00Z","auth_for_result":false,"restricted":false}

View File

@@ -0,0 +1,72 @@
{
"name":"Quel sont les mérites respectifs des participants",
"description":"{\"description\":\"\",\"randomOrder\":true}",
"ref":"zdslqbbzvk",
"date_start":"2025-10-07T09:41:24.294727Z",
"date_end":"2025-10-08T08:00:00Z",
"hide_results":false,
"restricted":false,
"auth_for_result":false,
"force_close":true,
"grades":[
{
"name":"Excellent",
"value":4,
"description":"",
"election_ref":"zdslqbbzvk",
"id":47253
},
{
"name":"Très bien",
"value":3,
"description":"",
"election_ref":"zdslqbbzvk",
"id":47254
},
{
"name":"Bien",
"value":2,
"description":"",
"election_ref":"zdslqbbzvk",
"id":47255
},
{
"name":"Passable",
"value":1,
"description":"",
"election_ref":"zdslqbbzvk",
"id":47256
},
{
"name":"Insuffisant",
"value":0,
"description":"",
"election_ref":"zdslqbbzvk",
"id":47257
}
],
"candidates":[
{
"name":"A",
"description":"",
"image":"",
"election_ref":"zdslqbbzvk",
"id":61445
},
{
"name":"B",
"description":"",
"image":"",
"election_ref":"zdslqbbzvk",
"id":61446
},
{
"name":"C",
"description":"",
"image":"",
"election_ref":"zdslqbbzvk",
"id":61447
}
],
"invites":[]
}

View File

@@ -0,0 +1,2 @@
{"name":"Quel sont les mérites respectifs des participants","description":"{\"description\":\"\",\"randomOrder\":true}","ref":"zdslqbbzvk","date_start":"2025-10-07T09:41:24.294727Z","date_end":"2025-10-08T08:00:00Z","hide_results":false,"restricted":false,"auth_for_result":false,"force_close":true,"grades":[{"name":"Excellent","value":4,"description":"","election_ref":"zdslqbbzvk","id":47253},{"name":"Très bien","value":3,"description":"","election_ref":"zdslqbbzvk","id":47254},{"name":"Bien","value":2,"description":"","election_ref":"zdslqbbzvk","id":47255},{"name":"Passable","value":1,"description":"","election_ref":"zdslqbbzvk","id":47256},{"name":"Insuffisant","value":0,"description":"","election_ref":"zdslqbbzvk","id":47257}],"candidates":[{"name":"A","description":"","image":"","election_ref":"zdslqbbzvk","id":61445},{"name":"B","description":"","image":"","election_ref":"zdslqbbzvk","id":61446},{"name":"C","description":"","image":"","election_ref":"zdslqbbzvk","id":61447}],"invites":[]}

View File

@@ -0,0 +1,4 @@
"name","Excellent","Très bien","Bien","Passable","Insuffisant"
"A","0","0","3","0","0"
"B","0","1","1","1","0"
"C","1","0","2","0","0"
1 name Excellent Très bien Bien Passable Insuffisant
2 A 0 0 3 0 0
3 B 0 1 1 1 0
4 C 1 0 2 0 0

View File

@@ -1,2 +1,5 @@
Participants,Adresse e-mail,A,B,C Participants,Adresse e-mail,A,B,C
admin,,Très Bien,Bien,Assez Bien admin,,Très Bien,Bien,Assez Bien
user1,,Très Bien,Bien,Assez Bien
user2,,Très Bien,Bien,Assez Bien
user3,,Très Bien,Bien,Assez Bien
1 Participants Adresse e-mail A B C
2 admin Très Bien Bien Assez Bien
3 user1 Très Bien Bien Assez Bien
4 user2 Très Bien Bien Assez Bien
5 user3 Très Bien Bien Assez Bien

1
samples/poll1.csv.json Normal file
View File

@@ -0,0 +1 @@
{"candidats": {"A": "A", "B": "B", "C": "C"}, "votants": {"decompte": 1}, "mentions": ["Tr\u00e8s Bien", "Bien", "Assez Bien", "Passable", "Insuffisant", "A Rejeter"], "votation": {"votes": [{"A": 0, "B": 1, "C": 2}]}}

View File

@@ -2,7 +2,20 @@
[[ -n $VIRTUAL_ENV ]] || source ../code/bin/activate [[ -n $VIRTUAL_ENV ]] || source ../code/bin/activate
for (( i=0;i<7;i++ ))
expect_error()
{
sample=$1
# error case on purpose
resultdir=tmp
[[ -d ${resultdir} ]] || mkdir ${resultdir}
./check.sh $sample 2>${resultdir}/$sample.stderr
diff ${resultdir}/$sample.stderr expected/$sample.stderr
}
for (( i=0;i<6;i++ ))
do do
./check.sh $i ./check.sh $i
done done
expect_error 6