mirror of
https://gitlab.com/animath/si/plateforme.git
synced 2024-12-25 17:42:24 +00:00
Merge branch 'TFJMv3' into 'master'
TfjmV3 See merge request animath/si/plateforme-tfjm!5
This commit is contained in:
commit
1677731b4a
929
.bashrc
929
.bashrc
@ -1,925 +1,6 @@
|
||||
# =============================================================== #
|
||||
#
|
||||
# PERSONAL $HOME/.bashrc FILE for bash-3.0 (or later)
|
||||
# By Emmanuel Rouat [no-email]
|
||||
#
|
||||
# Last modified: Tue Nov 20 22:04:47 CET 2012
|
||||
PS1='\[\033[01;31m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ '
|
||||
|
||||
# This file is normally read by interactive shells only.
|
||||
#+ Here is the place to define your aliases, functions and
|
||||
#+ other interactive features like your prompt.
|
||||
#
|
||||
# The majority of the code here assumes you are on a GNU
|
||||
#+ system (most likely a Linux box) and is often based on code
|
||||
#+ found on Usenet or Internet.
|
||||
#
|
||||
# See for instance:
|
||||
# http://tldp.org/LDP/abs/html/index.html
|
||||
# http://www.caliban.org/bash
|
||||
# http://www.shelldorado.com/scripts/categories.html
|
||||
# http://www.dotfiles.org
|
||||
#
|
||||
# The choice of colors was done for a shell with a dark background
|
||||
#+ (white on black), and this is usually also suited for pure text-mode
|
||||
#+ consoles (no X server available). If you use a white background,
|
||||
#+ you'll have to do some other choices for readability.
|
||||
#
|
||||
# This bashrc file is a bit overcrowded.
|
||||
# Remember, it is just just an example.
|
||||
# Tailor it to your needs.
|
||||
#
|
||||
# =============================================================== #
|
||||
|
||||
# --> Comments added by HOWTO author.
|
||||
|
||||
# If not running interactively, don't do anything
|
||||
[ -z "$PS1" ] && return
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Source global definitions (if any)
|
||||
#-------------------------------------------------------------
|
||||
|
||||
|
||||
if [ -f /etc/bashrc ]; then
|
||||
. /etc/bashrc # --> Read /etc/bashrc, if present.
|
||||
fi
|
||||
|
||||
|
||||
#--------------------------------------------------------------
|
||||
# Automatic setting of $DISPLAY (if not set already).
|
||||
# This works for me - your mileage may vary. . . .
|
||||
# The problem is that different types of terminals give
|
||||
#+ different answers to 'who am i' (rxvt in particular can be
|
||||
#+ troublesome) - however this code seems to work in a majority
|
||||
#+ of cases.
|
||||
#--------------------------------------------------------------
|
||||
|
||||
function get_xserver ()
|
||||
{
|
||||
case $TERM in
|
||||
xterm )
|
||||
XSERVER=$(who am i | awk '{print $NF}' | tr -d ')''(' )
|
||||
# Ane-Pieter Wieringa suggests the following alternative:
|
||||
# I_AM=$(who am i)
|
||||
# SERVER=${I_AM#*(}
|
||||
# SERVER=${SERVER%*)}
|
||||
XSERVER=${XSERVER%%:*}
|
||||
;;
|
||||
aterm | rxvt)
|
||||
# Find some code that works here. ...
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [ -z ${DISPLAY:=""} ]; then
|
||||
# get_xserver
|
||||
if [[ -z ${XSERVER} || ${XSERVER} == $(hostname) ||
|
||||
${XSERVER} == "unix" ]]; then
|
||||
DISPLAY=":0.0" # Display on local host.
|
||||
else
|
||||
DISPLAY=${XSERVER}:0.0 # Display on remote host.
|
||||
fi
|
||||
fi
|
||||
|
||||
export DISPLAY
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Some settings
|
||||
#-------------------------------------------------------------
|
||||
|
||||
#set -o nounset # These two options are useful for debugging.
|
||||
#set -o xtrace
|
||||
alias debug="set -o nounset; set -o xtrace"
|
||||
|
||||
ulimit -S -c 0 # Don't want coredumps.
|
||||
set -o notify
|
||||
set -o noclobber
|
||||
# set -o ignoreeof
|
||||
|
||||
|
||||
# Enable options:
|
||||
shopt -s cdspell
|
||||
shopt -s cdable_vars
|
||||
shopt -s checkhash
|
||||
shopt -s checkwinsize
|
||||
shopt -s sourcepath
|
||||
shopt -s no_empty_cmd_completion
|
||||
shopt -s cmdhist
|
||||
shopt -s histappend histreedit histverify
|
||||
shopt -s extglob # Necessary for programmable completion.
|
||||
|
||||
# Disable options:
|
||||
shopt -u mailwarn
|
||||
unset MAILCHECK # Don't want my shell to warn me of incoming mail.
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Greeting, motd etc. ...
|
||||
#-------------------------------------------------------------
|
||||
|
||||
# Color definitions (taken from Color Bash Prompt HowTo).
|
||||
# Some colors might look different of some terminals.
|
||||
# For example, I see 'Bold Red' as 'orange' on my screen,
|
||||
# hence the 'Green' 'BRed' 'Red' sequence I often use in my prompt.
|
||||
|
||||
|
||||
# Normal Colors
|
||||
Black='\e[0;30m' # Black
|
||||
Red='\e[0;31m' # Red
|
||||
Green='\e[0;32m' # Green
|
||||
Yellow='\e[0;33m' # Yellow
|
||||
Blue='\e[0;34m' # Blue
|
||||
Purple='\e[0;35m' # Purple
|
||||
Cyan='\e[0;36m' # Cyan
|
||||
White='\e[0;37m' # White
|
||||
|
||||
# Bold
|
||||
BBlack='\e[1;30m' # Black
|
||||
BRed='\e[1;31m' # Red
|
||||
BGreen='\e[1;32m' # Green
|
||||
BYellow='\e[1;33m' # Yellow
|
||||
BBlue='\e[1;34m' # Blue
|
||||
BPurple='\e[1;35m' # Purple
|
||||
BCyan='\e[1;36m' # Cyan
|
||||
BWhite='\e[1;37m' # White
|
||||
|
||||
# Background
|
||||
On_Black='\e[40m' # Black
|
||||
On_Red='\e[41m' # Red
|
||||
On_Green='\e[42m' # Green
|
||||
On_Yellow='\e[43m' # Yellow
|
||||
On_Blue='\e[44m' # Blue
|
||||
On_Purple='\e[45m' # Purple
|
||||
On_Cyan='\e[46m' # Cyan
|
||||
On_White='\e[47m' # White
|
||||
|
||||
NC="\e[m" # Color Reset
|
||||
|
||||
|
||||
ALERT=${BWhite}${On_Red} # Bold White on red background
|
||||
|
||||
|
||||
|
||||
echo -e "${BCyan}This is BASH ${BRed}${BASH_VERSION%.*}${BCyan}\
|
||||
- DISPLAY on ${BRed}$DISPLAY${NC}\n"
|
||||
date
|
||||
if [ -x /usr/games/fortune ]; then
|
||||
/usr/games/fortune -s # Makes our day a bit more fun.... :-)
|
||||
fi
|
||||
|
||||
# function _exit() # Function to run upon exit of shell.
|
||||
# {
|
||||
# echo -e "${BRed}Hasta la vista, baby${NC}"
|
||||
# }
|
||||
# trap _exit EXIT
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Shell Prompt - for many examples, see:
|
||||
# http://www.debian-administration.org/articles/205
|
||||
# http://www.askapache.com/linux/bash-power-prompt.html
|
||||
# http://tldp.org/HOWTO/Bash-Prompt-HOWTO
|
||||
# https://github.com/nojhan/liquidprompt
|
||||
#-------------------------------------------------------------
|
||||
# Current Format: [TIME USER@HOST PWD] >
|
||||
# TIME:
|
||||
# Green == machine load is low
|
||||
# Orange == machine load is medium
|
||||
# Red == machine load is high
|
||||
# ALERT == machine load is very high
|
||||
# USER:
|
||||
# Cyan == normal user
|
||||
# Orange == SU to user
|
||||
# Red == root
|
||||
# HOST:
|
||||
# Cyan == local session
|
||||
# Green == secured remote connection (via ssh)
|
||||
# Red == unsecured remote connection
|
||||
# PWD:
|
||||
# Green == more than 10% free disk space
|
||||
# Orange == less than 10% free disk space
|
||||
# ALERT == less than 5% free disk space
|
||||
# Red == current user does not have write privileges
|
||||
# Cyan == current filesystem is size zero (like /proc)
|
||||
# >:
|
||||
# White == no background or suspended jobs in this shell
|
||||
# Cyan == at least one background job in this shell
|
||||
# Orange == at least one suspended job in this shell
|
||||
#
|
||||
# Command is added to the history file each time you hit enter,
|
||||
# so it's available to all shells (using 'history -a').
|
||||
|
||||
|
||||
# Test connection type:
|
||||
if [ -n "${SSH_CONNECTION}" ]; then
|
||||
CNX=${Green} # Connected on remote machine, via ssh (good).
|
||||
elif [[ "${DISPLAY%%:0*}" != "" ]]; then
|
||||
CNX=${ALERT} # Connected on remote machine, not via ssh (bad).
|
||||
else
|
||||
CNX=${BCyan} # Connected on local machine.
|
||||
fi
|
||||
|
||||
# Test user type:
|
||||
if [[ ${USER} == "root" ]]; then
|
||||
SU=${Red} # User is root.
|
||||
# elif [[ ${USER} != $(logname) ]]; then
|
||||
# SU=${BRed} # User is not login user.
|
||||
else
|
||||
SU=${BCyan} # User is normal (well ... most of us are).
|
||||
fi
|
||||
|
||||
|
||||
|
||||
NCPU=$(grep -c 'processor' /proc/cpuinfo) # Number of CPUs
|
||||
SLOAD=$(( 100*${NCPU} )) # Small load
|
||||
MLOAD=$(( 200*${NCPU} )) # Medium load
|
||||
XLOAD=$(( 400*${NCPU} )) # Xlarge load
|
||||
|
||||
# Returns system load as percentage, i.e., '40' rather than '0.40)'.
|
||||
function load()
|
||||
{
|
||||
local SYSLOAD=$(cut -d " " -f1 /proc/loadavg | tr -d '.')
|
||||
# System load of the current host.
|
||||
echo $((10#$SYSLOAD)) # Convert to decimal.
|
||||
}
|
||||
|
||||
# Returns a color indicating system load.
|
||||
function load_color()
|
||||
{
|
||||
local SYSLOAD=$(load)
|
||||
if [ ${SYSLOAD} -gt ${XLOAD} ]; then
|
||||
echo -en ${ALERT}
|
||||
elif [ ${SYSLOAD} -gt ${MLOAD} ]; then
|
||||
echo -en ${Red}
|
||||
elif [ ${SYSLOAD} -gt ${SLOAD} ]; then
|
||||
echo -en ${BRed}
|
||||
else
|
||||
echo -en ${Green}
|
||||
fi
|
||||
}
|
||||
|
||||
# Returns a color according to free disk space in $PWD.
|
||||
function disk_color()
|
||||
{
|
||||
if [ ! -w "${PWD}" ] ; then
|
||||
echo -en ${Red}
|
||||
# No 'write' privilege in the current directory.
|
||||
elif [ -s "${PWD}" ] ; then
|
||||
local used=$(command df -P "$PWD" |
|
||||
awk 'END {print $5} {sub(/%/,"")}')
|
||||
if [ ${used} -gt 95 ]; then
|
||||
echo -en ${ALERT} # Disk almost full (>95%).
|
||||
elif [ ${used} -gt 90 ]; then
|
||||
echo -en ${BRed} # Free disk space almost gone.
|
||||
else
|
||||
echo -en ${Green} # Free disk space is ok.
|
||||
fi
|
||||
else
|
||||
echo -en ${Cyan}
|
||||
# Current directory is size '0' (like /proc, /sys etc).
|
||||
fi
|
||||
}
|
||||
|
||||
# Returns a color according to running/suspended jobs.
|
||||
function job_color()
|
||||
{
|
||||
if [ $(jobs -s | wc -l) -gt "0" ]; then
|
||||
echo -en ${BRed}
|
||||
elif [ $(jobs -r | wc -l) -gt "0" ] ; then
|
||||
echo -en ${BCyan}
|
||||
fi
|
||||
}
|
||||
|
||||
# Adds some text in the terminal frame (if applicable).
|
||||
|
||||
|
||||
# Now we construct the prompt.
|
||||
PROMPT_COMMAND="history -a"
|
||||
case ${TERM} in
|
||||
*term | rxvt | linux)
|
||||
PS1="\[\$(load_color)\][\A\[${NC}\] "
|
||||
# Time of day (with load info):
|
||||
PS1="\[\$(load_color)\][\A\[${NC}\] "
|
||||
# User@Host (with connection type info):
|
||||
PS1=${PS1}"\[${SU}\]\u\[${NC}\]@\[${CNX}\]\h\[${NC}\] "
|
||||
# PWD (with 'disk space' info):
|
||||
PS1=${PS1}"\[\$(disk_color)\]\W]\[${NC}\] "
|
||||
# Prompt (with 'job' info):
|
||||
PS1=${PS1}"\[\$(job_color)\]>\[${NC}\] "
|
||||
# Set title of current xterm:
|
||||
PS1=${PS1}"\[\e]0;[\u@\h] \w\a\]"
|
||||
;;
|
||||
*)
|
||||
PS1="(\A \u@\h \W) > " # --> PS1="(\A \u@\h \w) > "
|
||||
# --> Shows full pathname of current dir.
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
|
||||
export TIMEFORMAT=$'\nreal %3R\tuser %3U\tsys %3S\tpcpu %P\n'
|
||||
export HISTIGNORE="&:bg:fg:ll:h"
|
||||
export HISTTIMEFORMAT="$(echo -e ${BCyan})[%d/%m %H:%M:%S]$(echo -e ${NC}) "
|
||||
export HISTCONTROL=ignoredups
|
||||
export HOSTFILE=$HOME/.hosts # Put a list of remote hosts in ~/.hosts
|
||||
|
||||
|
||||
#============================================================
|
||||
#
|
||||
# ALIASES AND FUNCTIONS
|
||||
#
|
||||
# Arguably, some functions defined here are quite big.
|
||||
# If you want to make this file smaller, these functions can
|
||||
#+ be converted into scripts and removed from here.
|
||||
#
|
||||
#============================================================
|
||||
|
||||
#-------------------
|
||||
# Personnal Aliases
|
||||
#-------------------
|
||||
|
||||
alias rm='rm -i'
|
||||
alias cp='cp -i'
|
||||
alias mv='mv -i'
|
||||
# -> Prevents accidentally clobbering files.
|
||||
alias mkdir='mkdir -p'
|
||||
|
||||
alias h='history'
|
||||
alias j='jobs -l'
|
||||
alias which='type -a'
|
||||
alias ..='cd ..'
|
||||
alias ...='cd ../..'
|
||||
|
||||
# Pretty-print of some PATH variables:
|
||||
alias path='echo -e ${PATH//:/\\n}'
|
||||
alias libpath='echo -e ${LD_LIBRARY_PATH//:/\\n}'
|
||||
|
||||
|
||||
alias du='du -kh' # Makes a more readable output.
|
||||
alias df='df -kTh'
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# The 'ls' family (this assumes you use a recent GNU ls).
|
||||
#-------------------------------------------------------------
|
||||
# Add colors for filetype and human-readable sizes by default on 'ls':
|
||||
alias ls='ls -h --color'
|
||||
alias lx='ls -lXB' # Sort by extension.
|
||||
alias lk='ls -lSr' # Sort by size, biggest last.
|
||||
alias lt='ls -ltr' # Sort by date, most recent last.
|
||||
alias lc='ls -ltcr' # Sort by/show change time,most recent last.
|
||||
alias lu='ls -ltur' # Sort by/show access time,most recent last.
|
||||
|
||||
# The ubiquitous 'll': directories first, with alphanumeric sorting:
|
||||
alias ll="ls -lv --group-directories-first"
|
||||
alias lm='ll |more' # Pipe through 'more'
|
||||
alias lr='ll -R' # Recursive ls.
|
||||
alias la='ll -A' # Show hidden files.
|
||||
alias l='la'
|
||||
alias tree='tree -Csuh' # Nice alternative to 'recursive ls' ...
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Tailoring 'less'
|
||||
#-------------------------------------------------------------
|
||||
|
||||
alias more='less'
|
||||
export PAGER=less
|
||||
export LESSCHARSET='latin1'
|
||||
export LESSOPEN='|/usr/bin/lesspipe.sh %s 2>&-'
|
||||
# Use this if lesspipe.sh exists.
|
||||
export LESS='-i -N -w -z-4 -g -e -M -X -F -R -P%t?f%f \
|
||||
:stdin .?pb%pb\%:?lbLine %lb:?bbByte %bb:-...'
|
||||
|
||||
# LESS man page colors (makes Man pages more readable).
|
||||
export LESS_TERMCAP_mb=$'\E[01;31m'
|
||||
export LESS_TERMCAP_md=$'\E[01;31m'
|
||||
export LESS_TERMCAP_me=$'\E[0m'
|
||||
export LESS_TERMCAP_se=$'\E[0m'
|
||||
export LESS_TERMCAP_so=$'\E[01;44;33m'
|
||||
export LESS_TERMCAP_ue=$'\E[0m'
|
||||
export LESS_TERMCAP_us=$'\E[01;32m'
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Spelling typos - highly personnal and keyboard-dependent :-)
|
||||
#-------------------------------------------------------------
|
||||
|
||||
alias xs='cd'
|
||||
alias vf='cd'
|
||||
alias moer='more'
|
||||
alias moew='more'
|
||||
alias kk='ll'
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# A few fun ones
|
||||
#-------------------------------------------------------------
|
||||
|
||||
# Adds some text in the terminal frame (if applicable).
|
||||
|
||||
function xtitle()
|
||||
{
|
||||
case "$TERM" in
|
||||
*term* | rxvt)
|
||||
echo -en "\e]0;$*\a" ;;
|
||||
*) ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
# Aliases that use xtitle
|
||||
alias top='xtitle Processes on $HOST && top'
|
||||
alias make='xtitle Making $(basename $PWD) ; make'
|
||||
|
||||
# .. and functions
|
||||
function man()
|
||||
{
|
||||
for i ; do
|
||||
xtitle The $(basename $1|tr -d .[:digit:]) manual
|
||||
command man -a "$i"
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Make the following commands run in background automatically:
|
||||
#-------------------------------------------------------------
|
||||
|
||||
function te() # wrapper around xemacs/gnuserv
|
||||
{
|
||||
if [ "$(gnuclient -batch -eval t 2>&-)" == "t" ]; then
|
||||
gnuclient -q "$@";
|
||||
else
|
||||
( xemacs "$@" &);
|
||||
fi
|
||||
}
|
||||
|
||||
function soffice() { command soffice "$@" & }
|
||||
function firefox() { command firefox "$@" & }
|
||||
function xpdf() { command xpdf "$@" & }
|
||||
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# File & strings related functions:
|
||||
#-------------------------------------------------------------
|
||||
|
||||
|
||||
# Find a file with a pattern in name:
|
||||
function ff() { find . -type f -iname '*'"$*"'*' -ls ; }
|
||||
|
||||
# Find a file with pattern $1 in name and Execute $2 on it:
|
||||
function fe() { find . -type f -iname '*'"${1:-}"'*' \
|
||||
-exec ${2:-file} {} \; ; }
|
||||
|
||||
# Find a pattern in a set of files and highlight them:
|
||||
#+ (needs a recent version of egrep).
|
||||
function fstr()
|
||||
{
|
||||
OPTIND=1
|
||||
local mycase=""
|
||||
local usage="fstr: find string in files.
|
||||
Usage: fstr [-i] \"pattern\" [\"filename pattern\"] "
|
||||
while getopts :it opt
|
||||
do
|
||||
case "$opt" in
|
||||
i) mycase="-i " ;;
|
||||
*) echo "$usage"; return ;;
|
||||
esac
|
||||
done
|
||||
shift $(( $OPTIND - 1 ))
|
||||
if [ "$#" -lt 1 ]; then
|
||||
echo "$usage"
|
||||
return;
|
||||
fi
|
||||
find . -type f -name "${2:-*}" -print0 | \
|
||||
xargs -0 egrep --color=always -sn ${case} "$1" 2>&- | more
|
||||
|
||||
}
|
||||
|
||||
|
||||
function swap()
|
||||
{ # Swap 2 filenames around, if they exist (from Uzi's bashrc).
|
||||
local TMPFILE=tmp.$$
|
||||
|
||||
[ $# -ne 2 ] && echo "swap: 2 arguments needed" && return 1
|
||||
[ ! -e $1 ] && echo "swap: $1 does not exist" && return 1
|
||||
[ ! -e $2 ] && echo "swap: $2 does not exist" && return 1
|
||||
|
||||
mv "$1" $TMPFILE
|
||||
mv "$2" "$1"
|
||||
mv $TMPFILE "$2"
|
||||
}
|
||||
|
||||
function extract() # Handy Extract Program
|
||||
{
|
||||
if [ -f $1 ] ; then
|
||||
case $1 in
|
||||
*.tar.bz2) tar xvjf $1 ;;
|
||||
*.tar.gz) tar xvzf $1 ;;
|
||||
*.bz2) bunzip2 $1 ;;
|
||||
*.rar) unrar x $1 ;;
|
||||
*.gz) gunzip $1 ;;
|
||||
*.tar) tar xvf $1 ;;
|
||||
*.tbz2) tar xvjf $1 ;;
|
||||
*.tgz) tar xvzf $1 ;;
|
||||
*.zip) unzip $1 ;;
|
||||
*.Z) uncompress $1 ;;
|
||||
*.7z) 7z x $1 ;;
|
||||
*) echo "'$1' cannot be extracted via >extract<" ;;
|
||||
esac
|
||||
else
|
||||
echo "'$1' is not a valid file!"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Creates an archive (*.tar.gz) from given directory.
|
||||
function maketar() { tar cvzf "${1%%/}.tar.gz" "${1%%/}/"; }
|
||||
|
||||
# Create a ZIP archive of a file or folder.
|
||||
function makezip() { zip -r "${1%%/}.zip" "$1" ; }
|
||||
|
||||
# Make your directories and files access rights sane.
|
||||
function sanitize() { chmod -R u=rwX,g=rX,o= "$@" ;}
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Process/system related functions:
|
||||
#-------------------------------------------------------------
|
||||
|
||||
|
||||
function my_ps() { ps $@ -u $USER -o pid,%cpu,%mem,bsdtime,command ; }
|
||||
function pp() { my_ps f | awk '!/awk/ && $0~var' var=${1:-".*"} ; }
|
||||
|
||||
|
||||
function killps() # kill by process name
|
||||
{
|
||||
local pid pname sig="-TERM" # default signal
|
||||
if [ "$#" -lt 1 ] || [ "$#" -gt 2 ]; then
|
||||
echo "Usage: killps [-SIGNAL] pattern"
|
||||
return;
|
||||
fi
|
||||
if [ $# = 2 ]; then sig=$1 ; fi
|
||||
for pid in $(my_ps| awk '!/awk/ && $0~pat { print $1 }' pat=${!#} )
|
||||
do
|
||||
pname=$(my_ps | awk '$1~var { print $5 }' var=$pid )
|
||||
if ask "Kill process $pid <$pname> with signal $sig?"
|
||||
then kill $sig $pid
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
function mydf() # Pretty-print of 'df' output.
|
||||
{ # Inspired by 'dfc' utility.
|
||||
for fs ; do
|
||||
|
||||
if [ ! -d $fs ]
|
||||
then
|
||||
echo -e $fs" :No such file or directory" ; continue
|
||||
fi
|
||||
|
||||
local info=( $(command df -P $fs | awk 'END{ print $2,$3,$5 }') )
|
||||
local free=( $(command df -Pkh $fs | awk 'END{ print $4 }') )
|
||||
local nbstars=$(( 20 * ${info[1]} / ${info[0]} ))
|
||||
local out="["
|
||||
for ((j=0;j<20;j++)); do
|
||||
if [ ${j} -lt ${nbstars} ]; then
|
||||
out=$out"*"
|
||||
else
|
||||
out=$out"-"
|
||||
fi
|
||||
done
|
||||
out=${info[2]}" "$out"] ("$free" free on "$fs")"
|
||||
echo -e $out
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
function my_ip() # Get IP adress on ethernet.
|
||||
{
|
||||
MY_IP=$(/sbin/ifconfig eth0 | awk '/inet/ { print $2 } ' |
|
||||
sed -e s/addr://)
|
||||
echo ${MY_IP:-"Not connected"}
|
||||
}
|
||||
|
||||
function ii() # Get current host related info.
|
||||
{
|
||||
echo -e "\nYou are logged on ${BRed}$HOST"
|
||||
echo -e "\n${BRed}Additionnal information:$NC " ; uname -a
|
||||
echo -e "\n${BRed}Users logged on:$NC " ; w -hs |
|
||||
cut -d " " -f1 | sort | uniq
|
||||
echo -e "\n${BRed}Current date :$NC " ; date
|
||||
echo -e "\n${BRed}Machine stats :$NC " ; uptime
|
||||
echo -e "\n${BRed}Memory stats :$NC " ; free
|
||||
echo -e "\n${BRed}Diskspace :$NC " ; mydf / $HOME
|
||||
echo -e "\n${BRed}Local IP Address :$NC" ; my_ip
|
||||
echo -e "\n${BRed}Open connections :$NC "; netstat -pan --inet;
|
||||
echo
|
||||
}
|
||||
|
||||
#-------------------------------------------------------------
|
||||
# Misc utilities:
|
||||
#-------------------------------------------------------------
|
||||
|
||||
function repeat() # Repeat n times command.
|
||||
{
|
||||
local i max
|
||||
max=$1; shift;
|
||||
for ((i=1; i <= max ; i++)); do # --> C-like syntax
|
||||
eval "$@";
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
function ask() # See 'killps' for example of use.
|
||||
{
|
||||
echo -n "$@" '[y/n] ' ; read ans
|
||||
case "$ans" in
|
||||
y*|Y*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
function corename() # Get name of app that created a corefile.
|
||||
{
|
||||
for file ; do
|
||||
echo -n $file : ; gdb --core=$file --batch | head -1
|
||||
done
|
||||
}
|
||||
|
||||
|
||||
|
||||
#=========================================================================
|
||||
#
|
||||
# PROGRAMMABLE COMPLETION SECTION
|
||||
# Most are taken from the bash 2.05 documentation and from Ian McDonald's
|
||||
# 'Bash completion' package (http://www.caliban.org/bash/#completion)
|
||||
# You will in fact need bash more recent then 3.0 for some features.
|
||||
#
|
||||
# Note that most linux distributions now provide many completions
|
||||
# 'out of the box' - however, you might need to make your own one day,
|
||||
# so I kept those here as examples.
|
||||
#=========================================================================
|
||||
|
||||
if [ "${BASH_VERSION%.*}" \< "3.0" ]; then
|
||||
echo "You will need to upgrade to version 3.0 for full \
|
||||
programmable completion features"
|
||||
return
|
||||
fi
|
||||
|
||||
shopt -s extglob # Necessary.
|
||||
|
||||
complete -A hostname rsh rcp telnet rlogin ftp ping disk
|
||||
complete -A export printenv
|
||||
complete -A variable export local readonly unset
|
||||
complete -A enabled builtin
|
||||
complete -A alias alias unalias
|
||||
complete -A function function
|
||||
complete -A user su mail finger
|
||||
|
||||
complete -A helptopic help # Currently same as builtins.
|
||||
complete -A shopt shopt
|
||||
complete -A stopped -P '%' bg
|
||||
complete -A job -P '%' fg jobs disown
|
||||
|
||||
complete -A directory mkdir rmdir
|
||||
complete -A directory -o default cd
|
||||
|
||||
# Compression
|
||||
complete -f -o default -X '*.+(zip|ZIP)' zip
|
||||
complete -f -o default -X '!*.+(zip|ZIP)' unzip
|
||||
complete -f -o default -X '*.+(z|Z)' compress
|
||||
complete -f -o default -X '!*.+(z|Z)' uncompress
|
||||
complete -f -o default -X '*.+(gz|GZ)' gzip
|
||||
complete -f -o default -X '!*.+(gz|GZ)' gunzip
|
||||
complete -f -o default -X '*.+(bz2|BZ2)' bzip2
|
||||
complete -f -o default -X '!*.+(bz2|BZ2)' bunzip2
|
||||
complete -f -o default -X '!*.+(zip|ZIP|z|Z|gz|GZ|bz2|BZ2)' extract
|
||||
|
||||
|
||||
# Documents - Postscript,pdf,dvi.....
|
||||
complete -f -o default -X '!*.+(ps|PS)' gs ghostview ps2pdf ps2ascii
|
||||
complete -f -o default -X \
|
||||
'!*.+(dvi|DVI)' dvips dvipdf xdvi dviselect dvitype
|
||||
complete -f -o default -X '!*.+(pdf|PDF)' acroread pdf2ps
|
||||
complete -f -o default -X '!*.@(@(?(e)ps|?(E)PS|pdf|PDF)?\
|
||||
(.gz|.GZ|.bz2|.BZ2|.Z))' gv ggv
|
||||
complete -f -o default -X '!*.texi*' makeinfo texi2dvi texi2html texi2pdf
|
||||
complete -f -o default -X '!*.tex' tex latex slitex
|
||||
complete -f -o default -X '!*.lyx' lyx
|
||||
complete -f -o default -X '!*.+(htm*|HTM*)' lynx html2ps
|
||||
complete -f -o default -X \
|
||||
'!*.+(doc|DOC|xls|XLS|ppt|PPT|sx?|SX?|csv|CSV|od?|OD?|ott|OTT)' soffice
|
||||
|
||||
# Multimedia
|
||||
complete -f -o default -X \
|
||||
'!*.+(gif|GIF|jp*g|JP*G|bmp|BMP|xpm|XPM|png|PNG)' xv gimp ee gqview
|
||||
complete -f -o default -X '!*.+(mp3|MP3)' mpg123 mpg321
|
||||
complete -f -o default -X '!*.+(ogg|OGG)' ogg123
|
||||
complete -f -o default -X \
|
||||
'!*.@(mp[23]|MP[23]|ogg|OGG|wav|WAV|pls|\
|
||||
m3u|xm|mod|s[3t]m|it|mtm|ult|flac)' xmms
|
||||
complete -f -o default -X '!*.@(mp?(e)g|MP?(E)G|wma|avi|AVI|\
|
||||
asf|vob|VOB|bin|dat|vcd|ps|pes|fli|viv|rm|ram|yuv|mov|MOV|qt|\
|
||||
QT|wmv|mp3|MP3|ogg|OGG|ogm|OGM|mp4|MP4|wav|WAV|asx|ASX)' xine
|
||||
|
||||
|
||||
|
||||
complete -f -o default -X '!*.pl' perl perl5
|
||||
|
||||
|
||||
# This is a 'universal' completion function - it works when commands have
|
||||
#+ a so-called 'long options' mode , ie: 'ls --all' instead of 'ls -a'
|
||||
# Needs the '-o' option of grep
|
||||
#+ (try the commented-out version if not available).
|
||||
|
||||
# First, remove '=' from completion word separators
|
||||
#+ (this will allow completions like 'ls --color=auto' to work correctly).
|
||||
|
||||
COMP_WORDBREAKS=${COMP_WORDBREAKS/=/}
|
||||
|
||||
|
||||
_get_longopts()
|
||||
{
|
||||
#$1 --help | sed -e '/--/!d' -e 's/.*--\([^[:space:].,]*\).*/--\1/'| \
|
||||
#grep ^"$2" |sort -u ;
|
||||
$1 --help | grep -o -e "--[^[:space:].,]*" | grep -e "$2" |sort -u
|
||||
}
|
||||
|
||||
_longopts()
|
||||
{
|
||||
local cur
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
|
||||
case "${cur:-*}" in
|
||||
-*) ;;
|
||||
*) return ;;
|
||||
esac
|
||||
|
||||
case "$1" in
|
||||
\~*) eval cmd="$1" ;;
|
||||
*) cmd="$1" ;;
|
||||
esac
|
||||
COMPREPLY=( $(_get_longopts ${1} ${cur} ) )
|
||||
}
|
||||
complete -o default -F _longopts configure bash
|
||||
complete -o default -F _longopts wget id info a2ps ls recode
|
||||
|
||||
_tar()
|
||||
{
|
||||
local cur ext regex tar untar
|
||||
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
|
||||
# If we want an option, return the possible long options.
|
||||
case "$cur" in
|
||||
-*) COMPREPLY=( $(_get_longopts $1 $cur ) ); return 0;;
|
||||
esac
|
||||
|
||||
if [ $COMP_CWORD -eq 1 ]; then
|
||||
COMPREPLY=( $( compgen -W 'c t x u r d A' -- $cur ) )
|
||||
return 0
|
||||
fi
|
||||
|
||||
case "${COMP_WORDS[1]}" in
|
||||
?(-)c*f)
|
||||
COMPREPLY=( $( compgen -f $cur ) )
|
||||
return 0
|
||||
;;
|
||||
+([^Izjy])f)
|
||||
ext='tar'
|
||||
regex=$ext
|
||||
;;
|
||||
*z*f)
|
||||
ext='tar.gz'
|
||||
regex='t\(ar\.\)\(gz\|Z\)'
|
||||
;;
|
||||
*[Ijy]*f)
|
||||
ext='t?(ar.)bz?(2)'
|
||||
regex='t\(ar\.\)bz2\?'
|
||||
;;
|
||||
*)
|
||||
COMPREPLY=( $( compgen -f $cur ) )
|
||||
return 0
|
||||
;;
|
||||
|
||||
esac
|
||||
|
||||
if [[ "$COMP_LINE" == tar*.$ext' '* ]]; then
|
||||
# Complete on files in tar file.
|
||||
#
|
||||
# Get name of tar file from command line.
|
||||
tar=$( echo "$COMP_LINE" | \
|
||||
sed -e 's|^.* \([^ ]*'$regex'\) .*$|\1|' )
|
||||
# Devise how to untar and list it.
|
||||
untar=t${COMP_WORDS[1]//[^Izjyf]/}
|
||||
|
||||
COMPREPLY=( $( compgen -W "$( echo $( tar $untar $tar \
|
||||
2>/dev/null ) )" -- "$cur" ) )
|
||||
return 0
|
||||
|
||||
else
|
||||
# File completion on relevant files.
|
||||
COMPREPLY=( $( compgen -G $cur\*.$ext ) )
|
||||
|
||||
fi
|
||||
|
||||
return 0
|
||||
|
||||
}
|
||||
|
||||
complete -F _tar -o default tar
|
||||
|
||||
_make()
|
||||
{
|
||||
local mdef makef makef_dir="." makef_inc gcmd cur prev i;
|
||||
COMPREPLY=();
|
||||
cur=${COMP_WORDS[COMP_CWORD]};
|
||||
prev=${COMP_WORDS[COMP_CWORD-1]};
|
||||
case "$prev" in
|
||||
-*f)
|
||||
COMPREPLY=($(compgen -f $cur ));
|
||||
return 0
|
||||
;;
|
||||
esac;
|
||||
case "$cur" in
|
||||
-*)
|
||||
COMPREPLY=($(_get_longopts $1 $cur ));
|
||||
return 0
|
||||
;;
|
||||
esac;
|
||||
|
||||
# ... make reads
|
||||
# GNUmakefile,
|
||||
# then makefile
|
||||
# then Makefile ...
|
||||
if [ -f ${makef_dir}/GNUmakefile ]; then
|
||||
makef=${makef_dir}/GNUmakefile
|
||||
elif [ -f ${makef_dir}/makefile ]; then
|
||||
makef=${makef_dir}/makefile
|
||||
elif [ -f ${makef_dir}/Makefile ]; then
|
||||
makef=${makef_dir}/Makefile
|
||||
else
|
||||
makef=${makef_dir}/*.mk # Local convention.
|
||||
fi
|
||||
|
||||
|
||||
# Before we scan for targets, see if a Makefile name was
|
||||
#+ specified with -f.
|
||||
for (( i=0; i < ${#COMP_WORDS[@]}; i++ )); do
|
||||
if [[ ${COMP_WORDS[i]} == -f ]]; then
|
||||
# eval for tilde expansion
|
||||
eval makef=${COMP_WORDS[i+1]}
|
||||
break
|
||||
fi
|
||||
done
|
||||
[ ! -f $makef ] && return 0
|
||||
|
||||
# Deal with included Makefiles.
|
||||
makef_inc=$( grep -E '^-?include' $makef |
|
||||
sed -e "s,^.* ,"$makef_dir"/," )
|
||||
for file in $makef_inc; do
|
||||
[ -f $file ] && makef="$makef $file"
|
||||
done
|
||||
|
||||
|
||||
# If we have a partial word to complete, restrict completions
|
||||
#+ to matches of that word.
|
||||
if [ -n "$cur" ]; then gcmd='grep "^$cur"' ; else gcmd=cat ; fi
|
||||
|
||||
COMPREPLY=( $( awk -F':' '/^[a-zA-Z0-9][^$#\/\t=]*:([^=]|$)/ \
|
||||
{split($1,A,/ /);for(i in A)print A[i]}' \
|
||||
$makef 2>/dev/null | eval $gcmd ))
|
||||
|
||||
}
|
||||
|
||||
complete -F _make -X '+($*|*.[cho])' make gmake pmake
|
||||
|
||||
|
||||
|
||||
|
||||
_killall()
|
||||
{
|
||||
local cur prev
|
||||
COMPREPLY=()
|
||||
cur=${COMP_WORDS[COMP_CWORD]}
|
||||
|
||||
# Get a list of processes
|
||||
#+ (the first sed evaluation
|
||||
#+ takes care of swapped out processes, the second
|
||||
#+ takes care of getting the basename of the process).
|
||||
COMPREPLY=( $( ps -u $USER -o comm | \
|
||||
sed -e '1,1d' -e 's#[]\[]##g' -e 's#^.*/##'| \
|
||||
awk '{if ($0 ~ /^'$cur'/) print $0}' ))
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
complete -F _killall killall killps
|
||||
|
||||
|
||||
|
||||
# Local Variables:
|
||||
# mode:shell-script
|
||||
# sh-shell:bash
|
||||
# End:
|
||||
alias ls='ls --color=auto'
|
||||
alias ll='ls -l'
|
||||
alias la='ls -A'
|
||||
alias l='ls -lACF'
|
||||
|
@ -1,4 +1,3 @@
|
||||
__pycache__
|
||||
media
|
||||
import_olddb
|
||||
db.sqlite3
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -1,6 +1,3 @@
|
||||
# Server config files
|
||||
nginx_note.conf
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
dist
|
||||
build
|
||||
@ -43,8 +40,5 @@ env/
|
||||
venv/
|
||||
db.sqlite3
|
||||
|
||||
# Ignore migrations during first phase dev
|
||||
migrations/
|
||||
|
||||
# Don't git personal data
|
||||
import_olddb/
|
||||
# Don't git index
|
||||
whoosh_index/
|
||||
|
27
.gitlab-ci.yml
Normal file
27
.gitlab-ci.yml
Normal file
@ -0,0 +1,27 @@
|
||||
stages:
|
||||
- test
|
||||
- quality-assurance
|
||||
|
||||
py38:
|
||||
stage: test
|
||||
image: python:3.8-alpine
|
||||
before_script:
|
||||
- apk add --no-cache libmagic
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py38
|
||||
|
||||
py39:
|
||||
stage: test
|
||||
image: python:3.9-alpine
|
||||
before_script:
|
||||
- apk add --no-cache libmagic
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e py39
|
||||
|
||||
linters:
|
||||
stage: quality-assurance
|
||||
image: python:3-alpine
|
||||
before_script:
|
||||
- pip install tox --no-cache-dir
|
||||
script: tox -e linters
|
||||
allow_failure: true
|
@ -1,7 +0,0 @@
|
||||
ErrorDocument 403 /tfjm/server_files/403.php
|
||||
ErrorDocument 404 /tfjm/server_files/404.php
|
||||
|
||||
Options +FollowSymlinks
|
||||
Options -Indexes
|
||||
RewriteEngine On
|
||||
RewriteRule ^(.*)$ dispatcher.php?path=$1 [QSA,L]
|
6
.idea/.gitignore
vendored
6
.idea/.gitignore
vendored
@ -1,6 +0,0 @@
|
||||
# Default ignored files
|
||||
/workspace.xml
|
||||
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||||
</content>
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
@ -1,5 +0,0 @@
|
||||
<component name="ProjectCodeStyleConfiguration">
|
||||
<state>
|
||||
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
|
||||
</state>
|
||||
</component>
|
@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="tfjm@ynerant.fr" uuid="ce243a48-c634-4134-8105-e68ea53cd5ed">
|
||||
<driver-ref>mysql.8</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>com.mysql.cj.jdbc.Driver</jdbc-driver>
|
||||
<jdbc-url>jdbc:mysql://ynerant.fr:3306/tfjm</jdbc-url>
|
||||
<driver-properties>
|
||||
<property name="serverTimezone" value="GMT+1" />
|
||||
</driver-properties>
|
||||
<time-zone>GMT+1</time-zone>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="PublishConfigData" autoUpload="Always" serverName="inscription.tfjm.org" autoUploadExternalChanges="true">
|
||||
<serverData>
|
||||
<paths name="inscription.tfjm.org">
|
||||
<serverdata>
|
||||
<mappings>
|
||||
<mapping deploy="/var/inscription-tfjm" local="$PROJECT_DIR$" web="/" />
|
||||
</mappings>
|
||||
</serverdata>
|
||||
</paths>
|
||||
<paths name="ynerant.fr">
|
||||
<serverdata>
|
||||
<mappings>
|
||||
<mapping deploy="/var/www/html/tfjm" local="$PROJECT_DIR$" web="/tfjm" />
|
||||
</mappings>
|
||||
</serverdata>
|
||||
</paths>
|
||||
</serverData>
|
||||
<option name="myAutoUpload" value="ALWAYS" />
|
||||
</component>
|
||||
</project>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
</project>
|
@ -1,8 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/TFJM.iml" filepath="$PROJECT_DIR$/.idea/TFJM.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
19
Dockerfile
19
Dockerfile
@ -1,29 +1,40 @@
|
||||
FROM python:3-alpine
|
||||
FROM python:3.8-alpine
|
||||
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV DJANGO_ALLOW_ASYNC_UNSAFE 1
|
||||
|
||||
# Install LaTeX requirements
|
||||
RUN apk add --no-cache gettext texlive nginx gcc libc-dev libffi-dev postgresql-dev mariadb-connector-c-dev
|
||||
RUN apk add --no-cache gettext nginx gcc libc-dev libffi-dev libxml2-dev libxslt-dev postgresql-dev libmagic
|
||||
|
||||
RUN apk add --no-cache bash
|
||||
|
||||
RUN mkdir /code
|
||||
RUN mkdir /code /code/docs
|
||||
WORKDIR /code
|
||||
COPY requirements.txt /code/requirements.txt
|
||||
COPY docs/requirements.txt /code/docs/requirements.txt
|
||||
RUN pip install -r requirements.txt --no-cache-dir
|
||||
RUN pip install -r docs/requirements.txt --no-cache-dir
|
||||
|
||||
COPY . /code/
|
||||
|
||||
# Compile documentation
|
||||
RUN sphinx-build -M html docs docs/_build
|
||||
|
||||
RUN python manage.py collectstatic --noinput && \
|
||||
python manage.py compilemessages
|
||||
|
||||
# Configure nginx
|
||||
RUN mkdir /run/nginx
|
||||
RUN ln -sf /dev/stdout /var/log/nginx/access.log && ln -sf /dev/stderr /var/log/nginx/error.log
|
||||
RUN ln -sf /code/nginx_tfjm.conf /etc/nginx/conf.d/tfjm.conf
|
||||
RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
RUN crontab /code/tfjm.cron
|
||||
|
||||
# With a bashrc, the shell is better
|
||||
RUN ln -s /code/.bashrc /root/.bashrc
|
||||
|
||||
ENTRYPOINT ["/code/entrypoint.sh"]
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["./manage.py", "shell_plus", "--ptpython"]
|
||||
CMD ["./manage.py", "shell_plus", "--ipython"]
|
||||
|
674
LICENSE
Normal file
674
LICENSE
Normal file
@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) 2020 Animath
|
||||
|
||||
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, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
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, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) 2020 Animath
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
25
README.md
25
README.md
@ -1,7 +1,10 @@
|
||||
# Plateforme d'inscription du TFJM²
|
||||
# Plateforme du TFJM²
|
||||
|
||||
La plateforme du TFJM² est née pour l'édition 2020 du tournoi. D'abord codée en PHP, elle a subi une refonte totale en
|
||||
Python, à l'aide du framework Web [Django](https://www.djangoproject.com/).
|
||||
[![pipeline status](https://gitlab.com/animath/si/plateforme-tfjm/badges/master/pipeline.svg)](https://gitlab.com/animath/si/plateforme-tfjm/-/commits/master)
|
||||
[![coverage report](https://gitlab.com/animath/si/plateforme-tfjm/badges/master/coverage.svg)](https://gitlab.com/animath/si/plateforme-tfjm/-/commits/master)
|
||||
|
||||
La plateforme du TFJM² est née pour la dixième édition en 2019 de l'action.
|
||||
D'abord codée en PHP, elle a subi une refonte totale en Python, à l'aide du framework Web [Django](https://www.djangoproject.com/).
|
||||
|
||||
Cette plateforme permet aux participants et encadrants de s'inscrire et de déposer leurs autorisations nécessaires.
|
||||
Ils pourront ensuite déposer leurs solutions et notes de synthèse pour le premier tour en temps voulu. La plateforme
|
||||
@ -18,8 +21,8 @@ Le plus simple pour installer la plateforme est d'utiliser l'image Docker inclus
|
||||
exposé sur le port 80 avec le serveur Django. Ci-dessous une configuration Docker-Compose, à adapter selon vos besoins :
|
||||
|
||||
```yaml
|
||||
inscription-tfjm:
|
||||
build: ./inscription-tfjm
|
||||
plateforme-tfjm:
|
||||
build: https://gitlab.com/animath/si/plateforme-tfjm.git
|
||||
links:
|
||||
- postgres
|
||||
ports:
|
||||
@ -37,7 +40,6 @@ Il faut remplir les variables d'environnement suivantes :
|
||||
|
||||
```env
|
||||
TFJM_STAGE= # dev ou prod
|
||||
TFJM_YEAR=2021 # Année de la session du TFJM²
|
||||
DJANGO_DB_TYPE= # MySQL, PostgreSQL ou SQLite (par défaut)
|
||||
DJANGO_DB_HOST= # Hôte de la base de données
|
||||
DJANGO_DB_NAME= # Nom de la base de données
|
||||
@ -49,17 +51,20 @@ SMTP_HOST_USER= # Utilisateur du compte SMTP
|
||||
SMTP_HOST_PASSWORD= # Mot de passe du compte SMTP
|
||||
FROM_EMAIL=contact@tfjm.org # Nom de l'expéditeur des mails
|
||||
SERVER_EMAIL=contact@tfjm.org # Adresse e-mail expéditrice
|
||||
SYMPA_URL=lists.example.com # Serveur Sympa à utiliser
|
||||
SYMPA_EMAIL= # Adresse e-mail du compte administrateur de Sympa
|
||||
SYMPA_PASSWORD= # Mot de passe du compte administrateur de Sympa
|
||||
SYNAPSE_PASSWORD= # Mot de passe du robot Matrix
|
||||
```
|
||||
|
||||
Si le type de base de données sélectionné est SQLite, la variable `DJANGO_DB_HOST` sera utilisée en guise de chemin vers
|
||||
le fichier de base de données (par défaut, `db.sqlite3`).
|
||||
|
||||
En développement, il est recommandé d'utiliser SQLite pour des raisons de simplicité. Les paramètres de mail ne seront
|
||||
pas utilisés, et les mails qui doivent être envoyés seront envoyés dans la console.
|
||||
pas utilisés, et les mails qui doivent être envoyés seront envoyés dans la console. Les intégrations mail et Matrix
|
||||
seront également désactivées.
|
||||
|
||||
En production, il est recommandé de ne pas utiliser SQLite pour des raisons de performances.
|
||||
|
||||
La dernière différence entre le développment et la production est qu'en développement, chaque modification d'un fichier
|
||||
est détectée et le serveur se relance automatiquement dès lors.
|
||||
|
||||
Une fois le site lancé, le premier compte créé sera un compte administrateur.
|
||||
est détectée et le serveur se relance automatiquement dès lors.
|
@ -1 +1,4 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'api.apps.APIConfig'
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
@ -1,6 +1,8 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework import serializers
|
||||
from member.models import TFJMUser, Authorization, MotivationLetter, Solution, Synthesis
|
||||
from tournament.models import Team, Tournament, Pool
|
||||
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
@ -8,73 +10,10 @@ class UserSerializer(serializers.ModelSerializer):
|
||||
Serialize a User object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
model = User
|
||||
exclude = (
|
||||
'username',
|
||||
'password',
|
||||
'groups',
|
||||
'user_permissions',
|
||||
)
|
||||
|
||||
|
||||
class TeamSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize a Team object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class TournamentSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize a Tournament object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = Tournament
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class AuthorizationSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize an Authorization object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = Authorization
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class MotivationLetterSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize a MotivationLetter object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = MotivationLetter
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SolutionSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize a Solution object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = Solution
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class SynthesisSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize a Synthesis object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = Synthesis
|
||||
fields = "__all__"
|
||||
|
||||
|
||||
class PoolSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
Serialize a Pool object into JSON.
|
||||
"""
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = "__all__"
|
||||
|
27
apps/api/tests.py
Normal file
27
apps/api/tests.py
Normal file
@ -0,0 +1,27 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from unittest.case import skipIf
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
from django.test import TestCase
|
||||
|
||||
|
||||
class TestAPIPages(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_superuser(
|
||||
username="admin",
|
||||
password="apitest",
|
||||
email="",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
def test_user_page(self):
|
||||
response = self.client.get("/api/user/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@skipIf("logs" not in settings.INSTALLED_APPS, reason="logs app is not used")
|
||||
def test_logs_page(self):
|
||||
response = self.client.get("/api/logs/")
|
||||
self.assertEqual(response.status_code, 200)
|
@ -1,20 +1,20 @@
|
||||
from django.conf.urls import url, include
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.conf.urls import include, url
|
||||
from rest_framework import routers
|
||||
|
||||
from .viewsets import UserViewSet, TeamViewSet, TournamentViewSet, AuthorizationViewSet, MotivationLetterViewSet, \
|
||||
SolutionViewSet, SynthesisViewSet, PoolViewSet
|
||||
from .viewsets import UserViewSet
|
||||
|
||||
# Routers provide an easy way of automatically determining the URL conf.
|
||||
# Register each app API router and user viewset
|
||||
router = routers.DefaultRouter()
|
||||
router.register('user', UserViewSet)
|
||||
router.register('team', TeamViewSet)
|
||||
router.register('tournament', TournamentViewSet)
|
||||
router.register('authorization', AuthorizationViewSet)
|
||||
router.register('motivation_letter', MotivationLetterViewSet)
|
||||
router.register('solution', SolutionViewSet)
|
||||
router.register('synthesis', SynthesisViewSet)
|
||||
router.register('pool', PoolViewSet)
|
||||
|
||||
if "logs" in settings.INSTALLED_APPS:
|
||||
from logs.api.urls import register_logs_urls
|
||||
register_logs_urls(router, "logs")
|
||||
|
||||
app_name = 'api'
|
||||
|
||||
|
@ -1,124 +1,20 @@
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework import status
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
from member.models import TFJMUser, Authorization, MotivationLetter, Solution, Synthesis
|
||||
from tournament.models import Team, Tournament, Pool
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .serializers import UserSerializer, TeamSerializer, TournamentSerializer, AuthorizationSerializer, \
|
||||
MotivationLetterSerializer, SolutionSerializer, SynthesisSerializer, PoolSerializer
|
||||
from django.contrib.auth.models import User
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from .serializers import UserSerializer
|
||||
|
||||
|
||||
class UserViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of users.
|
||||
"""
|
||||
queryset = TFJMUser.objects.all()
|
||||
queryset = User.objects.order_by("id").all()
|
||||
serializer_class = UserSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['id', 'first_name', 'last_name', 'email', 'gender', 'student_class', 'role', 'year', 'team',
|
||||
'team__trigram', 'is_superuser', 'is_staff', 'is_active', ]
|
||||
filterset_fields = ['id', 'first_name', 'last_name', 'email', 'is_superuser', 'is_staff', 'is_active', ]
|
||||
search_fields = ['$first_name', '$last_name', ]
|
||||
|
||||
|
||||
class TeamViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of teams.
|
||||
"""
|
||||
queryset = Team.objects.all()
|
||||
serializer_class = TeamSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', 'trigram', 'validation_status', 'selected_for_final', 'access_code', 'tournament',
|
||||
'year', ]
|
||||
search_fields = ['$name', 'trigram', ]
|
||||
|
||||
|
||||
class TournamentViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of tournaments.
|
||||
"""
|
||||
queryset = Tournament.objects.all()
|
||||
serializer_class = TournamentSerializer
|
||||
filter_backends = [DjangoFilterBackend, SearchFilter]
|
||||
filterset_fields = ['name', 'size', 'price', 'date_start', 'date_end', 'final', 'organizers', 'year', ]
|
||||
search_fields = ['$name', ]
|
||||
|
||||
|
||||
class AuthorizationViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of authorizations.
|
||||
"""
|
||||
queryset = Authorization.objects.all()
|
||||
serializer_class = AuthorizationSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['user', 'type', ]
|
||||
|
||||
|
||||
class MotivationLetterViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of motivation letters.
|
||||
"""
|
||||
queryset = MotivationLetter.objects.all()
|
||||
serializer_class = MotivationLetterSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['team', 'team__trigram', ]
|
||||
|
||||
|
||||
class SolutionViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of solutions.
|
||||
"""
|
||||
queryset = Solution.objects.all()
|
||||
serializer_class = SolutionSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['team', 'team__trigram', 'problem', ]
|
||||
|
||||
|
||||
class SynthesisViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of syntheses.
|
||||
"""
|
||||
queryset = Synthesis.objects.all()
|
||||
serializer_class = SynthesisSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['team', 'team__trigram', 'source', 'round', ]
|
||||
|
||||
|
||||
class PoolViewSet(ModelViewSet):
|
||||
"""
|
||||
Display list of pools.
|
||||
If the request is a POST request and the format is "A;X;x;Y;y;Z;z;..." where A = 1 or 1 = 2,
|
||||
X, Y, Z, ... are team trigrams, x, y, z, ... are numbers of problems, then this is interpreted as a
|
||||
creation a pool for the round A with the solutions of problems x, y, z, ... of the teams X, Y, Z, ... respectively.
|
||||
"""
|
||||
queryset = Pool.objects.all()
|
||||
serializer_class = PoolSerializer
|
||||
filter_backends = [DjangoFilterBackend]
|
||||
filterset_fields = ['teams', 'teams__trigram', 'round', ]
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
data = request.data
|
||||
try:
|
||||
spl = data.split(";")
|
||||
if len(spl) >= 7:
|
||||
round = int(spl[0])
|
||||
teams = []
|
||||
solutions = []
|
||||
for i in range((len(spl) - 1) // 2):
|
||||
trigram = spl[1 + 2 * i]
|
||||
pb = int(spl[2 + 2 * i])
|
||||
team = Team.objects.get(trigram=trigram)
|
||||
solution = Solution.objects.get(team=team, problem=pb, final=team.selected_for_final)
|
||||
teams.append(team)
|
||||
solutions.append(solution)
|
||||
pool = Pool.objects.create(round=round)
|
||||
pool.teams.set(teams)
|
||||
pool.solutions.set(solutions)
|
||||
pool.save()
|
||||
serializer = PoolSerializer(pool)
|
||||
headers = self.get_success_headers(serializer.data)
|
||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||
except BaseException: # JSON data
|
||||
pass
|
||||
return super().create(request, *args, **kwargs)
|
4
apps/eastereggs/__init__.py
Normal file
4
apps/eastereggs/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'eastereggs.apps.EastereggsConfig'
|
8
apps/eastereggs/apps.py
Normal file
8
apps/eastereggs/apps.py
Normal file
@ -0,0 +1,8 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EastereggsConfig(AppConfig):
|
||||
name = 'eastereggs'
|
2
apps/eastereggs/migrations/__init__.py
Normal file
2
apps/eastereggs/migrations/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
19
apps/eastereggs/templates/eastereggs/xp.html
Normal file
19
apps/eastereggs/templates/eastereggs/xp.html
Normal file
@ -0,0 +1,19 @@
|
||||
{% extends "index.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div id="index-content"></div>
|
||||
{% include "eastereggs/xp_modal.html" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$("#index-content").load("{% url "index" %} #content");
|
||||
function displayModal() {
|
||||
$("#xpModal").modal('toggle');
|
||||
setTimeout(displayModal, 400);
|
||||
}
|
||||
displayModal();
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
20
apps/eastereggs/templates/eastereggs/xp_modal.html
Normal file
20
apps/eastereggs/templates/eastereggs/xp_modal.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
<div id="xpModal" class="modal fade" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Error" %}</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
{% trans "This task failed successfully." %}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">{% trans "Close" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
11
apps/eastereggs/urls.py
Normal file
11
apps/eastereggs/urls.py
Normal file
@ -0,0 +1,11 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
app_name = "eastereggs"
|
||||
|
||||
urlpatterns = [
|
||||
path("xp/", TemplateView.as_view(template_name="eastereggs/xp.html")),
|
||||
]
|
4
apps/logs/__init__.py
Normal file
4
apps/logs/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'logs.apps.LogsConfig'
|
2
apps/logs/api/__init__.py
Normal file
2
apps/logs/api/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
19
apps/logs/api/serializers.py
Normal file
19
apps/logs/api/serializers.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
from ..models import Changelog
|
||||
|
||||
|
||||
class ChangelogSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
REST API Serializer for Changelog types.
|
||||
The djangorestframework plugin will analyse the model `Changelog` and parse all fields in the API.
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Changelog
|
||||
fields = '__all__'
|
||||
# noinspection PyProtectedMember
|
||||
read_only_fields = [f.name for f in model._meta.get_fields()] # Changelogs are read-only protected
|
11
apps/logs/api/urls.py
Normal file
11
apps/logs/api/urls.py
Normal file
@ -0,0 +1,11 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from .views import ChangelogViewSet
|
||||
|
||||
|
||||
def register_logs_urls(router, path):
|
||||
"""
|
||||
Configure router for Activity REST API.
|
||||
"""
|
||||
router.register(path, ChangelogViewSet)
|
28
apps/logs/api/views.py
Normal file
28
apps/logs/api/views.py
Normal file
@ -0,0 +1,28 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django_filters.rest_framework import DjangoFilterBackend
|
||||
from rest_framework.filters import OrderingFilter
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from .serializers import ChangelogSerializer
|
||||
from ..models import Changelog
|
||||
|
||||
|
||||
class ChangelogViewSet(ModelViewSet):
|
||||
"""
|
||||
REST API View set.
|
||||
The djangorestframework plugin will get all `Changelog` objects, serialize it to JSON with the given serializer,
|
||||
then render it on /api/logs/
|
||||
"""
|
||||
|
||||
def check_permissions(self, request):
|
||||
# Only superusers can get access to logs
|
||||
return self.request.user and self.request.user.is_superuser
|
||||
|
||||
queryset = Changelog.objects.all()
|
||||
serializer_class = ChangelogSerializer
|
||||
filter_backends = [DjangoFilterBackend, OrderingFilter]
|
||||
filterset_fields = ['model', 'action', "instance_pk", 'user', 'ip', ]
|
||||
ordering_fields = ['timestamp', 'id', ]
|
||||
ordering = ['-id', ]
|
18
apps/logs/apps.py
Normal file
18
apps/logs/apps.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_delete, post_save, pre_save
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class LogsConfig(AppConfig):
|
||||
name = 'logs'
|
||||
verbose_name = _('Logs')
|
||||
|
||||
def ready(self):
|
||||
# noinspection PyUnresolvedReferences
|
||||
from . import signals
|
||||
pre_save.connect(signals.pre_save_object)
|
||||
post_save.connect(signals.save_object)
|
||||
post_delete.connect(signals.delete_object)
|
37
apps/logs/migrations/0001_initial.py
Normal file
37
apps/logs/migrations/0001_initial.py
Normal file
@ -0,0 +1,37 @@
|
||||
# Generated by Django 3.0.11 on 2021-01-21 21:06
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('contenttypes', '0002_remove_content_type_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Changelog',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip', models.GenericIPAddressField(blank=True, null=True, verbose_name='IP Address')),
|
||||
('instance_pk', models.CharField(max_length=255, verbose_name='identifier')),
|
||||
('previous', models.TextField(blank=True, default='', verbose_name='previous data')),
|
||||
('data', models.TextField(blank=True, default='', verbose_name='new data')),
|
||||
('action', models.CharField(choices=[('create', 'create'), ('edit', 'edit'), ('delete', 'delete')], default='edit', max_length=16, verbose_name='action')),
|
||||
('timestamp', models.DateTimeField(default=django.utils.timezone.now, verbose_name='timestamp')),
|
||||
('model', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contenttypes.ContentType', verbose_name='model')),
|
||||
('user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='user')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'changelog',
|
||||
'verbose_name_plural': 'changelogs',
|
||||
},
|
||||
),
|
||||
]
|
2
apps/logs/migrations/__init__.py
Normal file
2
apps/logs/migrations/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
88
apps/logs/models.py
Normal file
88
apps/logs/models.py
Normal file
@ -0,0 +1,88 @@
|
||||
# Copyright (C) 2018-2020 by BDE ENS Paris-Saclay
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import models
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class Changelog(models.Model):
|
||||
"""
|
||||
Store each modification in the database (except sessions and logging),
|
||||
including creating, editing and deleting models.
|
||||
"""
|
||||
|
||||
user = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.PROTECT,
|
||||
null=True,
|
||||
verbose_name=_('user'),
|
||||
)
|
||||
|
||||
ip = models.GenericIPAddressField(
|
||||
null=True,
|
||||
blank=True,
|
||||
verbose_name=_("IP Address")
|
||||
)
|
||||
|
||||
model = models.ForeignKey(
|
||||
ContentType,
|
||||
on_delete=models.PROTECT,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_('model'),
|
||||
)
|
||||
|
||||
instance_pk = models.CharField(
|
||||
max_length=255,
|
||||
null=False,
|
||||
blank=False,
|
||||
verbose_name=_('identifier'),
|
||||
)
|
||||
|
||||
previous = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_('previous data'),
|
||||
)
|
||||
|
||||
data = models.TextField(
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_('new data'),
|
||||
)
|
||||
|
||||
action = models.CharField( # create, edit or delete
|
||||
max_length=16,
|
||||
null=False,
|
||||
blank=False,
|
||||
choices=[
|
||||
('create', _('create')),
|
||||
('edit', _('edit')),
|
||||
('delete', _('delete')),
|
||||
],
|
||||
default='edit',
|
||||
verbose_name=_('action'),
|
||||
)
|
||||
|
||||
timestamp = models.DateTimeField(
|
||||
null=False,
|
||||
blank=False,
|
||||
default=timezone.now,
|
||||
name='timestamp',
|
||||
verbose_name=_('timestamp'),
|
||||
)
|
||||
|
||||
def delete(self, using=None, keep_parents=False):
|
||||
raise ValidationError(_("Logs cannot be destroyed."))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("changelog")
|
||||
verbose_name_plural = _("changelogs")
|
||||
|
||||
def __str__(self):
|
||||
return _("Changelog of type \"{action}\" for model {model} at {timestamp}").format(
|
||||
action=self.get_action_display(), model=str(self.model), timestamp=str(self.timestamp))
|
132
apps/logs/signals.py
Normal file
132
apps/logs/signals.py
Normal file
@ -0,0 +1,132 @@
|
||||
import getpass
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from tfjm.middlewares import get_current_authenticated_user, get_current_ip
|
||||
|
||||
from .models import Changelog
|
||||
|
||||
|
||||
# Ces modèles ne nécessitent pas de logs
|
||||
EXCLUDED = [
|
||||
'admin.logentry',
|
||||
'authtoken.token',
|
||||
'contenttypes.contenttype',
|
||||
'logs.changelog', # Never remove this line
|
||||
'mailer.dontsendentry',
|
||||
'mailer.message',
|
||||
'mailer.messagelog',
|
||||
'migrations.migration',
|
||||
'sessions.session',
|
||||
]
|
||||
|
||||
|
||||
def pre_save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Before a model get saved, we get the previous instance that is currently in the database
|
||||
"""
|
||||
qs = sender.objects.filter(pk=instance.pk).all()
|
||||
if qs.exists():
|
||||
instance._previous = qs.get()
|
||||
else:
|
||||
instance._previous = None
|
||||
|
||||
|
||||
def save_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Each time a model is saved, an entry in the table `Changelog` is added in the database
|
||||
in order to store each modification made
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||
return
|
||||
|
||||
# noinspection PyProtectedMember
|
||||
previous = instance._previous
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
||||
|
||||
if user is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
ip = "127.0.0.1"
|
||||
username = getpass.getuser()
|
||||
user = User.objects.get(username=username) if User.objects.filter(username=username).exists() else None
|
||||
|
||||
# On n'enregistre pas les connexions
|
||||
# noinspection PyProtectedMember
|
||||
if user is not None and instance._meta.label_lower == "auth.user" and previous \
|
||||
and instance.last_login != previous.last_login:
|
||||
return
|
||||
|
||||
changed_fields = '__all__'
|
||||
if previous:
|
||||
# On ne garde que les champs modifiés
|
||||
changed_fields = []
|
||||
for field in instance._meta.fields:
|
||||
if field.name.endswith("_ptr"):
|
||||
# A field ending with _ptr is a OneToOneRel with a subclass, e.g. NoteClub.note_ptr -> Note
|
||||
continue
|
||||
if getattr(instance, field.name) != getattr(previous, field.name):
|
||||
changed_fields.append(field.name)
|
||||
|
||||
if len(changed_fields) == 0:
|
||||
# Pas de log s'il n'y a pas de modification
|
||||
return
|
||||
|
||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles avec uniquement les champs modifiés
|
||||
class CustomSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = instance.__class__
|
||||
fields = changed_fields
|
||||
|
||||
previous_json = JSONRenderer().render(CustomSerializer(previous).data).decode("UTF-8") if previous else ""
|
||||
instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
|
||||
|
||||
Changelog.objects.create(user=user,
|
||||
ip=ip,
|
||||
model=ContentType.objects.get_for_model(instance),
|
||||
instance_pk=instance.pk,
|
||||
previous=previous_json,
|
||||
data=instance_json,
|
||||
action=("edit" if previous else "create")
|
||||
).save()
|
||||
|
||||
|
||||
def delete_object(sender, instance, **kwargs):
|
||||
"""
|
||||
Each time a model is deleted, an entry in the table `Changelog` is added in the database
|
||||
"""
|
||||
# noinspection PyProtectedMember
|
||||
if instance._meta.label_lower in EXCLUDED or hasattr(instance, "_no_signal"):
|
||||
return
|
||||
|
||||
# Si un utilisateur est connecté, on récupère l'utilisateur courant ainsi que son adresse IP
|
||||
user, ip = get_current_authenticated_user(), get_current_ip()
|
||||
|
||||
if user is None:
|
||||
# Si la modification n'a pas été faite via le client Web, on suppose que c'est du à `manage.py`
|
||||
# On récupère alors l'utilisateur·trice connecté·e à la VM, et on récupère la note associée
|
||||
ip = "127.0.0.1"
|
||||
username = getpass.getuser()
|
||||
user = User.objects.get(username=username) if User.objects.filter(username=username).exists() else None
|
||||
|
||||
# On crée notre propre sérialiseur JSON pour pouvoir sauvegarder les modèles
|
||||
class CustomSerializer(ModelSerializer):
|
||||
class Meta:
|
||||
model = instance.__class__
|
||||
fields = '__all__'
|
||||
|
||||
instance_json = JSONRenderer().render(CustomSerializer(instance).data).decode("UTF-8")
|
||||
|
||||
Changelog.objects.create(user=user,
|
||||
ip=ip,
|
||||
model=ContentType.objects.get_for_model(instance),
|
||||
instance_pk=instance.pk,
|
||||
previous=instance_json,
|
||||
data="",
|
||||
action="delete"
|
||||
)
|
21
apps/logs/tests.py
Normal file
21
apps/logs/tests.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.test import TestCase
|
||||
|
||||
from .models import Changelog
|
||||
|
||||
|
||||
class TestChangelog(TestCase):
|
||||
def test_logs(self):
|
||||
user = User.objects.create(email="admin@example.com")
|
||||
self.assertTrue(Changelog.objects.filter(action="create", instance_pk=user.pk,
|
||||
model=ContentType.objects.get_for_model(User)).exists())
|
||||
old_user_pk = user.pk
|
||||
user.delete()
|
||||
self.assertTrue(Changelog.objects.filter(action="delete", instance_pk=old_user_pk,
|
||||
model=ContentType.objects.get_for_model(User)).exists())
|
||||
|
||||
changelog = Changelog.objects.first()
|
||||
self.assertRaises(ValidationError, changelog.delete)
|
||||
str(Changelog.objects.all())
|
@ -1 +0,0 @@
|
||||
default_app_config = 'member.apps.MemberConfig'
|
@ -1,56 +0,0 @@
|
||||
from django.contrib.auth.admin import admin
|
||||
from polymorphic.admin import PolymorphicParentModelAdmin, PolymorphicChildModelAdmin
|
||||
from member.models import TFJMUser, Document, Solution, Synthesis, MotivationLetter, Authorization, Config
|
||||
|
||||
|
||||
@admin.register(TFJMUser)
|
||||
class TFJMUserAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for users.
|
||||
"""
|
||||
list_display = ('email', 'first_name', 'last_name', 'role', )
|
||||
search_fields = ('last_name', 'first_name',)
|
||||
|
||||
|
||||
@admin.register(Document)
|
||||
class DocumentAdmin(PolymorphicParentModelAdmin):
|
||||
"""
|
||||
Django admin page for any documents.
|
||||
"""
|
||||
child_models = (Authorization, MotivationLetter, Solution, Synthesis,)
|
||||
polymorphic_list = True
|
||||
|
||||
|
||||
@admin.register(Authorization)
|
||||
class AuthorizationAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Django admin page for Authorization.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(MotivationLetter)
|
||||
class MotivationLetterAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Django admin page for Motivation letters.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Solution)
|
||||
class SolutionAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Django admin page for solutions.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Synthesis)
|
||||
class SynthesisAdmin(PolymorphicChildModelAdmin):
|
||||
"""
|
||||
Django admin page for syntheses.
|
||||
"""
|
||||
|
||||
|
||||
@admin.register(Config)
|
||||
class ConfigAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Django admin page for configurations.
|
||||
"""
|
@ -1,10 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
class MemberConfig(AppConfig):
|
||||
"""
|
||||
The member app handles the information that concern a user, its documents, ...
|
||||
"""
|
||||
name = 'member'
|
||||
verbose_name = _('member')
|
@ -1,73 +0,0 @@
|
||||
from django.contrib.auth.forms import UserCreationForm
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import TFJMUser
|
||||
|
||||
|
||||
class SignUpForm(UserCreationForm):
|
||||
"""
|
||||
Coaches and participants register on the website through this form.
|
||||
TODO: Check if this form works, render it better
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["first_name"].required = True
|
||||
self.fields["last_name"].required = True
|
||||
self.fields["role"].choices = [
|
||||
('', _("Choose a role...")),
|
||||
('3participant', _("Participant")),
|
||||
('2coach', _("Coach")),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = (
|
||||
'role',
|
||||
'email',
|
||||
'first_name',
|
||||
'last_name',
|
||||
'birth_date',
|
||||
'gender',
|
||||
'address',
|
||||
'postal_code',
|
||||
'city',
|
||||
'country',
|
||||
'phone_number',
|
||||
'school',
|
||||
'student_class',
|
||||
'responsible_name',
|
||||
'responsible_phone',
|
||||
'responsible_email',
|
||||
'description',
|
||||
)
|
||||
|
||||
|
||||
class TFJMUserForm(forms.ModelForm):
|
||||
"""
|
||||
Form to update our own information when we are participant.
|
||||
"""
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = ('last_name', 'first_name', 'email', 'phone_number', 'gender', 'birth_date', 'address', 'postal_code',
|
||||
'city', 'country', 'school', 'student_class', 'responsible_name', 'responsible_phone',
|
||||
'responsible_email',)
|
||||
|
||||
|
||||
class CoachUserForm(forms.ModelForm):
|
||||
"""
|
||||
Form to update our own information when we are coach.
|
||||
"""
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = ('last_name', 'first_name', 'email', 'phone_number', 'gender', 'birth_date', 'address', 'postal_code',
|
||||
'city', 'country', 'description',)
|
||||
|
||||
|
||||
class AdminUserForm(forms.ModelForm):
|
||||
"""
|
||||
Form to update our own information when we are organizer or admin.
|
||||
"""
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = ('last_name', 'first_name', 'email', 'phone_number', 'description',)
|
@ -1,32 +0,0 @@
|
||||
import os
|
||||
from datetime import date
|
||||
from getpass import getpass
|
||||
from django.core.management import BaseCommand
|
||||
from member.models import TFJMUser
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Little script that generate a superuser.
|
||||
"""
|
||||
email = input("Email: ")
|
||||
password = "1"
|
||||
confirm_password = "2"
|
||||
while password != confirm_password:
|
||||
password = getpass("Password: ")
|
||||
confirm_password = getpass("Confirm password: ")
|
||||
if password != confirm_password:
|
||||
self.stderr.write(self.style.ERROR("Passwords don't match."))
|
||||
|
||||
user = TFJMUser.objects.create(
|
||||
email=email,
|
||||
password="",
|
||||
role="admin",
|
||||
year=os.getenv("TFJM_YEAR", date.today().year),
|
||||
is_active=True,
|
||||
is_staff=True,
|
||||
is_superuser=True,
|
||||
)
|
||||
user.set_password(password)
|
||||
user.save()
|
@ -1,75 +0,0 @@
|
||||
import os
|
||||
from urllib.request import urlretrieve
|
||||
from shutil import copyfile
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils import translation
|
||||
from member.models import Solution
|
||||
from tournament.models import Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
PROBLEMS = [
|
||||
'Création de puzzles',
|
||||
'Départ en vacances',
|
||||
'Un festin stratégique',
|
||||
'Sauver les meubles',
|
||||
'Prêt à décoller !',
|
||||
'Ils nous espionnent !',
|
||||
'De joyeux bûcherons',
|
||||
'Robots auto-réplicateurs',
|
||||
]
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('dir',
|
||||
type=str,
|
||||
default='.',
|
||||
help="Directory where solutions should be saved.")
|
||||
parser.add_argument('--language', '-l',
|
||||
type=str,
|
||||
choices=['en', 'fr'],
|
||||
default='fr',
|
||||
help="Language of the title of the files.")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Copy solutions elsewhere.
|
||||
"""
|
||||
d = options['dir']
|
||||
teams_dir = d + '/Par équipe'
|
||||
os.makedirs(teams_dir, exist_ok=True)
|
||||
|
||||
translation.activate(options['language'])
|
||||
|
||||
copied = 0
|
||||
|
||||
for tournament in Tournament.objects.all():
|
||||
os.mkdir(teams_dir + '/' + tournament.name)
|
||||
for team in tournament.teams.filter(validation_status='2valid'):
|
||||
os.mkdir(teams_dir + '/' + tournament.name + '/' + str(team))
|
||||
for sol in tournament.solutions:
|
||||
if not os.path.isfile('media/' + sol.file.name):
|
||||
self.stdout.write(self.style.WARNING(("Warning: solution '{sol}' is not found. Maybe the file"
|
||||
"was deleted?").format(sol=str(sol))))
|
||||
continue
|
||||
copyfile('media/' + sol.file.name, teams_dir + '/' + tournament.name
|
||||
+ '/' + str(sol.team) + '/' + str(sol) + '.pdf')
|
||||
copied += 1
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Successfully copied {copied} solutions!".format(copied=copied)))
|
||||
|
||||
os.mkdir(d + '/Par problème')
|
||||
|
||||
for pb in range(1, 9):
|
||||
sols = Solution.objects.filter(problem=pb).all()
|
||||
pbdir = d + '/Par problème/Problème n°{number} — {problem}'.format(number=pb, problem=self.PROBLEMS[pb - 1])
|
||||
os.mkdir(pbdir)
|
||||
for sol in sols:
|
||||
os.symlink('../../Par équipe/' + sol.tournament.name + '/' + str(sol.team) + '/' + str(sol) + '.pdf',
|
||||
pbdir + '/' + str(sol) + '.pdf')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Symlinks by problem created!"))
|
||||
|
||||
urlretrieve('https://tfjm.org/wp-content/uploads/2020/01/Problemes2020_23_01_v1_1.pdf', d + '/Énoncés.pdf')
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Questions retrieved!"))
|
@ -1,309 +0,0 @@
|
||||
import os
|
||||
|
||||
from django.core.management import BaseCommand, CommandError
|
||||
from django.db import transaction
|
||||
from member.models import TFJMUser, Document, Solution, Synthesis, Authorization, MotivationLetter
|
||||
from tournament.models import Team, Tournament
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
"""
|
||||
Import the old database.
|
||||
Tables must be found into the import_olddb folder, as CSV files.
|
||||
"""
|
||||
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument('--tournaments', '-t', action="store", help="Import tournaments")
|
||||
parser.add_argument('--teams', '-T', action="store", help="Import teams")
|
||||
parser.add_argument('--users', '-u', action="store", help="Import users")
|
||||
parser.add_argument('--documents', '-d', action="store", help="Import all documents")
|
||||
|
||||
def handle(self, *args, **options):
|
||||
if "tournaments" in options:
|
||||
self.import_tournaments()
|
||||
|
||||
if "teams" in options:
|
||||
self.import_teams()
|
||||
|
||||
if "users" in options:
|
||||
self.import_users()
|
||||
|
||||
if "documents" in options:
|
||||
self.import_documents()
|
||||
|
||||
@transaction.atomic
|
||||
def import_tournaments(self):
|
||||
"""
|
||||
Import tournaments into the new database.
|
||||
"""
|
||||
print("Importing tournaments...")
|
||||
with open("import_olddb/tournaments.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Tournament.objects.filter(pk=args[0]).exists():
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"id": args[0],
|
||||
"name": args[1],
|
||||
"size": args[2],
|
||||
"place": args[3],
|
||||
"price": args[4],
|
||||
"description": args[5],
|
||||
"date_start": args[6],
|
||||
"date_end": args[7],
|
||||
"date_inscription": args[8],
|
||||
"date_solutions": args[9],
|
||||
"date_syntheses": args[10],
|
||||
"date_solutions_2": args[11],
|
||||
"date_syntheses_2": args[12],
|
||||
"final": args[13],
|
||||
"year": args[14],
|
||||
}
|
||||
with transaction.atomic():
|
||||
Tournament.objects.create(**obj_dict)
|
||||
print(self.style.SUCCESS("Tournaments imported"))
|
||||
|
||||
@staticmethod
|
||||
def validation_status(status):
|
||||
if status == "NOT_READY":
|
||||
return "0invalid"
|
||||
elif status == "WAITING":
|
||||
return "1waiting"
|
||||
elif status == "VALIDATED":
|
||||
return "2valid"
|
||||
else:
|
||||
raise CommandError("Unknown status: {}".format(status))
|
||||
|
||||
@transaction.atomic
|
||||
def import_teams(self):
|
||||
"""
|
||||
Import teams into new database.
|
||||
"""
|
||||
self.stdout.write("Importing teams...")
|
||||
with open("import_olddb/teams.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Team.objects.filter(pk=args[0]).exists():
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"id": args[0],
|
||||
"name": args[1],
|
||||
"trigram": args[2],
|
||||
"tournament": Tournament.objects.get(pk=args[3]),
|
||||
"inscription_date": args[13],
|
||||
"validation_status": Command.validation_status(args[14]),
|
||||
"selected_for_final": args[15],
|
||||
"access_code": args[16],
|
||||
"year": args[17],
|
||||
}
|
||||
with transaction.atomic():
|
||||
Team.objects.create(**obj_dict)
|
||||
print(self.style.SUCCESS("Teams imported"))
|
||||
|
||||
@staticmethod
|
||||
def role(role):
|
||||
if role == "ADMIN":
|
||||
return "0admin"
|
||||
elif role == "ORGANIZER":
|
||||
return "1volunteer"
|
||||
elif role == "ENCADRANT":
|
||||
return "2coach"
|
||||
elif role == "PARTICIPANT":
|
||||
return "3participant"
|
||||
else:
|
||||
raise CommandError("Unknown role: {}".format(role))
|
||||
|
||||
@transaction.atomic
|
||||
def import_users(self):
|
||||
"""
|
||||
Import users into the new database.
|
||||
:return:
|
||||
"""
|
||||
self.stdout.write("Importing users...")
|
||||
with open("import_olddb/users.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if TFJMUser.objects.filter(pk=args[0]).exists():
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"id": args[0],
|
||||
"email": args[1],
|
||||
"username": args[1],
|
||||
"password": "bcrypt$" + args[2],
|
||||
"last_name": args[3],
|
||||
"first_name": args[4],
|
||||
"birth_date": args[5],
|
||||
"gender": "male" if args[6] == "M" else "female",
|
||||
"address": args[7],
|
||||
"postal_code": args[8],
|
||||
"city": args[9],
|
||||
"country": args[10],
|
||||
"phone_number": args[11],
|
||||
"school": args[12],
|
||||
"student_class": args[13].lower().replace('premiere', 'première') if args[13] else None,
|
||||
"responsible_name": args[14],
|
||||
"responsible_phone": args[15],
|
||||
"responsible_email": args[16],
|
||||
"description": args[17].replace("\\n", "\n") if args[17] else None,
|
||||
"role": Command.role(args[18]),
|
||||
"team": Team.objects.get(pk=args[19]) if args[19] else None,
|
||||
"year": args[20],
|
||||
"date_joined": args[23],
|
||||
"is_active": args[18] == "ADMIN" or os.getenv("TFJM_STAGE", "dev") == "prod",
|
||||
"is_staff": args[18] == "ADMIN",
|
||||
"is_superuser": args[18] == "ADMIN",
|
||||
}
|
||||
with transaction.atomic():
|
||||
TFJMUser.objects.create(**obj_dict)
|
||||
self.stdout.write(self.style.SUCCESS("Users imported"))
|
||||
|
||||
self.stdout.write("Importing organizers...")
|
||||
# We also import the information about the organizers of a tournament.
|
||||
with open("import_olddb/organizers.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
with transaction.atomic():
|
||||
tournament = Tournament.objects.get(pk=args[2])
|
||||
organizer = TFJMUser.objects.get(pk=args[1])
|
||||
tournament.organizers.add(organizer)
|
||||
tournament.save()
|
||||
self.stdout.write(self.style.SUCCESS("Organizers imported"))
|
||||
|
||||
@transaction.atomic
|
||||
def import_documents(self):
|
||||
"""
|
||||
Import the documents (authorizations, motivation letters, solutions, syntheses) from the old database.
|
||||
"""
|
||||
self.stdout.write("Importing documents...")
|
||||
with open("import_olddb/documents.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Document.objects.filter(file=args[0]).exists():
|
||||
doc = Document.objects.get(file=args[0])
|
||||
doc.uploaded_at = args[5].replace(" ", "T")
|
||||
doc.save()
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"file": args[0],
|
||||
"uploaded_at": args[5],
|
||||
}
|
||||
if args[4] != "MOTIVATION_LETTER":
|
||||
obj_dict["user"] = TFJMUser.objects.get(args[1]),
|
||||
obj_dict["type"] = args[4].lower()
|
||||
else:
|
||||
try:
|
||||
obj_dict["team"] = Team.objects.get(pk=args[2])
|
||||
except Team.DoesNotExist:
|
||||
print("Team with pk {} does not exist, ignoring".format(args[2]))
|
||||
continue
|
||||
with transaction.atomic():
|
||||
if args[4] != "MOTIVATION_LETTER":
|
||||
Authorization.objects.create(**obj_dict)
|
||||
else:
|
||||
MotivationLetter.objects.create(**obj_dict)
|
||||
self.stdout.write(self.style.SUCCESS("Authorizations imported"))
|
||||
|
||||
with open("import_olddb/solutions.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Document.objects.filter(file=args[0]).exists():
|
||||
doc = Document.objects.get(file=args[0])
|
||||
doc.uploaded_at = args[4].replace(" ", "T")
|
||||
doc.save()
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"file": args[0],
|
||||
"team": Team.objects.get(pk=args[1]),
|
||||
"problem": args[3],
|
||||
"uploaded_at": args[4],
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
Solution.objects.create(**obj_dict)
|
||||
except:
|
||||
print("Solution exists")
|
||||
self.stdout.write(self.style.SUCCESS("Solutions imported"))
|
||||
|
||||
with open("import_olddb/syntheses.csv") as f:
|
||||
first_line = True
|
||||
for line in f:
|
||||
if first_line:
|
||||
first_line = False
|
||||
continue
|
||||
|
||||
line = line[:-1].replace("\"", "")
|
||||
args = line.split(";")
|
||||
args = [arg if arg and arg != "NULL" else None for arg in args]
|
||||
|
||||
if Document.objects.filter(file=args[0]).exists():
|
||||
doc = Document.objects.get(file=args[0])
|
||||
doc.uploaded_at = args[5].replace(" ", "T")
|
||||
doc.save()
|
||||
continue
|
||||
|
||||
obj_dict = {
|
||||
"file": args[0],
|
||||
"team": Team.objects.get(pk=args[1]),
|
||||
"source": "opponent" if args[3] == "1" else "rapporteur",
|
||||
"round": args[4],
|
||||
"uploaded_at": args[5],
|
||||
}
|
||||
with transaction.atomic():
|
||||
try:
|
||||
Synthesis.objects.create(**obj_dict)
|
||||
except:
|
||||
print("Synthesis exists")
|
||||
self.stdout.write(self.style.SUCCESS("Syntheses imported"))
|
@ -1,368 +0,0 @@
|
||||
import os
|
||||
from datetime import date
|
||||
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from polymorphic.models import PolymorphicModel
|
||||
|
||||
from tournament.models import Team, Tournament
|
||||
|
||||
|
||||
class TFJMUser(AbstractUser):
|
||||
"""
|
||||
The model of registered users (organizers/juries/admins/coachs/participants)
|
||||
"""
|
||||
USERNAME_FIELD = 'email'
|
||||
REQUIRED_FIELDS = []
|
||||
|
||||
email = models.EmailField(
|
||||
unique=True,
|
||||
verbose_name=_("email"),
|
||||
help_text=_("This should be valid and will be controlled."),
|
||||
)
|
||||
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="users",
|
||||
verbose_name=_("team"),
|
||||
help_text=_("Concerns only coaches and participants."),
|
||||
)
|
||||
|
||||
birth_date = models.DateField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("birth date"),
|
||||
)
|
||||
|
||||
gender = models.CharField(
|
||||
max_length=16,
|
||||
null=True,
|
||||
default=None,
|
||||
choices=[
|
||||
("male", _("Male")),
|
||||
("female", _("Female")),
|
||||
("non-binary", _("Non binary")),
|
||||
],
|
||||
verbose_name=_("gender"),
|
||||
)
|
||||
|
||||
address = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("address"),
|
||||
)
|
||||
|
||||
postal_code = models.PositiveIntegerField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("postal code"),
|
||||
)
|
||||
|
||||
city = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("city"),
|
||||
)
|
||||
|
||||
country = models.CharField(
|
||||
max_length=255,
|
||||
default="France",
|
||||
null=True,
|
||||
verbose_name=_("country"),
|
||||
)
|
||||
|
||||
phone_number = models.CharField(
|
||||
max_length=20,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name=_("phone number"),
|
||||
)
|
||||
|
||||
school = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("school"),
|
||||
)
|
||||
|
||||
student_class = models.CharField(
|
||||
max_length=16,
|
||||
choices=[
|
||||
('seconde', _("Seconde or less")),
|
||||
('première', _("Première")),
|
||||
('terminale', _("Terminale")),
|
||||
],
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name="class",
|
||||
)
|
||||
|
||||
responsible_name = models.CharField(
|
||||
max_length=255,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("responsible name"),
|
||||
)
|
||||
|
||||
responsible_phone = models.CharField(
|
||||
max_length=20,
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("responsible phone"),
|
||||
)
|
||||
|
||||
responsible_email = models.EmailField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("responsible email"),
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("description"),
|
||||
)
|
||||
|
||||
role = models.CharField(
|
||||
max_length=16,
|
||||
choices=[
|
||||
("0admin", _("Admin")),
|
||||
("1volunteer", _("Organizer")),
|
||||
("2coach", _("Coach")),
|
||||
("3participant", _("Participant")),
|
||||
]
|
||||
)
|
||||
|
||||
year = models.PositiveIntegerField(
|
||||
default=os.getenv("TFJM_YEAR", date.today().year),
|
||||
verbose_name=_("year"),
|
||||
)
|
||||
|
||||
@property
|
||||
def participates(self):
|
||||
"""
|
||||
Return True iff this user is a participant or a coach, ie. if the user is a member of a team that worked
|
||||
for the tournament.
|
||||
"""
|
||||
return self.role == "3participant" or self.role == "2coach"
|
||||
|
||||
@property
|
||||
def organizes(self):
|
||||
"""
|
||||
Return True iff this user is a local or global organizer of the tournament. This includes juries.
|
||||
"""
|
||||
return self.role == "1volunteer" or self.role == "0admin"
|
||||
|
||||
@property
|
||||
def admin(self):
|
||||
"""
|
||||
Return True iff this user is a global organizer, ie. an administrator. This should be equivalent to be
|
||||
a superuser.
|
||||
"""
|
||||
return self.role == "0admin"
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("user")
|
||||
verbose_name_plural = _("users")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# We ensure that the username is the email of the user.
|
||||
self.username = self.email
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return self.first_name + " " + self.last_name
|
||||
|
||||
|
||||
class Document(PolymorphicModel):
|
||||
"""
|
||||
Abstract model of any saved document (solution, synthesis, motivation letter, authorization)
|
||||
"""
|
||||
file = models.FileField(
|
||||
unique=True,
|
||||
verbose_name=_("file"),
|
||||
)
|
||||
|
||||
uploaded_at = models.DateTimeField(
|
||||
auto_now_add=True,
|
||||
verbose_name=_("uploaded at"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("document")
|
||||
verbose_name_plural = _("documents")
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
self.file.delete(True)
|
||||
return super().delete(*args, **kwargs)
|
||||
|
||||
|
||||
class Authorization(Document):
|
||||
"""
|
||||
Model for authorization papers (parental consent, photo consent, sanitary plug, ...)
|
||||
"""
|
||||
user = models.ForeignKey(
|
||||
TFJMUser,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="authorizations",
|
||||
verbose_name=_("user"),
|
||||
)
|
||||
|
||||
type = models.CharField(
|
||||
max_length=32,
|
||||
choices=[
|
||||
("parental_consent", _("Parental consent")),
|
||||
("photo_consent", _("Photo consent")),
|
||||
("sanitary_plug", _("Sanitary plug")),
|
||||
("scholarship", _("Scholarship")),
|
||||
],
|
||||
verbose_name=_("type"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("authorization")
|
||||
verbose_name_plural = _("authorizations")
|
||||
|
||||
def __str__(self):
|
||||
return _("{authorization} for user {user}").format(authorization=self.type, user=str(self.user))
|
||||
|
||||
|
||||
class MotivationLetter(Document):
|
||||
"""
|
||||
Model for motivation letters of a team.
|
||||
"""
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="motivation_letters",
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("motivation letter")
|
||||
verbose_name_plural = _("motivation letters")
|
||||
|
||||
def __str__(self):
|
||||
return _("Motivation letter of team {team} ({trigram})").format(team=self.team.name, trigram=self.team.trigram)
|
||||
|
||||
|
||||
class Solution(Document):
|
||||
"""
|
||||
Model for solutions of team for a given problem, for the regional or final tournament.
|
||||
"""
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="solutions",
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
problem = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("problem"),
|
||||
)
|
||||
|
||||
final = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("final solution"),
|
||||
)
|
||||
|
||||
@property
|
||||
def tournament(self):
|
||||
"""
|
||||
Get the concerned tournament of a solution.
|
||||
Generally the local tournament of a team, but it can be the final tournament if this is a solution for the
|
||||
final tournament.
|
||||
"""
|
||||
return Tournament.get_final() if self.final else self.team.tournament
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("solution")
|
||||
verbose_name_plural = _("solutions")
|
||||
unique_together = ('team', 'problem', 'final',)
|
||||
|
||||
def __str__(self):
|
||||
if self.final:
|
||||
return _("Solution of team {trigram} for problem {problem} for final")\
|
||||
.format(trigram=self.team.trigram, problem=self.problem)
|
||||
else:
|
||||
return _("Solution of team {trigram} for problem {problem}")\
|
||||
.format(trigram=self.team.trigram, problem=self.problem)
|
||||
|
||||
|
||||
class Synthesis(Document):
|
||||
"""
|
||||
Model for syntheses of a team for a given round and for a given role, for the regional or final tournament.
|
||||
"""
|
||||
team = models.ForeignKey(
|
||||
Team,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="syntheses",
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
source = models.CharField(
|
||||
max_length=16,
|
||||
choices=[
|
||||
("opponent", _("Opponent")),
|
||||
("rapporteur", _("Rapporteur")),
|
||||
],
|
||||
verbose_name=_("source"),
|
||||
)
|
||||
|
||||
round = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, _("Round 1")),
|
||||
(2, _("Round 2")),
|
||||
],
|
||||
verbose_name=_("round"),
|
||||
)
|
||||
|
||||
final = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("final synthesis"),
|
||||
)
|
||||
|
||||
@property
|
||||
def tournament(self):
|
||||
"""
|
||||
Get the concerned tournament of a solution.
|
||||
Generally the local tournament of a team, but it can be the final tournament if this is a solution for the
|
||||
final tournament.
|
||||
"""
|
||||
return Tournament.get_final() if self.final else self.team.tournament
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("synthesis")
|
||||
verbose_name_plural = _("syntheses")
|
||||
unique_together = ('team', 'source', 'round', 'final',)
|
||||
|
||||
def __str__(self):
|
||||
return _("Synthesis of team {trigram} that is {source} for the round {round} of tournament {tournament}")\
|
||||
.format(trigram=self.team.trigram, source=self.get_source_display().lower(), round=self.round,
|
||||
tournament=self.tournament)
|
||||
|
||||
|
||||
class Config(models.Model):
|
||||
"""
|
||||
Dictionary of configuration variables.
|
||||
"""
|
||||
key = models.CharField(
|
||||
max_length=255,
|
||||
primary_key=True,
|
||||
verbose_name=_("key"),
|
||||
)
|
||||
|
||||
value = models.TextField(
|
||||
default="",
|
||||
verbose_name=_("value"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("configuration")
|
||||
verbose_name_plural = _("configurations")
|
@ -1,26 +0,0 @@
|
||||
import django_tables2 as tables
|
||||
from django_tables2 import A
|
||||
|
||||
from .models import TFJMUser
|
||||
|
||||
|
||||
class UserTable(tables.Table):
|
||||
"""
|
||||
Table of users that are matched with a given queryset.
|
||||
"""
|
||||
last_name = tables.LinkColumn(
|
||||
"member:information",
|
||||
args=[A("pk")],
|
||||
)
|
||||
|
||||
first_name = tables.LinkColumn(
|
||||
"member:information",
|
||||
args=[A("pk")],
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = TFJMUser
|
||||
fields = ("last_name", "first_name", "role", "date_joined", )
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped table-hover'
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
from django import template
|
||||
|
||||
import os
|
||||
|
||||
from member.models import Config
|
||||
|
||||
|
||||
def get_config(value):
|
||||
"""
|
||||
Return a value stored into the config table in the database with a given key.
|
||||
"""
|
||||
config = Config.objects.get_or_create(key=value)[0]
|
||||
return config.value
|
||||
|
||||
|
||||
def get_env(value):
|
||||
"""
|
||||
Get a specified environment variable.
|
||||
"""
|
||||
return os.getenv(value)
|
||||
|
||||
|
||||
register = template.Library()
|
||||
register.filter('get_config', get_config)
|
||||
register.filter('get_env', get_env)
|
@ -1,19 +0,0 @@
|
||||
from django.urls import path
|
||||
|
||||
from .views import CreateUserView, MyAccountView, UserDetailView, AddTeamView, JoinTeamView, MyTeamView,\
|
||||
ProfileListView, OrphanedProfileListView, OrganizersListView, ResetAdminView
|
||||
|
||||
app_name = "member"
|
||||
|
||||
urlpatterns = [
|
||||
path('signup/', CreateUserView.as_view(), name="signup"),
|
||||
path("my-account/", MyAccountView.as_view(), name="my_account"),
|
||||
path("information/<int:pk>/", UserDetailView.as_view(), name="information"),
|
||||
path("add-team/", AddTeamView.as_view(), name="add_team"),
|
||||
path("join-team/", JoinTeamView.as_view(), name="join_team"),
|
||||
path("my-team/", MyTeamView.as_view(), name="my_team"),
|
||||
path("profiles/", ProfileListView.as_view(), name="all_profiles"),
|
||||
path("orphaned-profiles/", OrphanedProfileListView.as_view(), name="orphaned_profiles"),
|
||||
path("organizers/", OrganizersListView.as_view(), name="organizers"),
|
||||
path("reset-admin/", ResetAdminView.as_view(), name="reset_admin"),
|
||||
]
|
@ -1,292 +0,0 @@
|
||||
import random
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q
|
||||
from django.http import FileResponse, Http404
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.views.generic import CreateView, UpdateView, DetailView, FormView
|
||||
from django_tables2 import SingleTableView
|
||||
from tournament.forms import TeamForm, JoinTeam
|
||||
from tournament.models import Team, Tournament, Pool
|
||||
from tournament.views import AdminMixin, TeamMixin, OrgaMixin
|
||||
|
||||
from .forms import SignUpForm, TFJMUserForm, AdminUserForm, CoachUserForm
|
||||
from .models import TFJMUser, Document, Solution, MotivationLetter, Synthesis
|
||||
from .tables import UserTable
|
||||
|
||||
|
||||
class CreateUserView(CreateView):
|
||||
"""
|
||||
Signup form view.
|
||||
"""
|
||||
model = TFJMUser
|
||||
form_class = SignUpForm
|
||||
template_name = "registration/signup.html"
|
||||
|
||||
# When errors are reported from the signup view, don't send passwords to admins
|
||||
@method_decorator(sensitive_post_parameters('password1', 'password2',))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('index')
|
||||
|
||||
|
||||
class MyAccountView(LoginRequiredMixin, UpdateView):
|
||||
"""
|
||||
Update our personal data.
|
||||
"""
|
||||
model = TFJMUser
|
||||
template_name = "member/my_account.html"
|
||||
|
||||
def get_form_class(self):
|
||||
# The used form can change according to the role of the user.
|
||||
return AdminUserForm if self.request.user.organizes else TFJMUserForm \
|
||||
if self.request.user.role == "3participant" else CoachUserForm
|
||||
|
||||
def get_object(self, queryset=None):
|
||||
return self.request.user
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy('member:my_account')
|
||||
|
||||
|
||||
class UserDetailView(LoginRequiredMixin, DetailView):
|
||||
"""
|
||||
View the personal information of a given user.
|
||||
Only organizers can see this page, since there are personal data.
|
||||
"""
|
||||
model = TFJMUser
|
||||
form_class = TFJMUserForm
|
||||
context_object_name = "tfjmuser"
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if isinstance(request.user, AnonymousUser):
|
||||
raise PermissionDenied
|
||||
|
||||
self.object = self.get_object()
|
||||
|
||||
if not request.user.admin \
|
||||
and (self.object.team is not None and request.user not in self.object.team.tournament.organizers.all())\
|
||||
and (self.object.team is not None and self.object.team.selected_for_final
|
||||
and request.user not in Tournament.get_final().organizers.all())\
|
||||
and self.request.user != self.object:
|
||||
raise PermissionDenied
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
"""
|
||||
An administrator can log in through this page as someone else, and act as this other person.
|
||||
"""
|
||||
if "view_as" in request.POST and self.request.user.admin:
|
||||
session = request.session
|
||||
session["admin"] = request.user.pk
|
||||
obj = self.get_object()
|
||||
session["_fake_user_id"] = obj.pk
|
||||
return redirect(request.path)
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
|
||||
context["title"] = str(self.object)
|
||||
|
||||
return context
|
||||
|
||||
|
||||
class AddTeamView(LoginRequiredMixin, CreateView):
|
||||
"""
|
||||
Register a new team.
|
||||
Users can choose the name, the trigram and a preferred tournament.
|
||||
"""
|
||||
model = Team
|
||||
form_class = TeamForm
|
||||
|
||||
def form_valid(self, form):
|
||||
if self.request.user.organizes:
|
||||
form.add_error('name', _("You can't organize and participate at the same time."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if self.request.user.team:
|
||||
form.add_error('name', _("You are already in a team."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
# Generate a random access code
|
||||
team = form.instance
|
||||
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
code = ""
|
||||
for i in range(6):
|
||||
code += random.choice(alphabet)
|
||||
team.access_code = code
|
||||
team.validation_status = "0invalid"
|
||||
|
||||
team.save()
|
||||
team.refresh_from_db()
|
||||
|
||||
self.request.user.team = team
|
||||
self.request.user.save()
|
||||
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("member:my_team")
|
||||
|
||||
|
||||
class JoinTeamView(LoginRequiredMixin, FormView):
|
||||
"""
|
||||
Join a team with a given access code.
|
||||
"""
|
||||
model = Team
|
||||
form_class = JoinTeam
|
||||
template_name = "tournament/team_form.html"
|
||||
|
||||
def form_valid(self, form):
|
||||
team = form.cleaned_data["team"]
|
||||
|
||||
if self.request.user.organizes:
|
||||
form.add_error('access_code', _("You can't organize and participate at the same time."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if self.request.user.team:
|
||||
form.add_error('access_code', _("You are already in a team."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if self.request.user.role == '2coach' and len(team.coaches) == 3:
|
||||
form.add_error('access_code', _("This team is full of coachs."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if self.request.user.role == '3participant' and len(team.participants) == 6:
|
||||
form.add_error('access_code', _("This team is full of participants."))
|
||||
return self.form_invalid(form)
|
||||
|
||||
if not team.invalid:
|
||||
form.add_error('access_code', _("This team is already validated or waiting for validation."))
|
||||
|
||||
self.request.user.team = team
|
||||
self.request.user.save()
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_success_url(self):
|
||||
return reverse_lazy("member:my_team")
|
||||
|
||||
|
||||
class MyTeamView(TeamMixin, View):
|
||||
"""
|
||||
Redirect to the page of the information of our personal team.
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
return redirect("tournament:team_detail", pk=request.user.team.pk)
|
||||
|
||||
|
||||
class DocumentView(AccessMixin, View):
|
||||
"""
|
||||
View a PDF document, if we have the right.
|
||||
|
||||
- Everyone can see the documents that concern itself.
|
||||
- An administrator can see anything.
|
||||
- An organizer can see documents that are related to its tournament.
|
||||
- A jury can see solutions and syntheses that are evaluated in their pools.
|
||||
"""
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
try:
|
||||
doc = Document.objects.get(file=self.kwargs["file"])
|
||||
except Document.DoesNotExist:
|
||||
raise Http404(_("No %(verbose_name)s found matching the query") %
|
||||
{'verbose_name': Document._meta.verbose_name})
|
||||
|
||||
if request.user.is_authenticated:
|
||||
grant = request.user.admin
|
||||
|
||||
if isinstance(doc, Solution) or isinstance(doc, Synthesis):
|
||||
grant = grant or doc.team == request.user.team or request.user in doc.tournament.organizers.all()
|
||||
elif isinstance(doc, MotivationLetter):
|
||||
grant = grant or doc.team == request.user.team or request.user in doc.team.tournament.organizers.all()
|
||||
grant = grant or doc.team.selected_for_final and request.user in Tournament.get_final().organizers.all()
|
||||
|
||||
if isinstance(doc, Solution):
|
||||
for pool in doc.pools.all():
|
||||
if request.user in pool.juries.all():
|
||||
grant = True
|
||||
break
|
||||
if pool.round == 2 and timezone.now() < doc.tournament.date_solutions_2:
|
||||
continue
|
||||
if self.request.user.team in pool.teams.all():
|
||||
grant = True
|
||||
elif isinstance(doc, Synthesis):
|
||||
for pool in request.user.pools.all(): # If the user is a jury in the pool
|
||||
if doc.team in pool.teams.all() and doc.final == pool.tournament.final:
|
||||
grant = True
|
||||
break
|
||||
else:
|
||||
pool = Pool.objects.filter(extra_access_token=self.request.session["extra_access_token"])
|
||||
if pool.exists():
|
||||
pool = pool.get()
|
||||
if isinstance(doc, Solution):
|
||||
grant = doc in pool.solutions.all()
|
||||
elif isinstance(doc, Synthesis):
|
||||
grant = doc.team in pool.teams.all() and doc.final == pool.tournament.final
|
||||
else:
|
||||
grant = False
|
||||
else:
|
||||
grant = False
|
||||
|
||||
if not grant:
|
||||
raise PermissionDenied
|
||||
|
||||
return FileResponse(doc.file, content_type="application/pdf", filename=str(doc) + ".pdf")
|
||||
|
||||
|
||||
class ProfileListView(AdminMixin, SingleTableView):
|
||||
"""
|
||||
List all registered profiles.
|
||||
"""
|
||||
model = TFJMUser
|
||||
queryset = TFJMUser.objects.order_by("role", "last_name", "first_name")
|
||||
table_class = UserTable
|
||||
template_name = "member/profile_list.html"
|
||||
extra_context = dict(title=_("All profiles"), type="all")
|
||||
|
||||
|
||||
class OrphanedProfileListView(AdminMixin, SingleTableView):
|
||||
"""
|
||||
List all orphaned profiles, ie. participants that have no team.
|
||||
"""
|
||||
model = TFJMUser
|
||||
queryset = TFJMUser.objects.filter((Q(role="2coach") | Q(role="3participant")) & Q(team__isnull=True))\
|
||||
.order_by("role", "last_name", "first_name")
|
||||
table_class = UserTable
|
||||
template_name = "member/profile_list.html"
|
||||
extra_context = dict(title=_("Orphaned profiles"), type="orphaned")
|
||||
|
||||
|
||||
class OrganizersListView(OrgaMixin, SingleTableView):
|
||||
"""
|
||||
List all organizers.
|
||||
"""
|
||||
model = TFJMUser
|
||||
queryset = TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer"))\
|
||||
.order_by("role", "last_name", "first_name")
|
||||
table_class = UserTable
|
||||
template_name = "member/profile_list.html"
|
||||
extra_context = dict(title=_("Organizers"), type="organizers")
|
||||
|
||||
|
||||
class ResetAdminView(AdminMixin, View):
|
||||
"""
|
||||
Return to admin view, clear the session field that let an administrator to log in as someone else.
|
||||
"""
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if "_fake_user_id" in request.session:
|
||||
del request.session["_fake_user_id"]
|
||||
return redirect(request.GET["path"])
|
4
apps/participation/__init__.py
Normal file
4
apps/participation/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
default_app_config = 'participation.apps.ParticipationConfig'
|
54
apps/participation/admin.py
Normal file
54
apps/participation/admin.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib import admin
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from .models import Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
||||
|
||||
|
||||
@admin.register(Team)
|
||||
class TeamAdmin(admin.ModelAdmin):
|
||||
list_display = ('name', 'trigram', 'valid',)
|
||||
search_fields = ('name', 'trigram',)
|
||||
list_filter = ('participation__valid',)
|
||||
|
||||
def valid(self, team):
|
||||
return team.participation.valid
|
||||
|
||||
valid.short_description = _('valid')
|
||||
|
||||
|
||||
@admin.register(Participation)
|
||||
class ParticipationAdmin(admin.ModelAdmin):
|
||||
list_display = ('team', 'valid',)
|
||||
search_fields = ('team__name', 'team__trigram',)
|
||||
list_filter = ('valid',)
|
||||
|
||||
|
||||
@admin.register(Pool)
|
||||
class PoolAdmin(admin.ModelAdmin):
|
||||
search_fields = ('participations__team__name', 'participations__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Passage)
|
||||
class PassageAdmin(admin.ModelAdmin):
|
||||
search_fields = ('pool__participations__team__name', 'pool__participations__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Solution)
|
||||
class SolutionAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Synthesis)
|
||||
class SynthesisAdmin(admin.ModelAdmin):
|
||||
list_display = ('participation',)
|
||||
search_fields = ('participation__team__name', 'participation__team__trigram',)
|
||||
|
||||
|
||||
@admin.register(Tournament)
|
||||
class TournamentAdmin(admin.ModelAdmin):
|
||||
list_display = ('name',)
|
||||
search_fields = ('name',)
|
19
apps/participation/apps.py
Normal file
19
apps/participation/apps.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.db.models.signals import post_save, pre_save
|
||||
|
||||
|
||||
class ParticipationConfig(AppConfig):
|
||||
"""
|
||||
The participation app contains the data about the teams, solutions, ...
|
||||
"""
|
||||
name = 'participation'
|
||||
|
||||
def ready(self):
|
||||
from participation.signals import create_notes, create_team_participation, update_mailing_list
|
||||
pre_save.connect(update_mailing_list, "participation.Team")
|
||||
post_save.connect(create_team_participation, "participation.Team")
|
||||
post_save.connect(create_notes, "participation.Passage")
|
||||
post_save.connect(create_notes, "participation.Pool")
|
206
apps/participation/forms.py
Normal file
206
apps/participation/forms.py
Normal file
@ -0,0 +1,206 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import re
|
||||
|
||||
from bootstrap_datepicker_plus import DatePickerInput, DateTimePickerInput
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import formats
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PyPDF3 import PdfFileReader
|
||||
|
||||
from .models import Note, Participation, Passage, Pool, Solution, Synthesis, Team, Tournament
|
||||
|
||||
|
||||
class TeamForm(forms.ModelForm):
|
||||
"""
|
||||
Form to create a team, with the name and the trigram,...
|
||||
"""
|
||||
def clean_trigram(self):
|
||||
trigram = self.cleaned_data["trigram"].upper()
|
||||
if not re.match("[A-Z]{3}", trigram):
|
||||
raise ValidationError(_("The trigram must be composed of three uppercase letters."))
|
||||
return trigram
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ('name', 'trigram',)
|
||||
|
||||
|
||||
class JoinTeamForm(forms.ModelForm):
|
||||
"""
|
||||
Form to join a team by the access code.
|
||||
"""
|
||||
def clean_access_code(self):
|
||||
access_code = self.cleaned_data["access_code"]
|
||||
if not Team.objects.filter(access_code=access_code).exists():
|
||||
raise ValidationError(_("No team was found with this access code."))
|
||||
return access_code
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if "access_code" in cleaned_data:
|
||||
team = Team.objects.get(access_code=cleaned_data["access_code"])
|
||||
self.instance = team
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = ('access_code',)
|
||||
|
||||
|
||||
class ParticipationForm(forms.ModelForm):
|
||||
"""
|
||||
Form to update the problem of a team participation.
|
||||
"""
|
||||
class Meta:
|
||||
model = Participation
|
||||
fields = ('tournament',)
|
||||
|
||||
|
||||
class RequestValidationForm(forms.Form):
|
||||
"""
|
||||
Form to ask about validation.
|
||||
"""
|
||||
_form_type = forms.CharField(
|
||||
initial="RequestValidationForm",
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
|
||||
engagement = forms.BooleanField(
|
||||
label=_("I engage myself to participate to the whole TFJM²."),
|
||||
required=True,
|
||||
)
|
||||
|
||||
|
||||
class ValidateParticipationForm(forms.Form):
|
||||
"""
|
||||
Form to let administrators to accept or refuse a team.
|
||||
"""
|
||||
_form_type = forms.CharField(
|
||||
initial="ValidateParticipationForm",
|
||||
widget=forms.HiddenInput(),
|
||||
)
|
||||
|
||||
message = forms.CharField(
|
||||
label=_("Message to address to the team:"),
|
||||
widget=forms.Textarea(),
|
||||
)
|
||||
|
||||
|
||||
class TournamentForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self.fields["date_start"].widget = DatePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATE_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["date_end"].widget = DatePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATE_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["inscription_limit"].widget = DateTimePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["solution_limit"].widget = DateTimePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["solutions_draw"].widget = DateTimePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["syntheses_first_phase_limit"].widget = DateTimePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["solutions_available_second_phase"].widget = DateTimePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["syntheses_second_phase_limit"].widget = DateTimePickerInput(
|
||||
format=formats.get_format_lazy(format_type="DATETIME_INPUT_FORMATS", use_l10n=True)[0])
|
||||
self.fields["organizers"].widget = forms.CheckboxSelectMultiple()
|
||||
|
||||
class Meta:
|
||||
model = Tournament
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class SolutionForm(forms.ModelForm):
|
||||
def clean_file(self):
|
||||
if "file" in self.files:
|
||||
file = self.files["file"]
|
||||
if file.size > 5e6:
|
||||
raise ValidationError(_("The uploaded file size must be under 5 Mo."))
|
||||
if file.content_type != "application/pdf":
|
||||
raise ValidationError(_("The uploaded file must be a PDF file."))
|
||||
pdf_reader = PdfFileReader(file)
|
||||
pages = len(pdf_reader.pages)
|
||||
if pages > 30:
|
||||
raise ValidationError(_("The PDF file must not have more than 30 pages."))
|
||||
return self.cleaned_data["photo_authorization"]
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
Don't save a solution with this way. Use a view instead
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Solution
|
||||
fields = ('problem', 'file',)
|
||||
|
||||
|
||||
class PoolForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = ('tournament', 'round', 'bbb_code', 'juries',)
|
||||
widgets = {
|
||||
"juries": forms.CheckboxSelectMultiple,
|
||||
}
|
||||
|
||||
|
||||
class PoolTeamsForm(forms.ModelForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields["participations"].queryset = self.instance.tournament.participations.all()
|
||||
|
||||
class Meta:
|
||||
model = Pool
|
||||
fields = ('participations',)
|
||||
widgets = {
|
||||
"participations": forms.CheckboxSelectMultiple,
|
||||
}
|
||||
|
||||
|
||||
class PassageForm(forms.ModelForm):
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
if "defender" in cleaned_data and "opponent" in cleaned_data and "reporter" in cleaned_data \
|
||||
and len({cleaned_data["defender"], cleaned_data["opponent"], cleaned_data["reporter"]}) < 3:
|
||||
self.add_error(None, _("The defender, the opponent and the reporter must be different."))
|
||||
if "defender" in self.cleaned_data and "solution_number" in self.cleaned_data \
|
||||
and not Solution.objects.filter(participation=cleaned_data["defender"],
|
||||
problem=cleaned_data["solution_number"]).exists():
|
||||
self.add_error("solution_number", _("This defender did not work on this problem."))
|
||||
return cleaned_data
|
||||
|
||||
class Meta:
|
||||
model = Passage
|
||||
fields = ('solution_number', 'place', 'defender', 'opponent', 'reporter',)
|
||||
|
||||
|
||||
class SynthesisForm(forms.ModelForm):
|
||||
def clean_file(self):
|
||||
if "file" in self.files:
|
||||
file = self.files["file"]
|
||||
if file.size > 2e6:
|
||||
raise ValidationError(_("The uploaded file size must be under 2 Mo."))
|
||||
if file.content_type != "application/pdf":
|
||||
raise ValidationError(_("The uploaded file must be a PDF file."))
|
||||
return self.cleaned_data["photo_authorization"]
|
||||
|
||||
def save(self, commit=True):
|
||||
"""
|
||||
Don't save a synthesis with this way. Use a view instead
|
||||
"""
|
||||
|
||||
class Meta:
|
||||
model = Synthesis
|
||||
fields = ('type', 'file',)
|
||||
|
||||
|
||||
class NoteForm(forms.ModelForm):
|
||||
class Meta:
|
||||
model = Note
|
||||
fields = ('defender_writing', 'defender_oral', 'opponent_writing',
|
||||
'opponent_oral', 'reporter_writing', 'reporter_oral', )
|
2
apps/participation/management/commands/__init__.py
Normal file
2
apps/participation/management/commands/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
54
apps/participation/management/commands/check_hello_asso.py
Normal file
54
apps/participation/management/commands/check_hello_asso.py
Normal file
@ -0,0 +1,54 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.core.management import BaseCommand
|
||||
import requests
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options): # noqa: C901
|
||||
organization = "animath"
|
||||
form_slug = "tfjmm-2018"
|
||||
from_date = "2000-01-01"
|
||||
url = f"https://api.helloasso.com/v5/organizations/{organization}/forms/Event/{form_slug}/payments" \
|
||||
f"?from={from_date}&pageIndex=1&pageSize=10000&retrieveOfflineDonations=false"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {os.getenv('HELLO_ASSO_TOKEN', '')}",
|
||||
}
|
||||
http_response = requests.get(url, headers=headers)
|
||||
response = http_response.json()
|
||||
|
||||
if http_response.status_code != 200:
|
||||
message = response["message"]
|
||||
self.stderr.write(f"Error while querying Hello Asso: {message}")
|
||||
return
|
||||
|
||||
for payment in response:
|
||||
if payment["state"] != "Authorized":
|
||||
continue
|
||||
|
||||
payer = payment["payer"]
|
||||
email = payer["email"]
|
||||
qs = User.objects.filter(email=email)
|
||||
if not qs.exists():
|
||||
self.stderr.write(f"Warning: a payment was found by the email address {email}, "
|
||||
"but this user is unknown.")
|
||||
continue
|
||||
user = qs.get()
|
||||
if not user.registration.participates:
|
||||
self.stderr.write(f"Warning: a payment was found by the email address {email}, "
|
||||
"but this user is not a participant.")
|
||||
continue
|
||||
payment_obj = user.registration.payment
|
||||
payment_obj.valid = True
|
||||
payment_obj.type = "helloasso"
|
||||
payment_obj.additional_information = f"Identifiant de transation : {payment['id']}\n" \
|
||||
f"Date : {payment['date']}\n" \
|
||||
f"Reçu : {payment['paymentReceiptUrl']}\n" \
|
||||
f"Montant : {payment['amount'] / 100:.2f} €"
|
||||
payment_obj.save()
|
||||
self.stdout.write(f"{payment_obj} is validated")
|
394
apps/participation/management/commands/fix_matrix_channels.py
Normal file
394
apps/participation/management/commands/fix_matrix_channels.py
Normal file
@ -0,0 +1,394 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
import os
|
||||
|
||||
from asgiref.sync import async_to_sync
|
||||
from django.core.management import BaseCommand
|
||||
from django.utils.http import urlencode
|
||||
from django.utils.translation import activate
|
||||
from participation.models import Team, Tournament
|
||||
from registration.models import AdminRegistration, Registration, VolunteerRegistration
|
||||
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options): # noqa: C901
|
||||
activate("fr")
|
||||
|
||||
Matrix.set_display_name("Bot du TFJM²")
|
||||
|
||||
if not os.getenv("SYNAPSE_PASSWORD"):
|
||||
avatar_uri = "plop"
|
||||
else: # pragma: no cover
|
||||
if not os.path.isfile(".matrix_avatar"):
|
||||
stat_file = os.stat("tfjm/static/logo.png")
|
||||
with open("tfjm/static/logo.png", "rb") as f:
|
||||
resp = Matrix.upload(f, filename="logo.png", content_type="image/png",
|
||||
filesize=stat_file.st_size)[0][0]
|
||||
avatar_uri = resp.content_uri
|
||||
with open(".matrix_avatar", "w") as f:
|
||||
f.write(avatar_uri)
|
||||
Matrix.set_avatar(avatar_uri)
|
||||
|
||||
with open(".matrix_avatar", "r") as f:
|
||||
avatar_uri = f.read().rstrip(" \t\r\n")
|
||||
|
||||
# Create basic channels
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#aide-jurys-orgas:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="aide-jurys-orgas",
|
||||
name="Aide jurys & orgas",
|
||||
topic="Pour discuter de propblèmes d'organisation",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#annonces:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="annonces",
|
||||
name="Annonces",
|
||||
topic="Informations importantes du TFJM²",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#bienvenue:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="bienvenue",
|
||||
name="Bienvenue",
|
||||
topic="Bienvenue au TFJM² 2021 !",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#bot:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="bot",
|
||||
name="Bot",
|
||||
topic="Vive les r0b0ts",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#cno:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="cno",
|
||||
name="CNO",
|
||||
topic="Channel des dieux",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#dev-bot:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="dev-bot",
|
||||
name="Bot - développement",
|
||||
topic="Vive le bot",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#faq:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="faq",
|
||||
name="FAQ",
|
||||
topic="Posez toutes vos questions ici !",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#flood:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="flood",
|
||||
name="Flood",
|
||||
topic="Discutez de tout et de rien !",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)("#je-cherche-une-equipe:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias="je-cherche-une-equipe",
|
||||
name="Je cherche une équipe",
|
||||
topic="Le Tinder du TFJM²",
|
||||
federate=False,
|
||||
preset=RoomPreset.public_chat,
|
||||
)
|
||||
|
||||
# Setup avatars
|
||||
Matrix.set_room_avatar("#aide-jurys-orgas:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#annonces:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#bienvenue:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#bot:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#cno:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#dev-bot:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#faq:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#flood:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar("#je-cherche-une-equipe:tfjm.org", avatar_uri)
|
||||
|
||||
# Read-only channels
|
||||
Matrix.set_room_power_level_event("#annonces:tfjm.org", "events_default", 50)
|
||||
Matrix.set_room_power_level_event("#bienvenue:tfjm.org", "events_default", 50)
|
||||
|
||||
# Invite everyone to public channels
|
||||
for r in Registration.objects.all():
|
||||
Matrix.invite("#annonces:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#bienvenue:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#bot:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#faq:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#flood:tfjm.org", f"@{r.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#je-cherche-une-equipe:tfjm.org",
|
||||
f"@{r.matrix_username}:tfjm.org")
|
||||
self.stdout.write(f"Invite {r} in most common channels...")
|
||||
|
||||
# Volunteers have access to the help channel
|
||||
for volunteer in VolunteerRegistration.objects.all():
|
||||
Matrix.invite("#aide-jurys-orgas:tfjm.org", f"@{volunteer.matrix_username}:tfjm.org")
|
||||
self.stdout.write(f"Invite {volunteer} in #aide-jury-orgas...")
|
||||
|
||||
# Admins are admins
|
||||
for admin in AdminRegistration.objects.all():
|
||||
self.stdout.write(f"Invite {admin} in #cno and #dev-bot...")
|
||||
Matrix.invite("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
Matrix.invite("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
|
||||
self.stdout.write(f"Give admin permissions for {admin}...")
|
||||
Matrix.set_room_power_level("#aide-jurys-orgas:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#annonces:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#bienvenue:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#cno:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#dev-bot:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#faq:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#flood:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level("#je-cherche-une-equipe:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
|
||||
# Create tournament-specific channels
|
||||
for tournament in Tournament.objects.all():
|
||||
self.stdout.write(f"Managing tournament of {tournament.name}.")
|
||||
|
||||
name = tournament.name
|
||||
slug = name.lower().replace(" ", "-")
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#annonces-{slug}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"annonces-{slug}",
|
||||
name=f"{name} - Annonces",
|
||||
topic=f"Annonces du tournoi de {name}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#general-{slug}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"general-{slug}",
|
||||
name=f"{name} - Général",
|
||||
topic=f"Accueil du tournoi de {name}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#flood-{slug}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"flood-{slug}",
|
||||
name=f"{name} - Flood",
|
||||
topic=f"Discussion libre du tournoi de {name}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#jury-{slug}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"jury-{slug}",
|
||||
name=f"{name} - Jury",
|
||||
topic=f"Discussion entre les orgas et jurys du tournoi de {name}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#orga-{slug}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"orga-{slug}",
|
||||
name=f"{name} - Organisateurs",
|
||||
topic=f"Discussion entre les orgas du tournoi de {name}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#tirage-au-sort-{slug}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"tirage-au-sort-{slug}",
|
||||
name=f"{name} - Tirage au sort",
|
||||
topic=f"Tirage au sort du tournoi de {name}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
# Setup avatars
|
||||
Matrix.set_room_avatar(f"#annonces-{slug}:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar(f"#flood-{slug}:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar(f"#general-{slug}:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar(f"#jury-{slug}:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar(f"#orga-{slug}:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar(f"#tirage-au-sort-{slug}:tfjm.org", avatar_uri)
|
||||
|
||||
# Invite admins and give permissions
|
||||
for admin in AdminRegistration.objects.all():
|
||||
self.stdout.write(f"Invite {admin} in all channels of the tournament {name}...")
|
||||
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
|
||||
self.stdout.write(f"Give permissions to {admin} in all channels of the tournament {name}...")
|
||||
Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level(f"#general-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org", f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
|
||||
# Invite organizers and give permissions
|
||||
for orga in tournament.organizers.all():
|
||||
self.stdout.write(f"Invite organizer {orga} in all channels of the tournament {name}...")
|
||||
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
|
||||
if not orga.is_admin:
|
||||
Matrix.set_room_power_level(f"#annonces-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#flood-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#general-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#orga-{slug}:tfjm.org", f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#tirage-au-sort-{slug}:tfjm.org",
|
||||
f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
|
||||
# Invite participants
|
||||
for participation in tournament.participations.filter(valid=True).all():
|
||||
for participant in participation.team.participants.all():
|
||||
self.stdout.write(f"Invite {participant} in public channels of the tournament {name}...")
|
||||
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
|
||||
|
||||
# Create pool-specific channels
|
||||
for pool in tournament.pools.all():
|
||||
self.stdout.write(f"Managing {pool}...")
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#poule-{slug}-{pool.id}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"poule-{slug}-{pool.id}",
|
||||
name=f"{name} - Jour {pool.round} - Poule "
|
||||
f"{', '.join(participation.team.trigram for participation in pool.participations.all())}",
|
||||
topic=f"Discussion avec les équipes - {pool}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#poule-{slug}-{pool.id}-jurys:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"poule-{slug}-{pool.id}-jurys",
|
||||
name=f"{name} - Jour {pool.round} - Jurys poule "
|
||||
f"{', '.join(participation.team.trigram for participation in pool.participations.all())}",
|
||||
topic=f"Discussion avec les jurys - {pool}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}:tfjm.org", avatar_uri)
|
||||
Matrix.set_room_avatar(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", avatar_uri)
|
||||
|
||||
url_params = urlencode(dict(url=f"https://visio.animath.live/b/{pool.bbb_code}",
|
||||
isAudioConf='false', displayName='$matrix_display_name',
|
||||
avatarUrl='$matrix_avatar_url', userId='$matrix_user_id')) \
|
||||
.replace("%24", "$")
|
||||
Matrix.add_integration(f"#poule-{slug}-{pool.id}:tfjm.org",
|
||||
f"https://scalar.vector.im/api/widgets/bigbluebutton.html?{url_params}",
|
||||
f"bbb-{slug}-{pool.id}", "bigbluebutton", "BigBlueButton", str(pool))
|
||||
Matrix.add_integration(f"#poule-{slug}-{pool.id}:tfjm.org",
|
||||
f"https://board.tfjm.org/boards/{slug}-{pool.id}", f"board-{slug}-{pool.id}",
|
||||
"customwidget", "Tableau", str(pool))
|
||||
|
||||
# Invite admins and give permissions
|
||||
for admin in AdminRegistration.objects.all():
|
||||
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", f"@{admin.matrix_username}:tfjm.org")
|
||||
|
||||
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}:tfjm.org",
|
||||
f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}-jurys:tfjm.org",
|
||||
f"@{admin.matrix_username}:tfjm.org", 95)
|
||||
|
||||
# Invite organizers and give permissions
|
||||
for orga in VolunteerRegistration.objects.all():
|
||||
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", f"@{orga.matrix_username}:tfjm.org")
|
||||
|
||||
if not orga.is_admin:
|
||||
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}:tfjm.org",
|
||||
f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}-jurys:tfjm.org",
|
||||
f"@{orga.matrix_username}:tfjm.org", 50)
|
||||
|
||||
# Invite the jury, give good permissions
|
||||
for jury in pool.juries.all():
|
||||
Matrix.invite(f"#annonces-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#general-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#flood-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#orga-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#poule-{slug}-{pool.id}-jurys:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
Matrix.invite(f"#tirage-au-sort-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org")
|
||||
|
||||
if not jury.is_admin:
|
||||
Matrix.set_room_power_level(f"#jury-{slug}:tfjm.org", f"@{jury.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}:tfjm.org",
|
||||
f"@{jury.matrix_username}:tfjm.org", 50)
|
||||
Matrix.set_room_power_level(f"#poule-{slug}-{pool.id}-jurys:tfjm.org",
|
||||
f"@{jury.matrix_username}:tfjm.org", 50)
|
||||
|
||||
# Invite participants to the right pool
|
||||
for participation in pool.participations.all():
|
||||
for participant in participation.team.participants.all():
|
||||
Matrix.invite(f"#poule-{slug}-{pool.id}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
|
||||
|
||||
# Create private channels for teams
|
||||
for team in Team.objects.all():
|
||||
self.stdout.write(f"Create private channel for {team}...")
|
||||
if not async_to_sync(Matrix.resolve_room_alias)(f"#equipe-{team.trigram.lower()}:tfjm.org"):
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.public,
|
||||
alias=f"equipe-{team.trigram.lower()}",
|
||||
name=f"Équipe {team.trigram}",
|
||||
topic=f"Discussion interne de l'équipe {team.name}",
|
||||
federate=False,
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
for participant in team.participants.all():
|
||||
Matrix.invite(f"#equipe-{team.trigram.lower}:tfjm.org", f"@{participant.matrix_username}:tfjm.org")
|
||||
Matrix.set_room_power_level(f"#equipe-{team.trigram.lower()}:tfjm.org",
|
||||
f"@{participant.matrix_username}:tfjm.org", 50)
|
74
apps/participation/management/commands/fix_sympa_lists.py
Normal file
74
apps/participation/management/commands/fix_sympa_lists.py
Normal file
@ -0,0 +1,74 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.core.management import BaseCommand
|
||||
from django.db.models import Q
|
||||
from participation.models import Team, Tournament
|
||||
from registration.models import AdminRegistration, ParticipantRegistration, VolunteerRegistration
|
||||
from tfjm.lists import get_sympa_client
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
"""
|
||||
Create Sympa mailing lists and register teams.
|
||||
"""
|
||||
sympa = get_sympa_client()
|
||||
|
||||
sympa.create_list("equipes", "Equipes du TFJM2", "hotline",
|
||||
"Liste de diffusion pour contacter toutes les equipes validees du TFJM2.",
|
||||
"education", raise_error=False)
|
||||
sympa.create_list("equipes-non-valides", "Equipes non valides du TFJM2", "hotline",
|
||||
"Liste de diffusion pour contacter toutes les equipes non validees du TFJM2.",
|
||||
"education", raise_error=False)
|
||||
|
||||
sympa.create_list("admins", "Administrateurs du TFJM2", "hotline",
|
||||
"Liste de diffusion pour contacter tous les administrateurs du TFJM2.",
|
||||
"education", raise_error=False)
|
||||
sympa.create_list("organisateurs", "Organisateurs du TFJM2", "hotline",
|
||||
"Liste de diffusion pour contacter tous les organisateurs du TFJM2.",
|
||||
"education", raise_error=False)
|
||||
sympa.create_list("jurys", "Jurys du TFJM2", "hotline",
|
||||
"Liste de diffusion pour contacter tous les jurys du TFJM2.",
|
||||
"education", raise_error=False)
|
||||
|
||||
for tournament in Tournament.objects.all():
|
||||
slug = tournament.name.lower().replace(" ", "-")
|
||||
sympa.create_list(f"equipes-{slug}", f"Equipes du tournoi {tournament.name}", "hotline",
|
||||
f"Liste de diffusion pour contacter toutes les equipes du tournoi {tournament.name}"
|
||||
" du TFJM2.", "education", raise_error=False)
|
||||
sympa.create_list(f"organisateurs-{slug}", f"Organisateurs du tournoi {tournament.name}", "hotline",
|
||||
"Liste de diffusion pour contacter tous les organisateurs du tournoi "
|
||||
f"{tournament.name} du TFJM2.", "education", raise_error=False)
|
||||
sympa.create_list(f"jurys-{slug}", f"Jurys du tournoi {tournament.name}", "hotline",
|
||||
f"Liste de diffusion pour contacter tous les jurys du tournoi {tournament.name}"
|
||||
f" du TFJM2.", "education", raise_error=False)
|
||||
|
||||
sympa.subscribe(tournament.teams_email, "equipes", True)
|
||||
sympa.subscribe(tournament.organizers_email, "organisateurs", True)
|
||||
sympa.subscribe(tournament.jurys_email, "jurys", True)
|
||||
|
||||
for team in Team.objects.filter(participation__valid=True).all():
|
||||
team.create_mailing_list()
|
||||
sympa.unsubscribe(team.email, "equipes-non-valides", True)
|
||||
sympa.subscribe(team.email, f"equipes-{team.participation.tournament.name.lower().replace(' ', '-')}",
|
||||
True, f"Equipe {team.name}")
|
||||
|
||||
for team in Team.objects.filter(Q(participation__valid=False) | Q(participation__valid__isnull=True)).all():
|
||||
team.create_mailing_list()
|
||||
sympa.subscribe(team.email, "equipes-non-valides", f"Equipe {team.name}", True)
|
||||
|
||||
for participant in ParticipantRegistration.objects.filter(team__isnull=False).all():
|
||||
sympa.subscribe(participant.user.email, f"equipe-{participant.team.trigram.lower}", True, f"{participant}")
|
||||
|
||||
for volunteer in VolunteerRegistration.objects.all():
|
||||
for organized_tournament in volunteer.organized_tournaments.all():
|
||||
slug = organized_tournament.name.lower().replace(" ", "-")
|
||||
sympa.subscribe(volunteer.user.email, f"organisateurs-{slug}", True)
|
||||
|
||||
for jury_in in volunteer.jury_in.all():
|
||||
slug = jury_in.tournament.name.lower().replace(" ", "-")
|
||||
sympa.subscribe(volunteer.user.email, f"jurys-{slug}", True)
|
||||
|
||||
for admin in AdminRegistration.objects.all():
|
||||
sympa.subscribe(admin.user.email, "admins", True)
|
131
apps/participation/migrations/0001_initial.py
Normal file
131
apps/participation/migrations/0001_initial.py
Normal file
@ -0,0 +1,131 @@
|
||||
# Generated by Django 3.0.11 on 2021-01-21 21:06
|
||||
|
||||
import datetime
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
import participation.models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Note',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('defender_writing', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13), (14, 14), (15, 15), (16, 16), (17, 17), (18, 18), (19, 19), (20, 20)], default=0, verbose_name='defender writing note')),
|
||||
('defender_oral', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10), (11, 11), (12, 12), (13, 13), (14, 14), (15, 15), (16, 16)], default=0, verbose_name='defender oral note')),
|
||||
('opponent_writing', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)], default=0, verbose_name='opponent writing note')),
|
||||
('opponent_oral', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10)], default=0, verbose_name='opponent oral note')),
|
||||
('reporter_writing', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9)], default=0, verbose_name='reporter writing note')),
|
||||
('reporter_oral', models.PositiveSmallIntegerField(choices=[(0, 0), (1, 1), (2, 2), (3, 3), (4, 4), (5, 5), (6, 6), (7, 7), (8, 8), (9, 9), (10, 10)], default=0, verbose_name='reporter oral note')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'note',
|
||||
'verbose_name_plural': 'notes',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Participation',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('valid', models.BooleanField(default=None, help_text='The participation got the validation of the organizers.', null=True, verbose_name='valid')),
|
||||
('final', models.BooleanField(default=False, help_text='The team is selected for the final tournament.', verbose_name='selected for final')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'participation',
|
||||
'verbose_name_plural': 'participations',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Passage',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('place', models.CharField(default='Non indiqué', help_text='Where the solution is presented?', max_length=255, verbose_name='place')),
|
||||
('solution_number', models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], verbose_name='defended solution')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'passage',
|
||||
'verbose_name_plural': 'passages',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Pool',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('round', models.PositiveSmallIntegerField(choices=[(1, 'Round 1'), (2, 'Round 2')], verbose_name='round')),
|
||||
('bbb_code', models.CharField(blank=True, default='', help_text='The code of the form xxx-xxx-xxx at the end of the BBB link.', max_length=11, validators=[django.core.validators.RegexValidator('[a-z]{3}-[a-z]{3}-[a-z]{3}')], verbose_name='BigBlueButton code')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'pool',
|
||||
'verbose_name_plural': 'pools',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Solution',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('problem', models.PositiveSmallIntegerField(choices=[(1, 'Problem #1'), (2, 'Problem #2'), (3, 'Problem #3'), (4, 'Problem #4'), (5, 'Problem #5'), (6, 'Problem #6'), (7, 'Problem #7'), (8, 'Problem #8')], verbose_name='problem')),
|
||||
('final_solution', models.BooleanField(default=False, verbose_name='solution for the final tournament')),
|
||||
('file', models.FileField(blank=True, default='', unique=True, upload_to=participation.models.get_solution_filename, verbose_name='file')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'solution',
|
||||
'verbose_name_plural': 'solutions',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Synthesis',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('type', models.PositiveSmallIntegerField(choices=[(1, 'opponent'), (2, 'reporter')])),
|
||||
('file', models.FileField(blank=True, default='', unique=True, upload_to=participation.models.get_synthesis_filename, verbose_name='file')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'synthesis',
|
||||
'verbose_name_plural': 'syntheses',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Team',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
|
||||
('trigram', models.CharField(help_text='The trigram must be composed of three uppercase letters.', max_length=3, unique=True, validators=[django.core.validators.RegexValidator('[A-Z]{3}')], verbose_name='trigram')),
|
||||
('access_code', models.CharField(help_text='The access code let other people to join the team.', max_length=6, verbose_name='access code')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'team',
|
||||
'verbose_name_plural': 'teams',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Tournament',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, unique=True, verbose_name='name')),
|
||||
('date_start', models.DateField(default=datetime.date.today, verbose_name='start')),
|
||||
('date_end', models.DateField(default=datetime.date.today, verbose_name='end')),
|
||||
('max_teams', models.PositiveSmallIntegerField(default=9, verbose_name='max team count')),
|
||||
('price', models.PositiveSmallIntegerField(default=21, verbose_name='price')),
|
||||
('inscription_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date for registrations')),
|
||||
('solution_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date to upload solutions')),
|
||||
('solutions_draw', models.DateTimeField(default=django.utils.timezone.now, verbose_name='random draw for solutions')),
|
||||
('syntheses_first_phase_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date to upload the syntheses for the first phase')),
|
||||
('solutions_available_second_phase', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date when the solutions for the second round become available')),
|
||||
('syntheses_second_phase_limit', models.DateTimeField(default=django.utils.timezone.now, verbose_name='limit date to upload the syntheses for the second phase')),
|
||||
('description', models.TextField(blank=True, verbose_name='description')),
|
||||
('final', models.BooleanField(default=False, verbose_name='final')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'tournament',
|
||||
'verbose_name_plural': 'tournaments',
|
||||
},
|
||||
),
|
||||
]
|
115
apps/participation/migrations/0002_auto_20210121_2206.py
Normal file
115
apps/participation/migrations/0002_auto_20210121_2206.py
Normal file
@ -0,0 +1,115 @@
|
||||
# Generated by Django 3.0.11 on 2021-01-21 21:06
|
||||
|
||||
import address.models
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('address', '0003_auto_20200830_1851'),
|
||||
('registration', '0001_initial'),
|
||||
('participation', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='tournament',
|
||||
name='organizers',
|
||||
field=models.ManyToManyField(related_name='organized_tournaments', to='registration.VolunteerRegistration', verbose_name='organizers'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='tournament',
|
||||
name='place',
|
||||
field=address.models.AddressField(on_delete=django.db.models.deletion.CASCADE, to='address.Address', verbose_name='place'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='team',
|
||||
index=models.Index(fields=['trigram'], name='participati_trigram_239255_idx'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='synthesis',
|
||||
name='participation',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='participation.Participation', verbose_name='participation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='synthesis',
|
||||
name='passage',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='syntheses', to='participation.Passage', verbose_name='passage'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='solution',
|
||||
name='participation',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='solutions', to='participation.Participation', verbose_name='participation'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pool',
|
||||
name='juries',
|
||||
field=models.ManyToManyField(related_name='jury_in', to='registration.VolunteerRegistration', verbose_name='juries'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pool',
|
||||
name='participations',
|
||||
field=models.ManyToManyField(related_name='pools', to='participation.Participation', verbose_name='participations'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pool',
|
||||
name='tournament',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='pools', to='participation.Tournament', verbose_name='tournament'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='passage',
|
||||
name='defender',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.Participation', verbose_name='defender'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='passage',
|
||||
name='opponent',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.Participation', verbose_name='opponent'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='passage',
|
||||
name='pool',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='passages', to='participation.Pool', verbose_name='pool'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='passage',
|
||||
name='reporter',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='participation.Participation', verbose_name='reporter'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='participation',
|
||||
name='team',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='participation.Team', verbose_name='team'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='participation',
|
||||
name='tournament',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='participation.Tournament', verbose_name='tournament'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='note',
|
||||
name='jury',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='registration.VolunteerRegistration', verbose_name='jury'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='note',
|
||||
name='passage',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notes', to='participation.Passage', verbose_name='passage'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='tournament',
|
||||
index=models.Index(fields=['name', 'date_start', 'date_end'], name='participati_name_b43174_idx'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='synthesis',
|
||||
unique_together={('participation', 'passage', 'type')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='solution',
|
||||
unique_together={('participation', 'problem', 'final_solution')},
|
||||
),
|
||||
]
|
2
apps/participation/migrations/__init__.py
Normal file
2
apps/participation/migrations/__init__.py
Normal file
@ -0,0 +1,2 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
647
apps/participation/models.py
Normal file
647
apps/participation/models.py
Normal file
@ -0,0 +1,647 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from datetime import date
|
||||
import os
|
||||
|
||||
from address.models import AddressField
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import RegexValidator
|
||||
from django.db import models
|
||||
from django.db.models import Index
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils import timezone
|
||||
from django.utils.crypto import get_random_string
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from registration.models import VolunteerRegistration
|
||||
from tfjm.lists import get_sympa_client
|
||||
from tfjm.matrix import Matrix, RoomPreset, RoomVisibility
|
||||
|
||||
|
||||
class Team(models.Model):
|
||||
"""
|
||||
The Team model represents a real team that participates to the TFJM².
|
||||
This only includes the registration detail.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
unique=True,
|
||||
)
|
||||
|
||||
trigram = models.CharField(
|
||||
max_length=3,
|
||||
verbose_name=_("trigram"),
|
||||
help_text=_("The trigram must be composed of three uppercase letters."),
|
||||
unique=True,
|
||||
validators=[RegexValidator("[A-Z]{3}")],
|
||||
)
|
||||
|
||||
access_code = models.CharField(
|
||||
max_length=6,
|
||||
verbose_name=_("access code"),
|
||||
help_text=_("The access code let other people to join the team."),
|
||||
)
|
||||
|
||||
@property
|
||||
def students(self):
|
||||
return self.participants.filter(studentregistration__isnull=False)
|
||||
|
||||
@property
|
||||
def coaches(self):
|
||||
return self.participants.filter(coachregistration__isnull=False)
|
||||
|
||||
@property
|
||||
def email(self):
|
||||
"""
|
||||
:return: The mailing list to contact the team members.
|
||||
"""
|
||||
return f"equipe-{self.trigram.lower()}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
||||
|
||||
def create_mailing_list(self):
|
||||
"""
|
||||
Create a new Sympa mailing list to contact the team.
|
||||
"""
|
||||
get_sympa_client().create_list(
|
||||
f"equipe-{self.trigram.lower()}",
|
||||
f"Equipe {self.name} ({self.trigram})",
|
||||
"hotline", # TODO Use a custom sympa template
|
||||
f"Liste de diffusion pour contacter l'equipe {self.name} du TFJM2",
|
||||
"education",
|
||||
raise_error=False,
|
||||
)
|
||||
|
||||
def delete_mailing_list(self):
|
||||
"""
|
||||
Drop the Sympa mailing list, if the team is empty or if the trigram changed.
|
||||
"""
|
||||
if self.participation.valid: # pragma: no cover
|
||||
get_sympa_client().unsubscribe(
|
||||
self.email, f"equipes-{self.participation.tournament.name.lower().replace(' ', '-')}", False)
|
||||
else:
|
||||
get_sympa_client().unsubscribe(self.email, "equipes-non-valides", False)
|
||||
get_sympa_client().delete_list(f"equipe-{self.trigram}")
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.access_code:
|
||||
# if the team got created, generate the access code, create the contact mailing list
|
||||
# and create a dedicated Matrix room.
|
||||
self.access_code = get_random_string(6)
|
||||
self.create_mailing_list()
|
||||
|
||||
Matrix.create_room(
|
||||
visibility=RoomVisibility.private,
|
||||
name=f"#équipe-{self.trigram.lower()}",
|
||||
alias=f"equipe-{self.trigram.lower()}",
|
||||
topic=f"Discussion de l'équipe {self.name}",
|
||||
preset=RoomPreset.private_chat,
|
||||
)
|
||||
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:team_detail", args=(self.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return _("Team {name} ({trigram})").format(name=self.name, trigram=self.trigram)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("team")
|
||||
verbose_name_plural = _("teams")
|
||||
indexes = [
|
||||
Index(fields=("trigram", )),
|
||||
]
|
||||
|
||||
|
||||
class Tournament(models.Model):
|
||||
name = models.CharField(
|
||||
max_length=255,
|
||||
verbose_name=_("name"),
|
||||
unique=True,
|
||||
)
|
||||
|
||||
date_start = models.DateField(
|
||||
verbose_name=_("start"),
|
||||
default=date.today,
|
||||
)
|
||||
|
||||
date_end = models.DateField(
|
||||
verbose_name=_("end"),
|
||||
default=date.today,
|
||||
)
|
||||
|
||||
place = AddressField(
|
||||
verbose_name=_("place"),
|
||||
)
|
||||
|
||||
max_teams = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("max team count"),
|
||||
default=9,
|
||||
)
|
||||
|
||||
price = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("price"),
|
||||
default=21,
|
||||
)
|
||||
|
||||
inscription_limit = models.DateTimeField(
|
||||
verbose_name=_("limit date for registrations"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
solution_limit = models.DateTimeField(
|
||||
verbose_name=_("limit date to upload solutions"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
solutions_draw = models.DateTimeField(
|
||||
verbose_name=_("random draw for solutions"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
syntheses_first_phase_limit = models.DateTimeField(
|
||||
verbose_name=_("limit date to upload the syntheses for the first phase"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
solutions_available_second_phase = models.DateTimeField(
|
||||
verbose_name=_("date when the solutions for the second round become available"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
syntheses_second_phase_limit = models.DateTimeField(
|
||||
verbose_name=_("limit date to upload the syntheses for the second phase"),
|
||||
default=timezone.now,
|
||||
)
|
||||
|
||||
description = models.TextField(
|
||||
verbose_name=_("description"),
|
||||
blank=True,
|
||||
)
|
||||
|
||||
organizers = models.ManyToManyField(
|
||||
VolunteerRegistration,
|
||||
verbose_name=_("organizers"),
|
||||
related_name="organized_tournaments",
|
||||
)
|
||||
|
||||
final = models.BooleanField(
|
||||
verbose_name=_("final"),
|
||||
default=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def teams_email(self):
|
||||
"""
|
||||
:return: The mailing list to contact the team members.
|
||||
"""
|
||||
return f"equipes-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
||||
|
||||
@property
|
||||
def organizers_email(self):
|
||||
"""
|
||||
:return: The mailing list to contact the team members.
|
||||
"""
|
||||
return f"organisateurs-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
||||
|
||||
@property
|
||||
def jurys_email(self):
|
||||
"""
|
||||
:return: The mailing list to contact the team members.
|
||||
"""
|
||||
return f"jurys-{self.name.lower().replace(' ', '-')}@{os.getenv('SYMPA_HOST', 'localhost')}"
|
||||
|
||||
def create_mailing_lists(self):
|
||||
"""
|
||||
Create a new Sympa mailing list to contact the team.
|
||||
"""
|
||||
get_sympa_client().create_list(
|
||||
f"equipes-{self.name.lower().replace(' ', '-')}",
|
||||
f"Equipes du tournoi de {self.name}",
|
||||
"hotline", # TODO Use a custom sympa template
|
||||
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
||||
"education",
|
||||
raise_error=False,
|
||||
)
|
||||
get_sympa_client().create_list(
|
||||
f"organisateurs-{self.name.lower().replace(' ', '-')}",
|
||||
f"Organisateurs du tournoi de {self.name}",
|
||||
"hotline", # TODO Use a custom sympa template
|
||||
f"Liste de diffusion pour contacter les equipes du tournoi {self.name} du TFJM²",
|
||||
"education",
|
||||
raise_error=False,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def final_tournament():
|
||||
qs = Tournament.objects.filter(final=True)
|
||||
if qs.exists():
|
||||
return qs.get()
|
||||
|
||||
@property
|
||||
def participations(self):
|
||||
if self.final:
|
||||
return Participation.objects.filter(final=True)
|
||||
return self.participation_set
|
||||
|
||||
@property
|
||||
def solutions(self):
|
||||
if self.final:
|
||||
return Solution.objects.filter(final_solution=True)
|
||||
return Solution.objects.filter(participation__tournament=self)
|
||||
|
||||
@property
|
||||
def syntheses(self):
|
||||
if self.final:
|
||||
return Synthesis.objects.filter(final_solution=True)
|
||||
return Synthesis.objects.filter(participation__tournament=self)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:tournament_detail", args=(self.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("tournament")
|
||||
verbose_name_plural = _("tournaments")
|
||||
indexes = [
|
||||
Index(fields=("name", "date_start", "date_end", )),
|
||||
]
|
||||
|
||||
|
||||
class Participation(models.Model):
|
||||
"""
|
||||
The Participation model contains all data that are related to the participation:
|
||||
chosen problem, validity status, solutions,...
|
||||
"""
|
||||
team = models.OneToOneField(
|
||||
Team,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("team"),
|
||||
)
|
||||
|
||||
tournament = models.ForeignKey(
|
||||
Tournament,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None,
|
||||
verbose_name=_("tournament"),
|
||||
)
|
||||
|
||||
valid = models.BooleanField(
|
||||
null=True,
|
||||
default=None,
|
||||
verbose_name=_("valid"),
|
||||
help_text=_("The participation got the validation of the organizers."),
|
||||
)
|
||||
|
||||
final = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("selected for final"),
|
||||
help_text=_("The team is selected for the final tournament."),
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:participation_detail", args=(self.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return _("Participation of the team {name} ({trigram})").format(name=self.team.name, trigram=self.team.trigram)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("participation")
|
||||
verbose_name_plural = _("participations")
|
||||
|
||||
|
||||
class Pool(models.Model):
|
||||
tournament = models.ForeignKey(
|
||||
Tournament,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="pools",
|
||||
verbose_name=_("tournament"),
|
||||
)
|
||||
|
||||
round = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("round"),
|
||||
choices=[
|
||||
(1, format_lazy(_("Round {round}"), round=1)),
|
||||
(2, format_lazy(_("Round {round}"), round=2)),
|
||||
]
|
||||
)
|
||||
|
||||
participations = models.ManyToManyField(
|
||||
Participation,
|
||||
related_name="pools",
|
||||
verbose_name=_("participations"),
|
||||
)
|
||||
|
||||
juries = models.ManyToManyField(
|
||||
VolunteerRegistration,
|
||||
related_name="jury_in",
|
||||
verbose_name=_("juries"),
|
||||
)
|
||||
|
||||
bbb_code = models.CharField(
|
||||
max_length=11,
|
||||
blank=True,
|
||||
default="",
|
||||
verbose_name=_("BigBlueButton code"),
|
||||
help_text=_("The code of the form xxx-xxx-xxx at the end of the BBB link."),
|
||||
validators=[RegexValidator("[a-z]{3}-[a-z]{3}-[a-z]{3}")],
|
||||
)
|
||||
|
||||
@property
|
||||
def bbb_url(self):
|
||||
return f"https://visio.animath.live/b/{self.bbb_code}"
|
||||
|
||||
@property
|
||||
def solutions(self):
|
||||
return Solution.objects.filter(participation__in=self.participations, final_solution=self.tournament.final)
|
||||
|
||||
def average(self, participation):
|
||||
return sum(passage.average(participation) for passage in self.passages.all())
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:pool_detail", args=(self.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return _("Pool {round} for tournament {tournament} with teams {teams}")\
|
||||
.format(round=self.round,
|
||||
tournament=str(self.tournament),
|
||||
teams=", ".join(participation.team.trigram for participation in self.participations.all()))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("pool")
|
||||
verbose_name_plural = _("pools")
|
||||
|
||||
|
||||
class Passage(models.Model):
|
||||
pool = models.ForeignKey(
|
||||
Pool,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("pool"),
|
||||
related_name="passages",
|
||||
)
|
||||
|
||||
place = models.CharField(
|
||||
verbose_name=_("place"),
|
||||
max_length=255,
|
||||
help_text=_("Where the solution is presented?"),
|
||||
default="Non indiqué",
|
||||
)
|
||||
|
||||
solution_number = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("defended solution"),
|
||||
choices=[
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
||||
],
|
||||
)
|
||||
|
||||
defender = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("defender"),
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
opponent = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("opponent"),
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
reporter = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.PROTECT,
|
||||
verbose_name=_("reporter"),
|
||||
related_name="+",
|
||||
)
|
||||
|
||||
@property
|
||||
def defended_solution(self) -> "Solution":
|
||||
return Solution.objects.get(
|
||||
participation=self.defender,
|
||||
problem=self.solution_number,
|
||||
final_solution=self.pool.tournament.final)
|
||||
|
||||
def avg(self, iterator) -> int:
|
||||
items = [i for i in iterator if i]
|
||||
return sum(items) / len(items) if items else 0
|
||||
|
||||
@property
|
||||
def average_defender_writing(self) -> int:
|
||||
return self.avg(note.defender_writing for note in self.notes.all())
|
||||
|
||||
@property
|
||||
def average_defender_oral(self) -> int:
|
||||
return self.avg(note.defender_oral for note in self.notes.all())
|
||||
|
||||
@property
|
||||
def average_defender(self) -> int:
|
||||
return self.average_defender_writing + 2 * self.average_defender_oral
|
||||
|
||||
@property
|
||||
def average_opponent_writing(self) -> int:
|
||||
return self.avg(note.opponent_writing for note in self.notes.all())
|
||||
|
||||
@property
|
||||
def average_opponent_oral(self) -> int:
|
||||
return self.avg(note.opponent_oral for note in self.notes.all())
|
||||
|
||||
@property
|
||||
def average_opponent(self) -> int:
|
||||
return self.average_opponent_writing + 2 * self.average_opponent_oral
|
||||
|
||||
@property
|
||||
def average_reporter_writing(self) -> int:
|
||||
return self.avg(note.reporter_writing for note in self.notes.all())
|
||||
|
||||
@property
|
||||
def average_reporter_oral(self) -> int:
|
||||
return self.avg(note.reporter_oral for note in self.notes.all())
|
||||
|
||||
@property
|
||||
def average_reporter(self) -> int:
|
||||
return self.average_reporter_writing + self.average_reporter_oral
|
||||
|
||||
def average(self, participation):
|
||||
return self.average_defender if participation == self.defender else self.average_opponent \
|
||||
if participation == self.opponent else self.average_reporter if participation == self.reporter else 0
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:passage_detail", args=(self.pk,))
|
||||
|
||||
def clean(self):
|
||||
if self.defender not in self.pool.participations.all():
|
||||
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
||||
.format(trigram=self.defender.team.trigram))
|
||||
if self.opponent not in self.pool.participations.all():
|
||||
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
||||
.format(trigram=self.opponent.team.trigram))
|
||||
if self.reporter not in self.pool.participations.all():
|
||||
raise ValidationError(_("Team {trigram} is not registered in the pool.")
|
||||
.format(trigram=self.reporter.team.trigram))
|
||||
return super().clean()
|
||||
|
||||
def __str__(self):
|
||||
return _("Passage of {defender} for problem {problem}")\
|
||||
.format(defender=self.defender.team, problem=self.solution_number)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("passage")
|
||||
verbose_name_plural = _("passages")
|
||||
|
||||
|
||||
def get_solution_filename(instance, filename):
|
||||
return f"solutions/{instance.participation.team.trigram}_{instance.problem}" \
|
||||
+ ("final" if instance.final_solution else "")
|
||||
|
||||
|
||||
def get_synthesis_filename(instance, filename):
|
||||
return f"syntheses/{instance.participation.team.trigram}_{instance.type}_{instance.passage.pk}"
|
||||
|
||||
|
||||
class Solution(models.Model):
|
||||
participation = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("participation"),
|
||||
related_name="solutions",
|
||||
)
|
||||
|
||||
problem = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("problem"),
|
||||
choices=[
|
||||
(i, format_lazy(_("Problem #{problem}"), problem=i)) for i in range(1, settings.PROBLEM_COUNT + 1)
|
||||
],
|
||||
)
|
||||
|
||||
final_solution = models.BooleanField(
|
||||
verbose_name=_("solution for the final tournament"),
|
||||
default=False,
|
||||
)
|
||||
|
||||
file = models.FileField(
|
||||
verbose_name=_("file"),
|
||||
upload_to=get_solution_filename,
|
||||
unique=True,
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return _("Solution of team {team} for problem {problem}")\
|
||||
.format(team=self.participation.team.name, problem=self.problem)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("solution")
|
||||
verbose_name_plural = _("solutions")
|
||||
unique_together = (('participation', 'problem', 'final_solution', ), )
|
||||
|
||||
|
||||
class Synthesis(models.Model):
|
||||
participation = models.ForeignKey(
|
||||
Participation,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("participation"),
|
||||
)
|
||||
|
||||
passage = models.ForeignKey(
|
||||
Passage,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="syntheses",
|
||||
verbose_name=_("passage"),
|
||||
)
|
||||
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=[
|
||||
(1, _("opponent"), ),
|
||||
(2, _("reporter"), ),
|
||||
]
|
||||
)
|
||||
|
||||
file = models.FileField(
|
||||
verbose_name=_("file"),
|
||||
upload_to=get_synthesis_filename,
|
||||
unique=True,
|
||||
blank=True,
|
||||
default="",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return _("Synthesis for the {type} of the {passage}").format(type=self.get_type_display(), passage=self.passage)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("synthesis")
|
||||
verbose_name_plural = _("syntheses")
|
||||
unique_together = (('participation', 'passage', 'type', ), )
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
jury = models.ForeignKey(
|
||||
VolunteerRegistration,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("jury"),
|
||||
related_name="notes",
|
||||
)
|
||||
|
||||
passage = models.ForeignKey(
|
||||
Passage,
|
||||
on_delete=models.CASCADE,
|
||||
verbose_name=_("passage"),
|
||||
related_name="notes",
|
||||
)
|
||||
|
||||
defender_writing = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("defender writing note"),
|
||||
choices=[(i, i) for i in range(0, 21)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
defender_oral = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("defender oral note"),
|
||||
choices=[(i, i) for i in range(0, 17)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
opponent_writing = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("opponent writing note"),
|
||||
choices=[(i, i) for i in range(0, 10)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
opponent_oral = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("opponent oral note"),
|
||||
choices=[(i, i) for i in range(0, 11)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
reporter_writing = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("reporter writing note"),
|
||||
choices=[(i, i) for i in range(0, 10)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
reporter_oral = models.PositiveSmallIntegerField(
|
||||
verbose_name=_("reporter oral note"),
|
||||
choices=[(i, i) for i in range(0, 11)],
|
||||
default=0,
|
||||
)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse_lazy("participation:passage_detail", args=(self.passage.pk,))
|
||||
|
||||
def __str__(self):
|
||||
return _("Notes of {jury} for {passage}").format(jury=self.jury, passage=self.passage)
|
||||
|
||||
def __bool__(self):
|
||||
return any((self.defender_writing, self.defender_oral, self.opponent_writing, self.opponent_oral,
|
||||
self.reporter_writing, self.reporter_oral))
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("note")
|
||||
verbose_name_plural = _("notes")
|
36
apps/participation/search_indexes.py
Normal file
36
apps/participation/search_indexes.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from haystack import indexes
|
||||
|
||||
from .models import Participation, Team, Tournament
|
||||
|
||||
|
||||
class TeamIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||
"""
|
||||
Index all teams by their name and trigram.
|
||||
"""
|
||||
text = indexes.NgramField(document=True, use_template=True)
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
|
||||
|
||||
class ParticipationIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||
"""
|
||||
Index all participations by their team name and team trigram.
|
||||
"""
|
||||
text = indexes.NgramField(document=True, use_template=True)
|
||||
|
||||
class Meta:
|
||||
model = Participation
|
||||
|
||||
|
||||
class TournamentIndex(indexes.ModelSearchIndex, indexes.Indexable):
|
||||
"""
|
||||
Index all tournaments by their name.
|
||||
"""
|
||||
text = indexes.NgramField(document=True, use_template=True)
|
||||
|
||||
class Meta:
|
||||
model = Tournament
|
46
apps/participation/signals.py
Normal file
46
apps/participation/signals.py
Normal file
@ -0,0 +1,46 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
from typing import Union
|
||||
|
||||
from participation.models import Note, Participation, Passage, Pool, Team
|
||||
from tfjm.lists import get_sympa_client
|
||||
|
||||
|
||||
def create_team_participation(instance, created, **_):
|
||||
"""
|
||||
When a team got created, create an associated participation.
|
||||
"""
|
||||
participation = Participation.objects.get_or_create(team=instance)[0]
|
||||
participation.save()
|
||||
if not created:
|
||||
participation.team.create_mailing_list()
|
||||
|
||||
|
||||
def update_mailing_list(instance: Team, **_):
|
||||
"""
|
||||
When a team name or trigram got updated, update mailing lists and Matrix rooms
|
||||
"""
|
||||
if instance.pk:
|
||||
old_team = Team.objects.get(pk=instance.pk)
|
||||
if old_team.trigram != instance.trigram:
|
||||
# TODO Rename Matrix room
|
||||
# Delete old mailing list, create a new one
|
||||
old_team.delete_mailing_list()
|
||||
instance.create_mailing_list()
|
||||
# Subscribe all team members in the mailing list
|
||||
for student in instance.students.all():
|
||||
get_sympa_client().subscribe(student.user.email, f"equipe-{instance.trigram.lower()}", False,
|
||||
f"{student.user.first_name} {student.user.last_name}")
|
||||
for coach in instance.coaches.all():
|
||||
get_sympa_client().subscribe(coach.user.email, f"equipe-{instance.trigram.lower()}", False,
|
||||
f"{coach.user.first_name} {coach.user.last_name}")
|
||||
|
||||
|
||||
def create_notes(instance: Union[Passage, Pool], **_):
|
||||
if isinstance(instance, Pool):
|
||||
for passage in instance.passages.all():
|
||||
create_notes(passage)
|
||||
return
|
||||
|
||||
for jury in instance.pool.juries.all():
|
||||
Note.objects.get_or_create(jury=jury, passage=instance)
|
142
apps/participation/tables.py
Normal file
142
apps/participation/tables.py
Normal file
@ -0,0 +1,142 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.utils import formats
|
||||
from django.utils.text import format_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
import django_tables2 as tables
|
||||
|
||||
from .models import Note, Passage, Pool, Team, Tournament
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
class TeamTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'participation:team_detail',
|
||||
args=[tables.A("id")],
|
||||
verbose_name=lambda: _("name").capitalize(),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped',
|
||||
}
|
||||
model = Team
|
||||
fields = ('name', 'trigram',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
class ParticipationTable(tables.Table):
|
||||
name = tables.LinkColumn(
|
||||
'participation:team_detail',
|
||||
args=[tables.A("team__id")],
|
||||
verbose_name=_("name").capitalize,
|
||||
accessor="team__name",
|
||||
)
|
||||
|
||||
trigram = tables.Column(
|
||||
verbose_name=_("trigram").capitalize,
|
||||
accessor="team__trigram",
|
||||
)
|
||||
|
||||
valid = tables.Column(
|
||||
verbose_name=_("valid").capitalize,
|
||||
accessor="valid",
|
||||
empty_values=(),
|
||||
)
|
||||
|
||||
def render_valid(self, value):
|
||||
return _("Validated") if value else _("Validation pending") if value is False else _("Not validated")
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped',
|
||||
}
|
||||
model = Team
|
||||
fields = ('name', 'trigram', 'valid',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
|
||||
class TournamentTable(tables.Table):
|
||||
name = tables.LinkColumn()
|
||||
|
||||
date = tables.Column(_("date").capitalize, accessor="id")
|
||||
|
||||
def render_date(self, record):
|
||||
return format_lazy(_("From {start} to {end}"),
|
||||
start=formats.date_format(record.date_start, format="SHORT_DATE_FORMAT", use_l10n=True),
|
||||
end=formats.date_format(record.date_end, format="SHORT_DATE_FORMAT", use_l10n=True))
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped',
|
||||
}
|
||||
model = Tournament
|
||||
fields = ('name', 'date',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
|
||||
class PoolTable(tables.Table):
|
||||
teams = tables.LinkColumn(
|
||||
'participation:pool_detail',
|
||||
args=[tables.A('id')],
|
||||
verbose_name=_("teams").capitalize,
|
||||
empty_values=(),
|
||||
)
|
||||
|
||||
def render_teams(self, record):
|
||||
return ", ".join(participation.team.trigram for participation in record.participations.all()) \
|
||||
or _("No defined team")
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped',
|
||||
}
|
||||
model = Pool
|
||||
fields = ('teams', 'round', 'tournament',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
|
||||
class PassageTable(tables.Table):
|
||||
defender = tables.LinkColumn(
|
||||
"participation:passage_detail",
|
||||
args=[tables.A("id")],
|
||||
verbose_name=_("defender").capitalize,
|
||||
)
|
||||
|
||||
def render_defender(self, value):
|
||||
return value.team
|
||||
|
||||
def render_opponent(self, value):
|
||||
return value.team
|
||||
|
||||
def render_reporter(self, value):
|
||||
return value.team
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped text-center',
|
||||
}
|
||||
model = Passage
|
||||
fields = ('defender', 'opponent', 'reporter', 'place',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
||||
|
||||
|
||||
class NoteTable(tables.Table):
|
||||
jury = tables.Column(
|
||||
attrs={
|
||||
"td": {
|
||||
"class": "text-nowrap",
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
class Meta:
|
||||
attrs = {
|
||||
'class': 'table table-condensed table-striped text-center',
|
||||
}
|
||||
model = Note
|
||||
fields = ('jury', 'defender_writing', 'defender_oral', 'opponent_writing', 'opponent_oral',
|
||||
'reporter_writing', 'reporter_oral',)
|
||||
template_name = 'django_tables2/bootstrap4.html'
|
39
apps/participation/templates/participation/chat.html
Normal file
39
apps/participation/templates/participation/chat.html
Normal file
@ -0,0 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-warning">
|
||||
{% blocktrans trimmed %}
|
||||
The chat is located on the dedicated Matrix server:
|
||||
{% endblocktrans %}
|
||||
</div>
|
||||
|
||||
<div class="alert text-center">
|
||||
<a class="btn btn-success" href="https://element.tfjm.org/#/room/#faq:tfjm.org" target="_blank">
|
||||
<i class="fas fa-server"></i> {% trans "Access to the Matrix server" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
To connect to the server, you can select "Log in", then use your credentials of this platform to connect
|
||||
with the central authentication server, then you must trust the connection between the Matrix account and the
|
||||
platform. Finally, you will be able to access to the chat platform.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
You will be invited in some basic rooms. You must confirm the invitations to join channels.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
{% blocktrans trimmed %}
|
||||
If you have any trouble, don't hesitate to contact us :)
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
13
apps/participation/templates/participation/create_team.html
Normal file
13
apps/participation/templates/participation/create_team.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
13
apps/participation/templates/participation/join_team.html
Normal file
13
apps/participation/templates/participation/join_team.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Join" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Demande de validation - TFJM²</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
Bonjour {{ user.registration }},
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
|
||||
au {{ team.participation.get_problem_display }} du TFJM².
|
||||
Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
|
||||
<a href="https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}">
|
||||
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Cordialement,
|
||||
</p>
|
||||
|
||||
<p>
|
||||
L'organisation du TFJM²
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,10 @@
|
||||
Bonjour {{ user.registration }},
|
||||
|
||||
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer
|
||||
au {{ team.participation.get_problem_display }} du TFJM².
|
||||
Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
|
||||
https://{{ domain }}{% url "participation:team_detail" pk=team.pk %}
|
||||
|
||||
Cordialement,
|
||||
|
||||
L'organisation du TFJM²
|
@ -5,22 +5,18 @@
|
||||
<title>Équipe non validée – TFJM²</title>
|
||||
</head>
|
||||
<body>
|
||||
Bonjour {{ user }},<br/>
|
||||
<br/>
|
||||
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations sont correctes.
|
||||
{% if message %}
|
||||
<p>
|
||||
Le CNO vous adresse le message suivant :
|
||||
<div>
|
||||
{{ message }}
|
||||
</div>
|
||||
</p>
|
||||
{% endif %}
|
||||
Bonjour,<br/>
|
||||
<br />
|
||||
N'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a> pour plus d'informations.
|
||||
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations
|
||||
de droit à l'image sont correctes. Les organisateurs vous adressent ce message :<br />
|
||||
<br />
|
||||
{{ message }}<br />
|
||||
<br />
|
||||
N'hésitez pas à nous contacter à l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a>
|
||||
pour plus d'informations.
|
||||
<br/>
|
||||
Avec toute notre bienveillance,<br/>
|
||||
Cordialement,<br/>
|
||||
<br/>
|
||||
Le comité national d'organisation du TFJM<sup>2</sup>
|
||||
Le comité d'organisation du TFJM²
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,12 @@
|
||||
Bonjour,
|
||||
|
||||
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos
|
||||
autorisations de droit à l'image sont correctes. Les organisateurs vous adressent ce message :
|
||||
|
||||
{{ message }}
|
||||
|
||||
N'hésitez pas à nous contacter à l'adresse contact@tfjm.org pour plus d'informations.
|
||||
|
||||
Cordialement,
|
||||
|
||||
Le comité d'organisation du TFJM²
|
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Équipe validée – TFJM²</title>
|
||||
</head>
|
||||
<body>
|
||||
Bonjour,<br/>
|
||||
<br/>
|
||||
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte
|
||||
à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.<br>
|
||||
Les organisateurs vous adressent ce message :<br/>
|
||||
<br/>
|
||||
{{ message }}<br />
|
||||
<br/>
|
||||
Cordialement,<br/>
|
||||
<br/>
|
||||
Le comité d'organisation du TFJM²
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,12 @@
|
||||
Bonjour,
|
||||
|
||||
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte
|
||||
à travailler sur vos problèmes. Vous pourrez ensuite envoyer vos solutions sur la plateforme.
|
||||
|
||||
Les organisateurs vous adressent ce message :
|
||||
|
||||
{{ message }}
|
||||
|
||||
Cordialement,
|
||||
|
||||
Le comité d'organisation du TFJM²
|
13
apps/participation/templates/participation/note_form.html
Normal file
13
apps/participation/templates/participation/note_form.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,65 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "any" as any %}
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{% trans "Participation of team" %} {{ participation.team.name }} ({{ participation.team.trigram }})</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-2">{% trans "Team:" %}</dt>
|
||||
<dd class="col-sm-10"><a href="{% url "participation:team_detail" pk=participation.team.pk %}">{{ participation.team }}</a></dd>
|
||||
|
||||
<dt class="col-sm-2">{% trans "Tournament:" %}</dt>
|
||||
<dd class="col-sm-10">
|
||||
{% if participation.tournament %}
|
||||
<a href="{% url "participation:tournament_detail" pk=participation.tournament.pk %}">{{ participation.tournament }}</a>
|
||||
{% else %}
|
||||
{% trans "any" %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-2">{% trans "Solutions:" %}</dt>
|
||||
<dd class="col-sm-10">
|
||||
{% for solution in participation.solutions.all %}
|
||||
<a href="{{ solution.file.url }}" data-turbolinks="false">{{ solution }}{% if not forloop.last %}, {% endif %}</a>
|
||||
{% empty %}
|
||||
{% trans "No solution was uploaded yet." %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
{% if participation.pools.all %}
|
||||
<dt class="col-sm-2">{% trans "Pools:" %}</dt>
|
||||
<dd class="col-sm-10">
|
||||
{% for pool in participation.pools.all %}
|
||||
<a href="{{ pool.get_absolute_url }}" data-turbolinks="false">{{ pool }}{% if not forloop.last %}, {% endif %}</a>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadSolutionModal">{% trans "Upload solution" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% trans "Upload solution" as modal_title %}
|
||||
{% trans "Upload" as modal_button %}
|
||||
{% url "participation:upload_solution" pk=participation.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="uploadSolution" modal_enctype="multipart/form-data" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('button[data-target="#uploadSolutionModal"]').click(function() {
|
||||
let modalBody = $("#uploadSolutionModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:upload_solution" pk=participation.pk %} #form-content")
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
140
apps/participation/templates/participation/passage_detail.html
Normal file
140
apps/participation/templates/participation/passage_detail.html
Normal file
@ -0,0 +1,140 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load django_tables2 i18n %}
|
||||
|
||||
{% block content %}
|
||||
{% trans "any" as any %}
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ passage }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">{% trans "Pool:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.pool.get_absolute_url }}">{{ passage.pool }}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Defender:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.defender.get_absolute_url }}">{{ passage.defender.team }}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Opponent:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.opponent.get_absolute_url }}">{{ passage.opponent.team }}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Reporter:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.reporter.get_absolute_url }}">{{ passage.reporter.team }}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Defended solution:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ passage.defended_solution.file.url }}" data-turbolinks="false">{{ passage.defended_solution }}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Place:" %}</dt>
|
||||
<dd class="col-sm-9">{{ passage.place }}</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Syntheses:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% for synthesis in passage.syntheses.all %}
|
||||
<a href="{{ synthesis.file.url }}" data-turbolinks="false">{{ synthesis }}{% if not forloop.last %}, {% endif %}</a>
|
||||
{% empty %}
|
||||
{% trans "No synthesis was uploaded yet." %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
{% if user.registration.is_admin %}
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-info" data-toggle="modal" data-target="#updateNotesModal">{% trans "Update notes" %}</button>
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#updatePassageModal">{% trans "Update" %}</button>
|
||||
</div>
|
||||
{% elif user.registration.participates %}
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#uploadSynthesisModal">{% trans "Upload synthesis" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if notes %}
|
||||
<hr>
|
||||
|
||||
<h2>{% trans "Notes detail" %}</h2>
|
||||
|
||||
{% render_table notes %}
|
||||
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-8">{% trans "Average points for the defender writing:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_defender_writing }}/20</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the defender oral:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_defender_oral }}/16</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the opponent writing:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_opponent_writing }}/9</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the opponent oral:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_opponent_oral }}/10</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the reporter writing:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter_writing }}/9</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Average points for the reporter oral:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter_oral }}/10</dd>
|
||||
</dl>
|
||||
|
||||
<hr>
|
||||
|
||||
<dl class="row">
|
||||
<dt class="col-sm-8">{% trans "Defender points:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_defender }}/52</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Opponent points:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_opponent }}/29</dd>
|
||||
|
||||
<dt class="col-sm-8">{% trans "Reporter points:" %}</dt>
|
||||
<dd class="col-sm-4">{{ passage.average_reporter }}/19</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.is_admin %}
|
||||
{% trans "Update passage" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:passage_update" pk=passage.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updatePassage" %}
|
||||
|
||||
{% trans "Update notes" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:update_notes" pk=my_note.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updateNotes" %}
|
||||
{% elif user.registration.participates %}
|
||||
{% trans "Upload synthesis" as modal_title %}
|
||||
{% trans "Upload" as modal_button %}
|
||||
{% url "participation:upload_synthesis" pk=passage.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="uploadSynthesis" modal_enctype="multipart/form-data" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
{% if user.registration.is_admin %}
|
||||
$('button[data-target="#updatePassageModal"]').click(function() {
|
||||
let modalBody = $("#updatePassageModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:passage_update" pk=passage.pk %} #form-content")
|
||||
});
|
||||
|
||||
$('button[data-target="#updateNotesModal"]').click(function() {
|
||||
let modalBody = $("#updateNotesModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:update_notes" pk=my_note.pk %} #form-content")
|
||||
});
|
||||
{% elif user.registration.participates %}
|
||||
$('button[data-target="#uploadSynthesisModal"]').click(function() {
|
||||
let modalBody = $("#uploadSynthesisModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:upload_synthesis" pk=passage.pk %} #form-content")
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
13
apps/participation/templates/participation/passage_form.html
Normal file
13
apps/participation/templates/participation/passage_form.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update passage" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
105
apps/participation/templates/participation/pool_detail.html
Normal file
105
apps/participation/templates/participation/pool_detail.html
Normal file
@ -0,0 +1,105 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load django_tables2 i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ pool }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-3">{% trans "Tournament:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ pool.tournament.get_absolute_url }}">{{ pool.tournament }}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Round:" %}</dt>
|
||||
<dd class="col-sm-9">{{ pool.get_round_display }}</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Teams:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% for participation in pool.participations.all %}
|
||||
<a href="{{ participation.get_absolute_url }}" data-turbolinks="false">{{ participation.team }}{% if not forloop.last %}, {% endif %}</a>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Juries:" %}</dt>
|
||||
<dd class="col-sm-9">{{ pool.juries.all|join:", " }}</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "Defended solutions:" %}</dt>
|
||||
<dd class="col-sm-9">
|
||||
{% for passage in pool.passages.all %}
|
||||
<a href="{{ passage.defended_solution.file.url }}" data-turbolinks="false">{{ passage.defended_solution }}{% if not forloop.last %}, {% endif %}</a>
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-3">{% trans "BigBlueButton link:" %}</dt>
|
||||
<dd class="col-sm-9"><a href="{{ pool.bbb_url }}">{{ pool.bbb_url }}</a></dd>
|
||||
</dl>
|
||||
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h5>{% trans "Ranking" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
{% for participation, note in notes %}
|
||||
<li><strong>{{ participation.team }} :</strong> {{ note }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% if user.registration.is_admin %}
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-success" data-toggle="modal" data-target="#addPassageModal">{% trans "Add passage" %}</button>
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#updatePoolModal">{% trans "Update" %}</button>
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#updateTeamsModal">{% trans "Update teams" %}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>{% trans "Passages" %}</h3>
|
||||
|
||||
{% render_table passages %}
|
||||
|
||||
{% trans "Add passage" as modal_title %}
|
||||
{% trans "Add" as modal_button %}
|
||||
{% url "participation:passage_create" pk=pool.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="addPassage" modal_button_type="success" %}
|
||||
|
||||
{% trans "Update pool" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:pool_update" pk=pool.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updatePool" %}
|
||||
|
||||
{% trans "Update teams" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:pool_update_teams" pk=pool.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updateTeams" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
$('button[data-target="#updatePoolModal"]').click(function() {
|
||||
let modalBody = $("#updatePoolModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:pool_update" pk=pool.pk %} #form-content")
|
||||
});
|
||||
|
||||
$('button[data-target="#updateTeamsModal"]').click(function() {
|
||||
let modalBody = $("#updateTeamsModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:pool_update_teams" pk=pool.pk %} #form-content")
|
||||
});
|
||||
|
||||
$('button[data-target="#addPassageModal"]').click(function() {
|
||||
let modalBody = $("#addPassageModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:passage_create" pk=pool.pk %} #form-content")
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
13
apps/participation/templates/participation/pool_form.html
Normal file
13
apps/participation/templates/participation/pool_form.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update pool" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
175
apps/participation/templates/participation/team_detail.html
Normal file
175
apps/participation/templates/participation/team_detail.html
Normal file
@ -0,0 +1,175 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% load crispy_forms_filters %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ team.name }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-sm-6 text-right">{% trans "Name:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.name }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Trigram:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.trigram }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Email:" %}</dt>
|
||||
<dd class="col-sm-6"><a href="mailto:{{ team.email }}">{{ team.email }}</a></dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Access code:" %}</dt>
|
||||
<dd class="col-sm-6">{{ team.access_code }}</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Coaches:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for coach in team.coaches.all %}
|
||||
<a href="{% url "registration:user_detail" pk=coach.user.pk %}">{{ coach }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% empty %}
|
||||
{% trans "any" %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Participants:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for student in team.students.all %}
|
||||
<a href="{% url "registration:user_detail" pk=student.user.pk %}">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% empty %}
|
||||
{% trans "any" %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Tournament:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% if team.participation.tournament %}
|
||||
<a href="{% url "participation:tournament_detail" pk=team.participation.tournament.pk %}">{{ team.participation.tournament }}</a>
|
||||
{% else %}
|
||||
{% trans "any" %}
|
||||
{% endif %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Photo authorizations:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for participant in team.participants.all %}
|
||||
{% if participant.photo_authorization %}
|
||||
<a href="{{ participant.photo_authorization.url }}" data-turbolinks="false">{{ participant }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% else %}
|
||||
{{ participant }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Health sheets:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for student in team.students.all %}
|
||||
{% if student.under_18 %}
|
||||
{% if student.health_sheet %}
|
||||
<a href="{{ student.health_sheet.url }}" data-turbolinks="false">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% else %}
|
||||
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-6 text-right">{% trans "Parental authorizations:" %}</dt>
|
||||
<dd class="col-sm-6">
|
||||
{% for student in team.students.all %}
|
||||
{% if student.under_18 %}
|
||||
{% if student.parental_authorization %}
|
||||
<a href="{{ student.parental_authorization.url }}" data-turbolinks="false">{{ student }}</a>{% if not forloop.last %},{% endif %}
|
||||
{% else %}
|
||||
{{ student }} ({% trans "Not uploaded yet" %}){% if not forloop.last %},{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="card-footer text-center">
|
||||
<button class="btn btn-primary" data-toggle="modal" data-target="#updateTeamModal">{% trans "Update" %}</button>
|
||||
{% if not team.participation.valid %}
|
||||
<button class="btn btn-danger" data-toggle="modal" data-target="#leaveTeamModal">{% trans "Leave" %}</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
{% if team.participation.valid %}
|
||||
<div class="text-center">
|
||||
<a class="btn btn-info" href="{% url "participation:participation_detail" pk=team.participation.pk %}">
|
||||
<i class="fas fa-file-pdf"></i> {% trans "Access to team participation" %}
|
||||
</a>
|
||||
</div>
|
||||
{% elif team.participation.valid == None %} {# Team did not ask for validation #}
|
||||
{% if user.registration.participates %}
|
||||
{% if can_validate %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "Your team has at least 4 members and a coach and all authorizations were given: the team can be validated." %}
|
||||
<div class="text-center">
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ request_validation_form|crispy }}
|
||||
<button class="btn btn-success" name="request-validation">{% trans "Submit my team to validation" %}</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Your team must be composed of 4 members and a coach and each member must upload their authorizations and confirm its email address." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "This team didn't ask for validation yet." %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% else %} {# Team is waiting for validation #}
|
||||
{% if user.registration.participates %}
|
||||
<div class="alert alert-warning">
|
||||
{% trans "Your validation is pending." %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
{% trans "The team requested to be validated. You may now control the authorizations and confirm that they can participate." %}
|
||||
</div>
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
{{ validation_form|crispy }}
|
||||
<div class="input-group btn-group">
|
||||
<button class="btn btn-success" name="validate" type="submit">{% trans "Validate" %}</button>
|
||||
<button class="btn btn-danger" name="invalidate" type="submit">{% trans "Invalidate" %}</button>
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% trans "Update team" as modal_title %}
|
||||
{% trans "Update" as modal_button %}
|
||||
{% url "participation:update_team" pk=team.pk as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="updateTeam" %}
|
||||
|
||||
{% trans "Leave team" as modal_title %}
|
||||
{% trans "Leave" as modal_button %}
|
||||
{% url "participation:team_leave" as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="leaveTeam" modal_button_type="danger" %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('button[data-target="#updateTeamModal"]').click(function() {
|
||||
let modalBody = $("#updateTeamModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:update_team" pk=team.pk %} #form-content");
|
||||
});
|
||||
$('button[data-target="#leaveTeamModal"]').click(function() {
|
||||
let modalBody = $("#leaveTeamModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:team_leave" %} #form-content");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
13
apps/participation/templates/participation/team_leave.html
Normal file
13
apps/participation/templates/participation/team_leave.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div class="alert alert-warning" id="form-content">
|
||||
{% csrf_token %}
|
||||
{% trans "Are you sure that you want to leave this team?" %}
|
||||
</div>
|
||||
<button class="btn btn-danger" type="submit">{% trans "Leave" %}</button>
|
||||
</form>
|
||||
{% endblock %}
|
13
apps/participation/templates/participation/team_list.html
Normal file
13
apps/participation/templates/participation/team_list.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load django_tables2 i18n %}
|
||||
|
||||
{% block contenttitle %}
|
||||
<h1>{% trans "All teams" %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-content">
|
||||
{% render_table table %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -0,0 +1,123 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load getconfig i18n django_tables2 %}
|
||||
|
||||
{% block content %}
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h4>{{ tournament.name }}</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl class="row">
|
||||
<dt class="col-xl-6 text-right">{% trans 'organizers'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.organizers.all|join:", " }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'size'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.max_teams }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'place'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.place }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'price'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{% if tournament.price %}{{ tournament.price }} €{% else %}{% trans "Free" %}{% endif %}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'dates'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{% trans "From" %} {{ tournament.date_start }} {% trans "to" %} {{ tournament.date_end }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'date of registration closing'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.inscription_limit }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'date of maximal solution submission'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.solution_limit }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'date of the random draw'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.solutions_draw }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'date of maximal syntheses submission for the first round'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.syntheses_first_phase_limit }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'date when solutions of round 2 are available'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.solutions_available_second_phase }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'date of maximal syntheses submission for the second round'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.syntheses_second_phase_limit }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'description'|capfirst %}</dt>
|
||||
<dd class="col-xl-6">{{ tournament.description }}</dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'To contact organizers' %}</dt>
|
||||
<dd class="col-xl-6"><a href="mailto:{{ tournament.organizers_email }}">{{ tournament.organizers_email }}</a></dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'To contact juries' %}</dt>
|
||||
<dd class="col-xl-6"><a href="mailto:{{ tournament.jurys_email }}">{{ tournament.jurys_email }}</a></dd>
|
||||
|
||||
<dt class="col-xl-6 text-right">{% trans 'To contact valid teams' %}</dt>
|
||||
<dd class="col-xl-6"><a href="mailto:{{ tournament.teams_email }}">{{ tournament.teams_email }}</a></dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
{% if user.registration.is_admin or user.registration in tournament.organizers.all %}
|
||||
<div class="card-footer text-center">
|
||||
<a href="{% url "participation:tournament_update" pk=tournament.pk %}"><button class="btn btn-secondary">{% trans "Edit tournament" %}</button></a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>{% trans "Teams" %}</h3>
|
||||
<div id="teams_table">
|
||||
{% render_table teams %}
|
||||
</div>
|
||||
|
||||
{% if pools.data %}
|
||||
<hr>
|
||||
|
||||
<h3>{% trans "Pools" %}</h3>
|
||||
<div id="pools_table">
|
||||
{% render_table pools %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.is_admin %}
|
||||
<button class="btn btn-block btn-success" data-toggle="modal" data-target="#addPoolModal">{% trans "Add new pool" %}</button>
|
||||
{% endif %}
|
||||
|
||||
{% if notes %}
|
||||
<hr>
|
||||
|
||||
<div class="card bg-light shadow">
|
||||
<div class="card-header text-center">
|
||||
<h5>{% trans "Ranking" %}</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<ul>
|
||||
{% for participation, note in notes %}
|
||||
<li><strong>{{ participation.team }} :</strong> {{ note }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if user.registration.is_admin %}
|
||||
{% trans "Add pool" as modal_title %}
|
||||
{% trans "Add" as modal_button %}
|
||||
{% url "participation:pool_create" as modal_action %}
|
||||
{% include "base_modal.html" with modal_id="addPool" %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extrajavascript %}
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
{% if user.registration.is_admin %}
|
||||
$('button[data-target="#addPoolModal"]').click(function() {
|
||||
let modalBody = $("#addPoolModal div.modal-body");
|
||||
if (!modalBody.html().trim())
|
||||
modalBody.load("{% url "participation:pool_create" %} #form-content")
|
||||
});
|
||||
{% endif %}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
@ -0,0 +1,17 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
{% if object.pk %}
|
||||
<button class="btn btn-primary" type="submit">{% trans "Update" %}</button>
|
||||
{% else %}
|
||||
<button class="btn btn-success" type="submit">{% trans "Create" %}</button>
|
||||
{% endif %}
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,16 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load django_tables2 i18n %}
|
||||
|
||||
{% block contenttitle %}
|
||||
<h1>{% trans "All tournaments" %}</h1>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="form-content">
|
||||
{% render_table table %}
|
||||
{% if user.registration.is_admin %}
|
||||
<a class="btn btn-block btn-success" href="{% url "participation:tournament_create" %}">{% trans "Add tournament" %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
15
apps/participation/templates/participation/update_team.html
Normal file
15
apps/participation/templates/participation/update_team.html
Normal file
@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
{{ participation_form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-success" type="submit">{% trans "Update" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
||||
|
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% load crispy_forms_filters i18n %}
|
||||
|
||||
{% block content %}
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div id="form-content">
|
||||
{% csrf_token %}
|
||||
{{ form|crispy }}
|
||||
</div>
|
||||
<button class="btn btn-primary" type="submit">{% trans "Upload" %}</button>
|
||||
</form>
|
||||
{% endblock content %}
|
@ -0,0 +1,3 @@
|
||||
{{ object.team.name }}
|
||||
{{ object.team.trigram }}
|
||||
{{ object.tournament.name }}
|
@ -0,0 +1,2 @@
|
||||
{{ object.name }}
|
||||
{{ object.trigram }}
|
@ -0,0 +1,3 @@
|
||||
{{ object.name }}
|
||||
{{ object.place }}
|
||||
{{ object.description }}
|
@ -0,0 +1,5 @@
|
||||
{{ object.link }}
|
||||
{{ object.participation.team.name }}
|
||||
{{ object.participation.team.trigram }}
|
||||
{{ object.participation.problem }}
|
||||
{{ object.participation.get_problem_display }}
|
585
apps/participation/tests.py
Normal file
585
apps/participation/tests.py
Normal file
@ -0,0 +1,585 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.contrib.sites.models import Site
|
||||
from django.core.management import call_command
|
||||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from registration.models import CoachRegistration, StudentRegistration
|
||||
|
||||
from .models import Participation, Team, Tournament
|
||||
|
||||
|
||||
class TestStudentParticipation(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.superuser = User.objects.create_superuser(
|
||||
username="admin",
|
||||
email="admin@example.com",
|
||||
password="toto1234",
|
||||
)
|
||||
|
||||
self.user = User.objects.create(
|
||||
first_name="Toto",
|
||||
last_name="Toto",
|
||||
email="toto@example.com",
|
||||
password="toto",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.user,
|
||||
student_class=12,
|
||||
school="Earth",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.team = Team.objects.create(
|
||||
name="Super team",
|
||||
trigram="AAA",
|
||||
access_code="azerty",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.second_user = User.objects.create(
|
||||
first_name="Lalala",
|
||||
last_name="Lalala",
|
||||
email="lalala@example.com",
|
||||
password="lalala",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=self.second_user,
|
||||
student_class=11,
|
||||
school="Moon",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
)
|
||||
self.second_team = Team.objects.create(
|
||||
name="Poor team",
|
||||
trigram="FFF",
|
||||
access_code="qwerty",
|
||||
)
|
||||
|
||||
self.coach = User.objects.create(
|
||||
first_name="Coach",
|
||||
last_name="Coach",
|
||||
email="coach@example.com",
|
||||
password="coach",
|
||||
)
|
||||
CoachRegistration.objects.create(user=self.coach)
|
||||
|
||||
self.tournament = Tournament.objects.create(
|
||||
name="France",
|
||||
place="Here",
|
||||
)
|
||||
|
||||
def test_admin_pages(self):
|
||||
"""
|
||||
Load Django-admin pages.
|
||||
"""
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
# Test team pages
|
||||
response = self.client.get(reverse("admin:index") + "participation/team/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/team/{self.team.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Team).id}/"
|
||||
f"{self.team.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.team.get_absolute_url()), 302, 200)
|
||||
|
||||
# Test participation pages
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.get(reverse("admin:index") + "participation/participation/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.get(reverse("admin:index")
|
||||
+ f"participation/participation/{self.team.participation.pk}/change/")
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response = self.client.get(reverse("admin:index") +
|
||||
f"r/{ContentType.objects.get_for_model(Participation).id}/"
|
||||
f"{self.team.participation.pk}/")
|
||||
self.assertRedirects(response, "http://" + Site.objects.get().domain +
|
||||
str(self.team.participation.get_absolute_url()), 302, 200)
|
||||
|
||||
def test_create_team(self):
|
||||
"""
|
||||
Try to create a team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:create_team"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team",
|
||||
trigram="123",
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team",
|
||||
trigram="TES",
|
||||
))
|
||||
self.assertTrue(Team.objects.filter(trigram="TES").exists())
|
||||
team = Team.objects.get(trigram="TES")
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(team.pk,)), 302, 200)
|
||||
|
||||
# Already in a team
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team 2",
|
||||
trigram="TET",
|
||||
))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_join_team(self):
|
||||
"""
|
||||
Try to join an existing team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:join_team"))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
team = Team.objects.create(name="Test", trigram="TES")
|
||||
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code="éééééé",
|
||||
))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code=team.access_code,
|
||||
))
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(team.pk,)), 302, 200)
|
||||
self.assertTrue(Team.objects.filter(trigram="TES").exists())
|
||||
|
||||
# Already joined
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code=team.access_code,
|
||||
))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_team_list(self):
|
||||
"""
|
||||
Test to display the list of teams.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:team_list"))
|
||||
self.assertTrue(response.status_code, 200)
|
||||
|
||||
def test_no_myteam_redirect_noteam(self):
|
||||
"""
|
||||
Test redirection.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_team_detail"))
|
||||
self.assertTrue(response.status_code, 200)
|
||||
|
||||
def test_team_detail(self):
|
||||
"""
|
||||
Try to display the information of a team.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
response = self.client.get(reverse("participation:my_team_detail"))
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
|
||||
response = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Can't see other teams
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.client.force_login(self.second_user)
|
||||
response = self.client.get(reverse("participation:team_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_request_validate_team(self):
|
||||
"""
|
||||
The team ask for validation.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
second_user = User.objects.create(
|
||||
first_name="Blublu",
|
||||
last_name="Blublu",
|
||||
email="blublu@example.com",
|
||||
password="blublu",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=second_user,
|
||||
student_class=12,
|
||||
school="Jupiter",
|
||||
give_contact_to_animath=True,
|
||||
email_confirmed=True,
|
||||
team=self.team,
|
||||
photo_authorization="authorization/photo/mai-linh",
|
||||
health_sheet="authorization/health/mai-linh",
|
||||
parental_authorization="authorization/parental/mai-linh",
|
||||
)
|
||||
|
||||
third_user = User.objects.create(
|
||||
first_name="Zupzup",
|
||||
last_name="Zupzup",
|
||||
email="zupzup@example.com",
|
||||
password="zupzup",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=third_user,
|
||||
student_class=10,
|
||||
school="Sun",
|
||||
give_contact_to_animath=False,
|
||||
email_confirmed=True,
|
||||
team=self.team,
|
||||
photo_authorization="authorization/photo/yohann",
|
||||
health_sheet="authorization/health/yohann",
|
||||
parental_authorization="authorization/parental/yohann",
|
||||
)
|
||||
|
||||
fourth_user = User.objects.create(
|
||||
first_name="tfjm",
|
||||
last_name="tfjm",
|
||||
email="tfjm@example.com",
|
||||
password="tfjm",
|
||||
)
|
||||
StudentRegistration.objects.create(
|
||||
user=fourth_user,
|
||||
student_class=10,
|
||||
school="Sun",
|
||||
give_contact_to_animath=False,
|
||||
email_confirmed=True,
|
||||
team=self.team,
|
||||
photo_authorization="authorization/photo/tfjm",
|
||||
health_sheet="authorization/health/tfjm",
|
||||
parental_authorization="authorization/parental/tfjm",
|
||||
)
|
||||
|
||||
self.coach.registration.team = self.team
|
||||
self.coach.registration.health_sheet = "authorization/health/coach"
|
||||
self.coach.registration.photo_authorization = "authorization/photo/coach"
|
||||
self.coach.registration.email_confirmed = True
|
||||
self.coach.registration.save()
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
# Admin users can't ask for validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.assertIsNone(self.team.participation.valid)
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertFalse(resp.context["can_validate"])
|
||||
# Can't validate
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.user.registration.photo_authorization = "authorization/photo/ananas"
|
||||
self.user.registration.health_sheet = "authorization/health/ananas"
|
||||
self.user.registration.parental_authorization = "authorization/parental/ananas"
|
||||
self.user.registration.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(resp.context["can_validate"])
|
||||
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertFalse(self.team.participation.valid)
|
||||
self.assertIsNotNone(self.team.participation.valid)
|
||||
|
||||
# Team already asked for validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="RequestValidationForm",
|
||||
engagement=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
def test_validate_team(self):
|
||||
"""
|
||||
A team asked for validation. Try to validate it.
|
||||
"""
|
||||
self.team.participation.valid = False
|
||||
self.team.participation.save()
|
||||
|
||||
# No right to do that
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="J'ai 4 ans",
|
||||
validate=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.client.force_login(self.superuser)
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.team.pk,)))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Woops I didn't said anything",
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
# Test invalidate team
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Wsh nope",
|
||||
invalidate=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertIsNone(self.team.participation.valid)
|
||||
|
||||
# Team did not ask validation
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Bienvenue ça va être trop cool",
|
||||
validate=True,
|
||||
))
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
self.team.participation.tournament = self.tournament
|
||||
self.team.participation.valid = False
|
||||
self.team.participation.save()
|
||||
|
||||
# Test validate team
|
||||
resp = self.client.post(reverse("participation:team_detail", args=(self.team.pk,)), data=dict(
|
||||
_form_type="ValidateParticipationForm",
|
||||
message="Bienvenue ça va être trop cool",
|
||||
validate=True,
|
||||
))
|
||||
self.assertRedirects(resp, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.team.participation.refresh_from_db()
|
||||
self.assertTrue(self.team.participation.valid)
|
||||
|
||||
def test_update_team(self):
|
||||
"""
|
||||
Try to update team information.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
self.coach.registration.team = self.team
|
||||
self.coach.registration.save()
|
||||
|
||||
response = self.client.get(reverse("participation:update_team", args=(self.team.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response = self.client.post(reverse("participation:update_team", args=(self.team.pk,)), data=dict(
|
||||
name="Updated team name",
|
||||
trigram="BBB",
|
||||
))
|
||||
self.assertRedirects(response, reverse("participation:team_detail", args=(self.team.pk,)), 302, 200)
|
||||
self.assertTrue(Team.objects.filter(trigram="BBB").exists())
|
||||
|
||||
def test_leave_team(self):
|
||||
"""
|
||||
A user is in a team, and leaves it.
|
||||
"""
|
||||
# User is not in a team
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
# Team is valid
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
# Unauthenticated users are redirected to login page
|
||||
self.client.logout()
|
||||
response = self.client.get(reverse("participation:team_leave"))
|
||||
self.assertRedirects(response, reverse("login") + "?next=" + reverse("participation:team_leave"), 302, 200)
|
||||
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.team.participation.valid = None
|
||||
self.team.participation.save()
|
||||
|
||||
response = self.client.post(reverse("participation:team_leave"))
|
||||
self.assertRedirects(response, reverse("index"), 302, 200)
|
||||
self.user.registration.refresh_from_db()
|
||||
self.assertIsNone(self.user.registration.team)
|
||||
self.assertFalse(Team.objects.filter(pk=self.team.pk).exists())
|
||||
|
||||
def test_no_myparticipation_redirect_nomyparticipation(self):
|
||||
"""
|
||||
Ensure a permission denied when we search my team participation when we are in no team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_participation_detail(self):
|
||||
"""
|
||||
Try to display the detail of a team participation.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
# Can't see the participation if it is not valid
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertRedirects(response,
|
||||
reverse("participation:participation_detail", args=(self.team.participation.pk,)),
|
||||
302, 403)
|
||||
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.save()
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertRedirects(response,
|
||||
reverse("participation:participation_detail", args=(self.team.participation.pk,)),
|
||||
302, 200)
|
||||
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Can't see other participations
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.client.force_login(self.second_user)
|
||||
response = self.client.get(reverse("participation:participation_detail", args=(self.team.participation.pk,)))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_forbidden_access(self):
|
||||
"""
|
||||
Load personal pages and ensure that these are protected.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
|
||||
resp = self.client.get(reverse("participation:team_detail", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:update_team", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:team_authorizations", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
resp = self.client.get(reverse("participation:participation_detail", args=(self.second_team.pk,)))
|
||||
self.assertEqual(resp.status_code, 403)
|
||||
|
||||
def test_cover_matrix(self):
|
||||
"""
|
||||
Load matrix scripts, to cover them and ensure that they can run.
|
||||
"""
|
||||
self.user.registration.team = self.team
|
||||
self.user.registration.save()
|
||||
self.second_user.registration.team = self.second_team
|
||||
self.second_user.registration.save()
|
||||
self.team.participation.valid = True
|
||||
self.team.participation.received_participation = self.second_team.participation
|
||||
self.team.participation.save()
|
||||
|
||||
call_command('fix_matrix_channels')
|
||||
|
||||
|
||||
class TestAdmin(TestCase):
|
||||
def setUp(self) -> None:
|
||||
self.user = User.objects.create_superuser(
|
||||
username="admin@example.com",
|
||||
email="admin@example.com",
|
||||
password="admin",
|
||||
)
|
||||
self.client.force_login(self.user)
|
||||
|
||||
self.team1 = Team.objects.create(
|
||||
name="Toto",
|
||||
trigram="TOT",
|
||||
)
|
||||
self.team1.participation.valid = True
|
||||
self.team1.participation.problem = 1
|
||||
self.team1.participation.save()
|
||||
|
||||
self.team2 = Team.objects.create(
|
||||
name="Bliblu",
|
||||
trigram="BIU",
|
||||
)
|
||||
self.team2.participation.valid = True
|
||||
self.team2.participation.problem = 1
|
||||
self.team2.participation.save()
|
||||
|
||||
self.team3 = Team.objects.create(
|
||||
name="Zouplop",
|
||||
trigram="ZPL",
|
||||
)
|
||||
self.team3.participation.valid = True
|
||||
self.team3.participation.problem = 1
|
||||
self.team3.participation.save()
|
||||
|
||||
self.other_team = Team.objects.create(
|
||||
name="I am different",
|
||||
trigram="IAD",
|
||||
)
|
||||
self.other_team.participation.valid = True
|
||||
self.other_team.participation.problem = 2
|
||||
self.other_team.participation.save()
|
||||
|
||||
def test_research(self):
|
||||
"""
|
||||
Try to search some things.
|
||||
"""
|
||||
call_command("rebuild_index", "--noinput", "--verbosity", 0)
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" + self.team1.name)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
response = self.client.get(reverse("haystack_search") + "?q=" + self.team2.trigram)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertTrue(response.context["object_list"])
|
||||
|
||||
def test_create_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't create a team.
|
||||
"""
|
||||
response = self.client.post(reverse("participation:create_team"), data=dict(
|
||||
name="Test team",
|
||||
trigram="TES",
|
||||
))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_join_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't join a team.
|
||||
"""
|
||||
team = Team.objects.create(name="Test", trigram="TES")
|
||||
|
||||
response = self.client.post(reverse("participation:join_team"), data=dict(
|
||||
access_code=team.access_code,
|
||||
))
|
||||
self.assertTrue(response.status_code, 403)
|
||||
|
||||
def test_leave_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't leave a team.
|
||||
"""
|
||||
response = self.client.get(reverse("participation:team_leave"))
|
||||
self.assertTrue(response.status_code, 403)
|
||||
|
||||
def test_my_team_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't access to "My team".
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_team_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_my_participation_forbidden(self):
|
||||
"""
|
||||
Ensure that an admin can't access to "My participation".
|
||||
"""
|
||||
response = self.client.get(reverse("participation:my_participation_detail"))
|
||||
self.assertEqual(response.status_code, 403)
|
42
apps/participation/urls.py
Normal file
42
apps/participation/urls.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Copyright (C) 2020 by Animath
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from django.urls import path
|
||||
from django.views.generic import TemplateView
|
||||
|
||||
from .views import CreateTeamView, JoinTeamView, MyParticipationDetailView, MyTeamDetailView, NoteUpdateView, \
|
||||
ParticipationDetailView, PassageCreateView, PassageDetailView, PassageUpdateView, PoolCreateView, PoolDetailView, \
|
||||
PoolUpdateTeamsView, PoolUpdateView, SolutionUploadView, SynthesisUploadView, TeamAuthorizationsView, \
|
||||
TeamDetailView, TeamLeaveView, TeamListView, TeamUpdateView, TournamentCreateView, TournamentDetailView, \
|
||||
TournamentListView, TournamentUpdateView
|
||||
|
||||
|
||||
app_name = "participation"
|
||||
|
||||
urlpatterns = [
|
||||
path("create_team/", CreateTeamView.as_view(), name="create_team"),
|
||||
path("join_team/", JoinTeamView.as_view(), name="join_team"),
|
||||
path("teams/", TeamListView.as_view(), name="team_list"),
|
||||
path("team/", MyTeamDetailView.as_view(), name="my_team_detail"),
|
||||
path("team/<int:pk>/", TeamDetailView.as_view(), name="team_detail"),
|
||||
path("team/<int:pk>/update/", TeamUpdateView.as_view(), name="update_team"),
|
||||
path("team/<int:pk>/authorizations/", TeamAuthorizationsView.as_view(), name="team_authorizations"),
|
||||
path("team/leave/", TeamLeaveView.as_view(), name="team_leave"),
|
||||
path("detail/", MyParticipationDetailView.as_view(), name="my_participation_detail"),
|
||||
path("detail/<int:pk>/", ParticipationDetailView.as_view(), name="participation_detail"),
|
||||
path("detail/<int:pk>/solution/", SolutionUploadView.as_view(), name="upload_solution"),
|
||||
path("tournament/", TournamentListView.as_view(), name="tournament_list"),
|
||||
path("tournament/create/", TournamentCreateView.as_view(), name="tournament_create"),
|
||||
path("tournament/<int:pk>/", TournamentDetailView.as_view(), name="tournament_detail"),
|
||||
path("tournament/<int:pk>/update/", TournamentUpdateView.as_view(), name="tournament_update"),
|
||||
path("pools/create/", PoolCreateView.as_view(), name="pool_create"),
|
||||
path("pools/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
|
||||
path("pools/<int:pk>/update/", PoolUpdateView.as_view(), name="pool_update"),
|
||||
path("pools/<int:pk>/update-teams/", PoolUpdateTeamsView.as_view(), name="pool_update_teams"),
|
||||
path("pools/passages/add/<int:pk>/", PassageCreateView.as_view(), name="passage_create"),
|
||||
path("pools/passages/<int:pk>/", PassageDetailView.as_view(), name="passage_detail"),
|
||||
path("pools/passages/<int:pk>/update/", PassageUpdateView.as_view(), name="passage_update"),
|
||||
path("pools/passages/<int:pk>/solution/", SynthesisUploadView.as_view(), name="upload_synthesis"),
|
||||
path("pools/passages/notes/<int:pk>/", NoteUpdateView.as_view(), name="update_notes"),
|
||||
path("chat/", TemplateView.as_view(template_name="participation/chat.html"), name="chat")
|
||||
]
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user