Merge branch 'django' into 'master'

Django

See merge request animath/si/plateforme!4
This commit is contained in:
Yohann D'ANELLO 2020-07-13 19:30:06 +00:00
commit 1ae6049974
126 changed files with 8675 additions and 31 deletions

925
.bashrc Normal file
View File

@ -0,0 +1,925 @@
# =============================================================== #
#
# 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
# 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:

4
.dockerignore Normal file
View File

@ -0,0 +1,4 @@
__pycache__
media
import_olddb
db.sqlite3

50
.gitignore vendored Normal file
View File

@ -0,0 +1,50 @@
# Server config files
nginx_note.conf
# Byte-compiled / optimized / DLL files
dist
build
__pycache__
*.py[cod]
*$py.class
*.swp
*.egg-info
_build
.tox
.coverage
coverage
# Translations
*.mo
*.pot
# Jupyter Notebook
.ipynb_checkpoints
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# PyCharm project settings
.idea
# VSCode project settings
.vscode
# Local data
secrets.py
*.log
media/
# Virtualenv
env/
venv/
db.sqlite3
# Ignore migrations during first phase dev
migrations/
# Don't git personal data
import_olddb/

View File

@ -1,40 +1,29 @@
FROM php:7.3-apache as plateforme-builder
FROM python:3-alpine
# Enabling apache rewrite mod
RUN a2enmod rewrite
ENV PYTHONUNBUFFERED 1
RUN apt clean && apt update && apt upgrade -y
# Install LaTeX requirements
RUN apk add --no-cache gettext texlive nginx gcc libc-dev libffi-dev postgresql-dev mariadb-connector-c-dev
# Install MySQL drivers
RUN docker-php-ext-install pdo_mysql \
&& docker-php-ext-enable pdo_mysql
RUN apk add --no-cache bash
# Install zip utilities
RUN apt install -y libzip-dev zip \
&& docker-php-ext-configure zip --with-libzip \
&& docker-php-ext-install zip \
&& docker-php-ext-enable zip
RUN mkdir /code
WORKDIR /code
COPY requirements.txt /code/requirements.txt
RUN pip install -r requirements.txt --no-cache-dir
# Install LaTeX utilities
RUN apt update && apt upgrade -y && apt install -yq texlive texlive-base texlive-binaries texlive-lang-french
COPY . /code/
# Setup locales
RUN apt install locales locales-all -y && locale-gen fr_FR.UTF-8
ENV LANG fr_FR.UTF-8
ENV LANGUAGE fr_FR:fr
ENV LC_ALL fr_FR.UTF-8
# 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
# Setup timezone
RUN echo Europe/Paris > /etc/timezone \
&& ln -sf /usr/share/zoneinfo/Europe/Paris /etc/localtime \
&& dpkg-reconfigure -f noninteractive tzdata
# With a bashrc, the shell is better
RUN ln -s /code/.bashrc /root/.bashrc
# Setup mailing
RUN apt install -yq msmtp ca-certificates
COPY setup/msmtprc /etc/msmtprc
RUN echo "sendmail_path=msmtp -t" >> /usr/local/etc/php/conf.d/php-sendmail.ini
ENTRYPOINT ["/code/entrypoint.sh"]
EXPOSE 80
# Setting environment
ENV TFJM_LOCAL_PATH /var/www/html
ENV TFJM_MAIL_DOMAIN tfjm.org
ENV TFJM_URL_BASE https://inscription.tfjm.org
CMD ["./manage.py", "shell_plus", "--ptpython"]

65
README.md Normal file
View File

@ -0,0 +1,65 @@
# Plateforme d'inscription 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/).
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
offre également un accès pour les organisateurs et les jurys leur permettant de communiquer avec les équipes et de
récupérer les documents nécessaires.
Un wiki plus détaillé arrivera ultérieurement. L'interface organisateur et jury est vouée à être plus poussée.
L'instance de production est disponible à l'adresse [inscription.tfjm.org](https://inscription.tfjm.org).
## Installation
Le plus simple pour installer la plateforme est d'utiliser l'image Docker incluse, qui fait tourner un serveur Nginx
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
links:
- postgres
ports:
- "80:80"
env_file:
- ./inscription-tfjm.env
volumes:
# - ./inscription-tfjm:/code
- ./inscription-tfjm/media:/code/media
```
Le volume `/code` n'est à ajouter uniquement en développement, et jamais en production.
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
DJANGO_DB_USER= # Utilisateur de la base de données
DJANGO_DB_PASSWORD= # Mot de passe pour accéder à la base de données
SMTP_HOST= # Hôte SMTP pour l'envoi de mails
SMTP_PORT=465 # Port du serveur SMTP
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
```
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.
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.

1
apps/api/__init__.py Normal file
View File

@ -0,0 +1 @@
default_app_config = 'api.apps.APIConfig'

10
apps/api/apps.py Normal file
View File

@ -0,0 +1,10 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class APIConfig(AppConfig):
"""
Manage the inscription through a JSON API.
"""
name = 'api'
verbose_name = _('API')

80
apps/api/serializers.py Normal file
View File

@ -0,0 +1,80 @@
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):
"""
Serialize a User object into JSON.
"""
class Meta:
model = TFJMUser
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__"

26
apps/api/urls.py Normal file
View File

@ -0,0 +1,26 @@
from django.conf.urls import url, include
from rest_framework import routers
from .viewsets import UserViewSet, TeamViewSet, TournamentViewSet, AuthorizationViewSet, MotivationLetterViewSet, \
SolutionViewSet, SynthesisViewSet, PoolViewSet
# 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)
app_name = 'api'
# Wire up our API using automatic URL routing.
# Additionally, we include login URLs for the browsable API.
urlpatterns = [
url('^', include(router.urls)),
url('^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
]

124
apps/api/viewsets.py Normal file
View File

@ -0,0 +1,124 @@
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
from .serializers import UserSerializer, TeamSerializer, TournamentSerializer, AuthorizationSerializer, \
MotivationLetterSerializer, SolutionSerializer, SynthesisSerializer, PoolSerializer
class UserViewSet(ModelViewSet):
"""
Display list of users.
"""
queryset = TFJMUser.objects.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', ]
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)

1
apps/member/__init__.py Normal file
View File

@ -0,0 +1 @@
default_app_config = 'member.apps.MemberConfig'

56
apps/member/admin.py Normal file
View File

@ -0,0 +1,56 @@
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.
"""

10
apps/member/apps.py Normal file
View File

@ -0,0 +1,10 @@
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')

73
apps/member/forms.py Normal file
View File

@ -0,0 +1,73 @@
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',)

View File

View File

@ -0,0 +1,32 @@
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()

View File

@ -0,0 +1,75 @@
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!"))

View File

@ -0,0 +1,309 @@
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"))

View File

368
apps/member/models.py Normal file
View File

@ -0,0 +1,368 @@
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")

26
apps/member/tables.py Normal file
View File

@ -0,0 +1,26 @@
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'
}

View File

View File

@ -0,0 +1,25 @@
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)

19
apps/member/urls.py Normal file
View File

@ -0,0 +1,19 @@
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"),
]

292
apps/member/views.py Normal file
View File

@ -0,0 +1,292 @@
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"])

View File

@ -0,0 +1 @@
default_app_config = 'tournament.apps.TournamentConfig'

31
apps/tournament/admin.py Normal file
View File

@ -0,0 +1,31 @@
from django.contrib.auth.admin import admin
from .models import Team, Tournament, Pool, Payment
@admin.register(Team)
class TeamAdmin(admin.ModelAdmin):
"""
Django admin page for teams.
"""
@admin.register(Tournament)
class TournamentAdmin(admin.ModelAdmin):
"""
Django admin page for tournaments.
"""
@admin.register(Pool)
class PoolAdmin(admin.ModelAdmin):
"""
Django admin page for pools.
"""
@admin.register(Payment)
class PaymentAdmin(admin.ModelAdmin):
"""
Django admin page for payments.
"""

10
apps/tournament/apps.py Normal file
View File

@ -0,0 +1,10 @@
from django.apps import AppConfig
from django.utils.translation import gettext_lazy as _
class TournamentConfig(AppConfig):
"""
The tournament app handles all that is related to the tournaments.
"""
name = 'tournament'
verbose_name = _('tournament')

262
apps/tournament/forms.py Normal file
View File

@ -0,0 +1,262 @@
import os
import re
from django import forms
from django.db.models import Q
from django.template.defaultfilters import filesizeformat
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from member.models import TFJMUser, Solution, Synthesis
from tfjm.inputs import DatePickerInput, DateTimePickerInput, AmountInput
from tournament.models import Tournament, Team, Pool
class TournamentForm(forms.ModelForm):
"""
Create and update tournaments.
"""
# Only organizers can organize tournaments. Well, that's pretty normal...
organizers = forms.ModelMultipleChoiceField(
TFJMUser.objects.filter(Q(role="0admin") | Q(role="1volunteer")).order_by('role'),
label=_("Organizers"),
)
def clean(self):
cleaned_data = super().clean()
if not self.instance.pk:
if Tournament.objects.filter(name=cleaned_data["data"], year=os.getenv("TFJM_YEAR")):
self.add_error("name", _("This tournament already exists."))
if cleaned_data["final"] and Tournament.objects.filter(final=True, year=os.getenv("TFJM_YEAR")):
self.add_error("name", _("The final tournament was already defined."))
return cleaned_data
class Meta:
model = Tournament
exclude = ('year',)
widgets = {
"price": AmountInput(),
"date_start": DatePickerInput(),
"date_end": DatePickerInput(),
"date_inscription": DateTimePickerInput(),
"date_solutions": DateTimePickerInput(),
"date_syntheses": DateTimePickerInput(),
"date_solutions_2": DateTimePickerInput(),
"date_syntheses_2": DateTimePickerInput(),
}
class OrganizerForm(forms.ModelForm):
"""
Register an organizer in the website.
"""
class Meta:
model = TFJMUser
fields = ('last_name', 'first_name', 'email', 'is_superuser',)
def clean(self):
cleaned_data = super().clean()
if TFJMUser.objects.filter(email=cleaned_data["email"], year=os.getenv("TFJM_YEAR")).exists():
self.add_error("email", _("This organizer already exist."))
return cleaned_data
def save(self, commit=True):
user = self.instance
user.role = '0admin' if user.is_superuser else '1volunteer'
user.save()
super().save(commit)
class TeamForm(forms.ModelForm):
"""
Add and update a team.
"""
tournament = forms.ModelChoiceField(
Tournament.objects.filter(date_inscription__gte=timezone.now(), final=False),
)
class Meta:
model = Team
fields = ('name', 'trigram', 'tournament',)
def clean(self):
cleaned_data = super().clean()
cleaned_data["trigram"] = cleaned_data["trigram"].upper()
if not re.match("[A-Z]{3}", cleaned_data["trigram"]):
self.add_error("trigram", _("The trigram must be composed of three upcase letters."))
if not self.instance.pk:
if Team.objects.filter(trigram=cleaned_data["trigram"], year=os.getenv("TFJM_YEAR")).exists():
self.add_error("trigram", _("This trigram is already used."))
if Team.objects.filter(name=cleaned_data["name"], year=os.getenv("TFJM_YEAR")).exists():
self.add_error("name", _("This name is already used."))
if cleaned_data["tournament"].date_inscription < timezone.now:
self.add_error("tournament", _("This tournament is already closed."))
return cleaned_data
class JoinTeam(forms.Form):
"""
Form to join a team with an access code.
"""
access_code = forms.CharField(
label=_("Access code"),
max_length=6,
)
def clean(self):
cleaned_data = super().clean()
if not re.match("[a-z0-9]{6}", cleaned_data["access_code"]):
self.add_error('access_code', _("The access code must be composed of 6 alphanumeric characters."))
team = Team.objects.filter(access_code=cleaned_data["access_code"])
if not team.exists():
self.add_error('access_code', _("This access code is invalid."))
team = team.get()
if not team.invalid:
self.add_error('access_code', _("The team is already validated."))
cleaned_data["team"] = team
return cleaned_data
class SolutionForm(forms.ModelForm):
"""
Form to upload a solution.
"""
problem = forms.ChoiceField(
label=_("Problem"),
choices=[(str(i), _("Problem #%(problem)d") % {"problem": i}) for i in range(1, 9)],
)
def clean_file(self):
content = self.cleaned_data['file']
content_type = content.content_type
if content_type in ["application/pdf"]:
if content.size > 5 * 2 ** 20:
raise forms.ValidationError(
_('Please keep filesize under %(max_size)s. Current filesize %(current_size)s') % {
"max_size": filesizeformat(2 * 2 ** 20),
"current_size": filesizeformat(content.size)
})
else:
raise forms.ValidationError(_('The file should be a PDF file.'))
return content
class Meta:
model = Solution
fields = ('file', 'problem',)
class SynthesisForm(forms.ModelForm):
"""
Form to upload a synthesis.
"""
def clean_file(self):
content = self.cleaned_data['file']
content_type = content.content_type
if content_type in ["application/pdf"]:
if content.size > 5 * 2 ** 20:
raise forms.ValidationError(
_('Please keep filesize under %(max_size)s. Current filesize %(current_size)s') % {
"max_size": filesizeformat(2 * 2 ** 20),
"current_size": filesizeformat(content.size)
})
else:
raise forms.ValidationError(_('The file should be a PDF file.'))
return content
class Meta:
model = Synthesis
fields = ('file', 'source', 'round',)
class PoolForm(forms.ModelForm):
"""
Form to add a pool.
Should not be used: prefer to pass by API and auto-add pools with the results of the draw.
"""
team1 = forms.ModelChoiceField(
Team.objects.filter(validation_status="2valid").all(),
empty_label=_("Choose a team..."),
label=_("Team 1"),
)
problem1 = forms.IntegerField(
min_value=1,
max_value=8,
initial=1,
label=_("Problem defended by team 1"),
)
team2 = forms.ModelChoiceField(
Team.objects.filter(validation_status="2valid").all(),
empty_label=_("Choose a team..."),
label=_("Team 2"),
)
problem2 = forms.IntegerField(
min_value=1,
max_value=8,
initial=2,
label=_("Problem defended by team 2"),
)
team3 = forms.ModelChoiceField(
Team.objects.filter(validation_status="2valid").all(),
empty_label=_("Choose a team..."),
label=_("Team 3"),
)
problem3 = forms.IntegerField(
min_value=1,
max_value=8,
initial=3,
label=_("Problem defended by team 3"),
)
def clean(self):
cleaned_data = super().clean()
team1, pb1 = cleaned_data["team1"], cleaned_data["problem1"]
team2, pb2 = cleaned_data["team2"], cleaned_data["problem2"]
team3, pb3 = cleaned_data["team3"], cleaned_data["problem3"]
sol1 = Solution.objects.get(team=team1, problem=pb1, final=team1.selected_for_final)
sol2 = Solution.objects.get(team=team2, problem=pb2, final=team2.selected_for_final)
sol3 = Solution.objects.get(team=team3, problem=pb3, final=team3.selected_for_final)
cleaned_data["teams"] = [team1, team2, team3]
cleaned_data["solutions"] = [sol1, sol2, sol3]
return cleaned_data
def save(self, commit=True):
pool = super().save(commit)
pool.refresh_from_db()
pool.teams.set(self.cleaned_data["teams"])
pool.solutions.set(self.cleaned_data["solutions"])
pool.save()
return pool
class Meta:
model = Pool
fields = ('round', 'juries',)

View File

432
apps/tournament/models.py Normal file
View File

@ -0,0 +1,432 @@
import os
import random
from django.core.mail import send_mail
from django.db import models
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
class Tournament(models.Model):
"""
Store the information of a tournament.
"""
name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
organizers = models.ManyToManyField(
'member.TFJMUser',
related_name="organized_tournaments",
verbose_name=_("organizers"),
help_text=_("List of all organizers that can see and manipulate data of the tournament and the teams."),
)
size = models.PositiveSmallIntegerField(
verbose_name=_("size"),
help_text=_("Number of teams that are allowed to join the tournament."),
)
place = models.CharField(
max_length=255,
verbose_name=_("place"),
)
price = models.PositiveSmallIntegerField(
verbose_name=_("price"),
help_text=_("Price asked to participants. Free with a scholarship."),
)
description = models.TextField(
verbose_name=_("description"),
)
date_start = models.DateField(
default=timezone.now,
verbose_name=_("date start"),
)
date_end = models.DateField(
default=timezone.now,
verbose_name=_("date end"),
)
date_inscription = models.DateTimeField(
default=timezone.now,
verbose_name=_("date of registration closing"),
)
date_solutions = models.DateTimeField(
default=timezone.now,
verbose_name=_("date of maximal solution submission"),
)
date_syntheses = models.DateTimeField(
default=timezone.now,
verbose_name=_("date of maximal syntheses submission for the first round"),
)
date_solutions_2 = models.DateTimeField(
default=timezone.now,
verbose_name=_("date when solutions of round 2 are available"),
)
date_syntheses_2 = models.DateTimeField(
default=timezone.now,
verbose_name=_("date of maximal syntheses submission for the second round"),
)
final = models.BooleanField(
verbose_name=_("final tournament"),
help_text=_("It should be only one final tournament."),
)
year = models.PositiveIntegerField(
default=os.getenv("TFJM_YEAR", timezone.now().year),
verbose_name=_("year"),
)
@property
def teams(self):
"""
Get all teams that are registered to this tournament, with a distinction for the final tournament.
"""
return self._teams if not self.final else Team.objects.filter(selected_for_final=True)
@property
def linked_organizers(self):
"""
Display a list of the organizers with links to their personal page.
"""
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.organizers.all()]
@property
def solutions(self):
"""
Get all sent solutions for this tournament.
"""
from member.models import Solution
return Solution.objects.filter(final=self.final) if self.final \
else Solution.objects.filter(team__tournament=self, final=False)
@property
def syntheses(self):
"""
Get all sent syntheses for this tournament.
"""
from member.models import Synthesis
return Synthesis.objects.filter(final=self.final) if self.final \
else Synthesis.objects.filter(team__tournament=self, final=False)
@classmethod
def get_final(cls):
"""
Get the final tournament.
This should exist and be unique.
"""
return cls.objects.get(year=os.getenv("TFJM_YEAR"), final=True)
class Meta:
verbose_name = _("tournament")
verbose_name_plural = _("tournaments")
def send_mail_to_organizers(self, template_name, subject="Contact TFJM²", **kwargs):
"""
Send a mail to all organizers of the tournament.
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
version and in templates/mail_templates/<template_name>.txt for the plain text version.
The context of the template contains the tournament and the user. Extra context can be given through the kwargs.
"""
context = kwargs
context["tournament"] = self
for user in self.organizers.all():
context["user"] = user
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
from member.models import TFJMUser
for user in TFJMUser.objects.get(is_superuser=True).all():
context["user"] = user
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
def __str__(self):
return self.name
class Team(models.Model):
"""
Store information about a registered team.
"""
name = models.CharField(
max_length=255,
verbose_name=_("name"),
)
trigram = models.CharField(
max_length=3,
verbose_name=_("trigram"),
help_text=_("The trigram should be composed of 3 capitalize letters, that is a funny acronym for the team."),
)
tournament = models.ForeignKey(
Tournament,
on_delete=models.PROTECT,
related_name="_teams",
verbose_name=_("tournament"),
help_text=_("The tournament where the team is registered."),
)
inscription_date = models.DateTimeField(
auto_now_add=True,
verbose_name=_("inscription date"),
)
validation_status = models.CharField(
max_length=8,
choices=[
("0invalid", _("Registration not validated")),
("1waiting", _("Waiting for validation")),
("2valid", _("Registration validated")),
],
verbose_name=_("validation status"),
)
selected_for_final = models.BooleanField(
default=False,
verbose_name=_("selected for final"),
)
access_code = models.CharField(
max_length=6,
unique=True,
verbose_name=_("access code"),
)
year = models.PositiveIntegerField(
default=os.getenv("TFJM_YEAR", timezone.now().year),
verbose_name=_("year"),
)
@property
def valid(self):
return self.validation_status == "2valid"
@property
def waiting(self):
return self.validation_status == "1waiting"
@property
def invalid(self):
return self.validation_status == "0invalid"
@property
def coaches(self):
"""
Get all coaches of a team.
"""
return self.users.all().filter(role="2coach")
@property
def linked_coaches(self):
"""
Get a list of the coaches of a team with html links to their pages.
"""
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.coaches]
@property
def participants(self):
"""
Get all particpants of a team, coaches excluded.
"""
return self.users.all().filter(role="3participant")
@property
def linked_participants(self):
"""
Get a list of the participants of a team with html links to their pages.
"""
return ['<a href="{url}">'.format(url=reverse_lazy("member:information", args=(user.pk,))) + str(user) + '</a>'
for user in self.participants]
@property
def future_tournament(self):
"""
Get the last tournament where the team is registered.
Only matters if the team is selected for final: if this is the case, we return the final tournament.
Useful for deadlines.
"""
return Tournament.get_final() if self.selected_for_final else self.tournament
@property
def can_validate(self):
"""
Check if a given team is able to ask for validation.
A team can validate if:
* All participants filled the photo consent
* Minor participants filled the parental consent
* Minor participants filled the sanitary plug
* Teams sent their motivation letter
* The team contains at least 4 participants
* The team contains at least 1 coach
"""
# TODO In a normal time, team needs a motivation letter and authorizations.
return self.coaches.exists() and self.participants.count() >= 4\
and self.tournament.date_inscription <= timezone.now()
class Meta:
verbose_name = _("team")
verbose_name_plural = _("teams")
unique_together = (('name', 'year',), ('trigram', 'year',),)
def send_mail(self, template_name, subject="Contact TFJM²", **kwargs):
"""
Send a mail to all members of a team with a given template.
The template of the mail should be found either in templates/mail_templates/<template_name>.html for the HTML
version and in templates/mail_templates/<template_name>.txt for the plain text version.
The context of the template contains the team and the user. Extra context can be given through the kwargs.
"""
context = kwargs
context["team"] = self
for user in self.users.all():
context["user"] = user
message = render_to_string("mail_templates/" + template_name + ".txt", context=context)
message_html = render_to_string("mail_templates/" + template_name + ".html", context=context)
send_mail(subject, message, "contact@tfjm.org", [user.email], html_message=message_html)
def __str__(self):
return self.trigram + "" + self.name
class Pool(models.Model):
"""
Store information of a pool.
A pool is only a list of accessible solutions to some teams and some juries.
TODO: check that the set of teams is equal to the set of the teams that have a solution in this set.
TODO: Moreover, a team should send only one solution.
"""
teams = models.ManyToManyField(
Team,
related_name="pools",
verbose_name=_("teams"),
)
solutions = models.ManyToManyField(
"member.Solution",
related_name="pools",
verbose_name=_("solutions"),
)
round = models.PositiveIntegerField(
choices=[
(1, _("Round 1")),
(2, _("Round 2")),
],
verbose_name=_("round"),
)
juries = models.ManyToManyField(
"member.TFJMUser",
related_name="pools",
verbose_name=_("juries"),
)
extra_access_token = models.CharField(
max_length=64,
default="",
verbose_name=_("extra access token"),
help_text=_("Let other users access to the pool data without logging in."),
)
@property
def problems(self):
"""
Get problem numbers of the sent solutions as a list of integers.
"""
return list(d["problem"] for d in self.solutions.values("problem").all())
@property
def tournament(self):
"""
Get the concerned tournament.
We assume that the pool is correct, so all solutions belong to the same tournament.
"""
return self.solutions.first().tournament
@property
def syntheses(self):
"""
Get the syntheses of the teams that are in this pool, for the correct round.
"""
from member.models import Synthesis
return Synthesis.objects.filter(team__in=self.teams.all(), round=self.round, final=self.tournament.final)
def save(self, **kwargs):
if not self.extra_access_token:
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
code = "".join(random.choice(alphabet) for _ in range(64))
self.extra_access_token = code
super().save(**kwargs)
class Meta:
verbose_name = _("pool")
verbose_name_plural = _("pools")
class Payment(models.Model):
"""
Store some information about payments, to recover data.
TODO: handle it...
"""
user = models.OneToOneField(
'member.TFJMUser',
on_delete=models.CASCADE,
related_name="payment",
verbose_name=_("user"),
)
team = models.ForeignKey(
Team,
on_delete=models.CASCADE,
related_name="payments",
verbose_name=_("team"),
)
method = models.CharField(
max_length=16,
choices=[
("not_paid", _("Not paid")),
("credit_card", _("Credit card")),
("check", _("Bank check")),
("transfer", _("Bank transfer")),
("cash", _("Cash")),
("scholarship", _("Scholarship")),
],
default="not_paid",
verbose_name=_("payment method"),
)
validation_status = models.CharField(
max_length=8,
choices=[
("0invalid", _("Registration not validated")),
("1waiting", _("Waiting for validation")),
("2valid", _("Registration validated")),
],
verbose_name=_("validation status"),
)
class Meta:
verbose_name = _("payment")
verbose_name_plural = _("payments")
def __str__(self):
return _("Payment of {user}").format(str(self.user))

164
apps/tournament/tables.py Normal file
View File

@ -0,0 +1,164 @@
import django_tables2 as tables
from django.urls import reverse_lazy
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from django_tables2 import A
from member.models import Solution, Synthesis
from .models import Tournament, Team, Pool
class TournamentTable(tables.Table):
"""
List all tournaments.
"""
name = tables.LinkColumn(
"tournament:detail",
args=[A("pk")],
)
date_start = tables.Column(
verbose_name=_("dates").capitalize(),
)
def render_date_start(self, record):
return _("From {start:%b %d %Y} to {end:%b %d %Y}").format(start=record.date_start, end=record.date_end)
class Meta:
model = Tournament
fields = ("name", "date_start", "date_inscription", "date_solutions", "size", )
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('date_start', 'name',)
class TeamTable(tables.Table):
"""
Table of some teams. Can be filtered with a queryset (for example, teams of a tournament)
"""
name = tables.LinkColumn(
"tournament:team_detail",
args=[A("pk")],
)
class Meta:
model = Team
fields = ("name", "trigram", "validation_status", )
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
order_by = ('-validation_status', 'trigram',)
class SolutionTable(tables.Table):
"""
Display a table of some solutions.
"""
team = tables.LinkColumn(
"tournament:team_detail",
args=[A("team.pk")],
)
tournament = tables.LinkColumn(
"tournament:detail",
args=[A("tournament.pk")],
accessor=A("tournament"),
order_by=("team__tournament__date_start", "team__tournament__name",),
verbose_name=_("Tournament"),
)
file = tables.LinkColumn(
"document",
args=[A("file")],
attrs={
"a": {
"data-turbolinks": "false",
}
}
)
def render_file(self):
return _("Download")
class Meta:
model = Solution
fields = ("team", "tournament", "problem", "uploaded_at", "file", )
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
class SynthesisTable(tables.Table):
"""
Display a table of some syntheses.
"""
team = tables.LinkColumn(
"tournament:team_detail",
args=[A("team.pk")],
)
tournament = tables.LinkColumn(
"tournament:detail",
args=[A("tournament.pk")],
accessor=A("tournament"),
order_by=("team__tournament__date_start", "team__tournament__name",),
verbose_name=_("tournament"),
)
file = tables.LinkColumn(
"document",
args=[A("file")],
attrs={
"a": {
"data-turbolinks": "false",
}
}
)
def render_file(self):
return _("Download")
class Meta:
model = Synthesis
fields = ("team", "tournament", "round", "source", "uploaded_at", "file", )
attrs = {
'class': 'table table-condensed table-striped table-hover'
}
class PoolTable(tables.Table):
"""
Display a table of some pools.
"""
problems = tables.Column(
verbose_name=_("Problems"),
orderable=False,
)
tournament = tables.LinkColumn(
"tournament:detail",
args=[A("tournament.pk")],
verbose_name=_("Tournament"),
order_by=("teams__tournament__date_start", "teams__tournament__name",),
)
def render_teams(self, record, value):
return format_html('<a href="{url}">{trigrams}</a>',
url=reverse_lazy('tournament:pool_detail', args=(record.pk,)),
trigrams=", ".join(team.trigram for team in value.all()))
def render_problems(self, value):
return ", ".join([str(pb) for pb in value])
class Meta:
model = Pool
fields = ("teams", "tournament", "problems", "round", )
attrs = {
'class': 'table table-condensed table-striped table-hover'
}

24
apps/tournament/urls.py Normal file
View File

@ -0,0 +1,24 @@
from django.urls import path
from .views import TournamentListView, TournamentCreateView, TournamentDetailView, TournamentUpdateView, \
TeamDetailView, TeamUpdateView, AddOrganizerView, SolutionsView, SolutionsOrgaListView, SynthesesView, \
SynthesesOrgaListView, PoolListView, PoolCreateView, PoolDetailView
app_name = "tournament"
urlpatterns = [
path('list/', TournamentListView.as_view(), name="list"),
path("add/", TournamentCreateView.as_view(), name="add"),
path('<int:pk>/', TournamentDetailView.as_view(), name="detail"),
path('<int:pk>/update/', TournamentUpdateView.as_view(), name="update"),
path('team/<int:pk>/', TeamDetailView.as_view(), name="team_detail"),
path('team/<int:pk>/update/', TeamUpdateView.as_view(), name="team_update"),
path("add-organizer/", AddOrganizerView.as_view(), name="add_organizer"),
path("solutions/", SolutionsView.as_view(), name="solutions"),
path("all-solutions/", SolutionsOrgaListView.as_view(), name="all_solutions"),
path("syntheses/", SynthesesView.as_view(), name="syntheses"),
path("all_syntheses/", SynthesesOrgaListView.as_view(), name="all_syntheses"),
path("pools/", PoolListView.as_view(), name="pools"),
path("pool/add/", PoolCreateView.as_view(), name="create_pool"),
path("pool/<int:pk>/", PoolDetailView.as_view(), name="pool_detail"),
]

662
apps/tournament/views.py Normal file
View File

@ -0,0 +1,662 @@
import random
import zipfile
from datetime import timedelta
from io import BytesIO
from django.contrib.auth.mixins import LoginRequiredMixin, AccessMixin
from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail
from django.db.models import Q
from django.http import HttpResponse
from django.shortcuts import redirect
from django.template.loader import render_to_string
from django.urls import reverse_lazy
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from django.views.generic import DetailView, CreateView, UpdateView
from django.views.generic.edit import BaseFormView
from django_tables2.views import SingleTableView
from member.models import TFJMUser, Solution, Synthesis
from .forms import TournamentForm, OrganizerForm, SolutionForm, SynthesisForm, TeamForm, PoolForm
from .models import Tournament, Team, Pool
from .tables import TournamentTable, TeamTable, SolutionTable, SynthesisTable, PoolTable
class AdminMixin(LoginRequiredMixin):
"""
If a view extends this mixin, then the view will be only accessible to administrators.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.admin:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
class OrgaMixin(AccessMixin):
"""
If a view extends this mixin, then the view will be only accessible to administrators or organizers.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated and not request.session["extra_access_token"]:
return self.handle_no_permission()
elif request.user.is_authenticated and not request.user.organizes:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
class TeamMixin(LoginRequiredMixin):
"""
If a view extends this mixin, then the view will be only accessible to users that are registered in a team.
"""
def dispatch(self, request, *args, **kwargs):
if not request.user.is_authenticated or not request.user.team:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
class TournamentListView(SingleTableView):
"""
Display the list of all tournaments, ordered by start date then name.
"""
model = Tournament
table_class = TournamentTable
extra_context = dict(title=_("Tournaments list"),)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
team_users = TFJMUser.objects.filter(Q(team__isnull=False) | Q(role="admin") | Q(role="organizer"))\
.order_by('-role')
valid_team_users = team_users.filter(
Q(team__validation_status="2valid") | Q(role="admin") | Q(role="organizer"))
context["team_users_emails"] = [user.email for user in team_users]
context["valid_team_users_emails"] = [user.email for user in valid_team_users]
return context
class TournamentCreateView(AdminMixin, CreateView):
"""
Create a tournament. Only accessible to admins.
"""
model = Tournament
form_class = TournamentForm
extra_context = dict(title=_("Add tournament"),)
def get_success_url(self):
return reverse_lazy('tournament:detail', args=(self.object.pk,))
class TournamentDetailView(DetailView):
"""
Display the detail of a tournament.
Accessible to all, including not authenticated users.
"""
model = Tournament
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Tournament of {name}").format(name=self.object.name)
if self.object.final:
team_users = TFJMUser.objects.filter(team__selected_for_final=True)
valid_team_users = team_users
else:
team_users = TFJMUser.objects.filter(
Q(team__tournament=self.object)
| Q(organized_tournaments=self.object)).order_by('role')
valid_team_users = team_users.filter(
Q(team__validation_status="2valid")
| Q(role="admin")
| Q(organized_tournaments=self.object))
context["team_users_emails"] = [user.email for user in team_users]
context["valid_team_users_emails"] = [user.email for user in valid_team_users]
context["teams"] = TeamTable(self.object.teams.all())
return context
class TournamentUpdateView(OrgaMixin, UpdateView):
"""
Update the data of a tournament.
Reserved to admins and organizers of the tournament.
"""
def dispatch(self, request, *args, **kwargs):
"""
Restrict the view to organizers of tournaments, then process the request.
"""
if self.request.user.role == "1volunteer" and self.request.user not in self.get_object().organizers.all():
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
model = Tournament
form_class = TournamentForm
extra_context = dict(title=_("Update tournament"),)
def get_success_url(self):
return reverse_lazy('tournament:detail', args=(self.object.pk,))
class TeamDetailView(LoginRequiredMixin, DetailView):
"""
View the detail of a team.
Restricted to this team, admins and organizers of its tournament.
"""
model = Team
def dispatch(self, request, *args, **kwargs):
"""
Protect the page and process the request.
"""
if not request.user.is_authenticated or \
(not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all()
and not (self.get_object().selected_for_final
and request.user in Tournament.get_final().organizers.all())
and self.get_object() != request.user.team):
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Process POST requests. Supported requests:
- get the solutions of the team as a ZIP archive
- a user leaves its team (if the composition is not validated yet)
- the team requests the validation
- Organizers can validate or invalidate the request
- Admins can delete teams
- Admins can select teams for the final tournament
"""
team = self.get_object()
if "zip" in request.POST:
solutions = team.solutions.all()
out = BytesIO()
zf = zipfile.ZipFile(out, "w")
for solution in solutions:
zf.write(solution.file.path, str(solution) + ".pdf")
zf.close()
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
resp['Content-Disposition'] = 'attachment; filename={}'\
.format(_("Solutions for team {team}.zip")
.format(team=str(team)).replace(" ", "%20"))
return resp
elif "leave" in request.POST and request.user.participates:
request.user.team = None
request.user.save()
if not team.users.exists():
team.delete()
return redirect('tournament:detail', pk=team.tournament.pk)
elif "request_validation" in request.POST and request.user.participates and team.can_validate:
team.validation_status = "1waiting"
team.save()
team.tournament.send_mail_to_organizers("request_validation", "Demande de validation TFJM²", team=team)
return redirect('tournament:team_detail', pk=team.pk)
elif "validate" in request.POST and request.user.organizes:
team.validation_status = "2valid"
team.save()
team.send_mail("validate_team", "Équipe validée TFJM²")
return redirect('tournament:team_detail', pk=team.pk)
elif "invalidate" in request.POST and request.user.organizes:
team.validation_status = "0invalid"
team.save()
team.send_mail("unvalidate_team", "Équipe non validée TFJM²")
return redirect('tournament:team_detail', pk=team.pk)
elif "delete" in request.POST and request.user.organizes:
team.delete()
return redirect('tournament:detail', pk=team.tournament.pk)
elif "select_final" in request.POST and request.user.admin and not team.selected_for_final and team.pools:
# We copy all solutions for solutions for the final
for solution in team.solutions.all():
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
id = ""
for i in range(64):
id += random.choice(alphabet)
with solution.file.open("rb") as source:
with open("/code/media/" + id, "wb") as dest:
for chunk in source.chunks():
dest.write(chunk)
new_sol = Solution(
file=id,
team=team,
problem=solution.problem,
final=True,
)
new_sol.save()
team.selected_for_final = True
team.save()
team.send_mail("select_for_final", "Sélection pour la finale, félicitations ! - TFJM²",
final=Tournament.get_final())
return redirect('tournament:team_detail', pk=team.pk)
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["title"] = _("Information about team")
context["ordered_solutions"] = self.object.solutions.order_by('final', 'problem',).all()
context["team_users_emails"] = [user.email for user in self.object.users.all()]
return context
class TeamUpdateView(LoginRequiredMixin, UpdateView):
"""
Update the information about a team.
Team members, admins and organizers are allowed to do this.
"""
model = Team
form_class = TeamForm
extra_context = dict(title=_("Update team"),)
def dispatch(self, request, *args, **kwargs):
if not request.user.admin and self.request.user not in self.get_object().tournament.organizers.all() \
and self.get_object() != self.request.user.team:
raise PermissionDenied
return super().dispatch(request, *args, **kwargs)
class AddOrganizerView(AdminMixin, CreateView):
"""
Add a new organizer account. No password is created, the user should reset its password using the link
sent by mail. Only name and email are requested.
Only admins are granted to do this.
"""
model = TFJMUser
form_class = OrganizerForm
extra_context = dict(title=_("Add organizer"),)
template_name = "tournament/add_organizer.html"
def form_valid(self, form):
user = form.instance
msg = render_to_string("mail_templates/add_organizer.txt", context=dict(user=user))
msg_html = render_to_string("mail_templates/add_organizer.html", context=dict(user=user))
send_mail('Organisateur du TFJM² 2020', msg, 'contact@tfjm.org', [user.email], html_message=msg_html)
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy('index')
class SolutionsView(TeamMixin, BaseFormView, SingleTableView):
"""
Upload and view solutions for a team.
"""
model = Solution
table_class = SolutionTable
form_class = SolutionForm
template_name = "tournament/solutions_list.html"
extra_context = dict(title=_("Solutions"))
def post(self, request, *args, **kwargs):
if "zip" in request.POST:
solutions = request.user.team.solutions
out = BytesIO()
zf = zipfile.ZipFile(out, "w")
for solution in solutions:
zf.write(solution.file.path, str(solution) + ".pdf")
zf.close()
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
resp['Content-Disposition'] = 'attachment; filename={}'\
.format(_("Solutions for team {team}.zip")
.format(team=str(request.user.team)).replace(" ", "%20"))
return resp
return super().post(request, *args, **kwargs)
def get_context_data(self, **kwargs):
self.object_list = self.get_queryset()
context = super().get_context_data(**kwargs)
context["now"] = timezone.now()
context["real_deadline"] = self.request.user.team.future_tournament.date_solutions + timedelta(minutes=30)
return context
def get_queryset(self):
qs = super().get_queryset().filter(team=self.request.user.team)
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
'problem',)
def form_valid(self, form):
solution = form.instance
solution.team = self.request.user.team
solution.final = solution.team.selected_for_final
if timezone.now() > solution.tournament.date_solutions + timedelta(minutes=30):
form.add_error('file', _("You can't publish your solution anymore. Deadline: {date:%m-%d-%Y %H:%M}.")
.format(date=timezone.localtime(solution.tournament.date_solutions)))
return super().form_invalid(form)
prev_sol = Solution.objects.filter(problem=solution.problem, team=solution.team, final=solution.final)
for sol in prev_sol.all():
sol.delete()
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
id = ""
for i in range(64):
id += random.choice(alphabet)
solution.file.name = id
solution.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("tournament:solutions")
class SolutionsOrgaListView(OrgaMixin, SingleTableView):
"""
View all solutions sent by teams for the organized tournaments. Juries can view solutions of their pools.
Organizers can download a ZIP archive for each organized tournament.
"""
model = Solution
table_class = SolutionTable
template_name = "tournament/solutions_orga_list.html"
extra_context = dict(title=_("All solutions"))
def post(self, request, *args, **kwargs):
if "tournament_zip" in request.POST:
tournament = Tournament.objects.get(pk=int(request.POST["tournament_zip"]))
solutions = tournament.solutions
if not request.user.admin and request.user not in tournament.organizers.all():
raise PermissionDenied
out = BytesIO()
zf = zipfile.ZipFile(out, "w")
for solution in solutions:
zf.write(solution.file.path, str(solution) + ".pdf")
zf.close()
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
resp['Content-Disposition'] = 'attachment; filename={}'\
.format(_("Solutions for tournament {tournament}.zip")
.format(tournament=str(tournament)).replace(" ", "%20"))
return resp
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
context["tournaments"] = \
Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments
return context
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_authenticated and not self.request.user.admin:
if self.request.user in Tournament.get_final().organizers.all():
qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(pools__juries=self.request.user)
| Q(final=True))
else:
qs = qs.filter(Q(team__tournament__organizers=self.request.user) | Q(pools__juries=self.request.user))
elif not self.request.user.is_authenticated:
qs = qs.filter(pools__extra_access_token=self.request.session["extra_access_token"])
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
'problem',).distinct()
class SynthesesView(TeamMixin, BaseFormView, SingleTableView):
"""
Upload and view syntheses for a team.
"""
model = Synthesis
table_class = SynthesisTable
form_class = SynthesisForm
template_name = "tournament/syntheses_list.html"
extra_context = dict(title=_("Syntheses"))
def post(self, request, *args, **kwargs):
if "zip" in request.POST:
syntheses = request.user.team.syntheses
out = BytesIO()
zf = zipfile.ZipFile(out, "w")
for synthesis in syntheses:
zf.write(synthesis.file.path, str(synthesis) + ".pdf")
zf.close()
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
resp['Content-Disposition'] = 'attachment; filename={}'\
.format(_("Syntheses for team {team}.zip")
.format(team=str(request.user.team)).replace(" ", "%20"))
return resp
return super().post(request, *args, **kwargs)
def get_queryset(self):
qs = super().get_queryset().filter(team=self.request.user.team)
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
'round', 'source',)
def get_context_data(self, **kwargs):
self.object_list = self.get_queryset()
context = super().get_context_data(**kwargs)
context["now"] = timezone.now()
context["real_deadline_1"] = self.request.user.team.future_tournament.date_syntheses + timedelta(minutes=30)
context["real_deadline_2"] = self.request.user.team.future_tournament.date_syntheses_2 + timedelta(minutes=30)
return context
def form_valid(self, form):
synthesis = form.instance
synthesis.team = self.request.user.team
synthesis.final = synthesis.team.selected_for_final
if synthesis.round == '1' and timezone.now() > (synthesis.tournament.date_syntheses + timedelta(minutes=30)):
form.add_error('file', _("You can't publish your synthesis anymore for the first round."
" Deadline: {date:%m-%d-%Y %H:%M}.")
.format(date=timezone.localtime(synthesis.tournament.date_syntheses)))
return super().form_invalid(form)
if synthesis.round == '2' and timezone.now() > synthesis.tournament.date_syntheses_2 + timedelta(minutes=30):
form.add_error('file', _("You can't publish your synthesis anymore for the second round."
" Deadline: {date:%m-%d-%Y %H:%M}.")
.format(date=timezone.localtime(synthesis.tournament.date_syntheses_2)))
return super().form_invalid(form)
prev_syn = Synthesis.objects.filter(team=synthesis.team, round=synthesis.round, source=synthesis.source,
final=synthesis.final)
for syn in prev_syn.all():
syn.delete()
alphabet = "0123456789abcdefghijklmnopqrstuvwxyz0123456789"
id = ""
for i in range(64):
id += random.choice(alphabet)
synthesis.file.name = id
synthesis.save()
return super().form_valid(form)
def get_success_url(self):
return reverse_lazy("tournament:syntheses")
class SynthesesOrgaListView(OrgaMixin, SingleTableView):
"""
View all syntheses sent by teams for the organized tournaments. Juries can view syntheses of their pools.
Organizers can download a ZIP archive for each organized tournament.
"""
model = Synthesis
table_class = SynthesisTable
template_name = "tournament/syntheses_orga_list.html"
extra_context = dict(title=_("All syntheses"))
def post(self, request, *args, **kwargs):
if "tournament_zip" in request.POST:
tournament = Tournament.objects.get(pk=request.POST["tournament_zip"])
syntheses = tournament.syntheses
if not request.user.admin and request.user not in tournament.organizers.all():
raise PermissionDenied
out = BytesIO()
zf = zipfile.ZipFile(out, "w")
for synthesis in syntheses:
zf.write(synthesis.file.path, str(synthesis) + ".pdf")
zf.close()
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
resp['Content-Disposition'] = 'attachment; filename={}'\
.format(_("Syntheses for tournament {tournament}.zip")
.format(tournament=str(tournament)).replace(" ", "%20"))
return resp
return self.get(request, *args, **kwargs)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
if self.request.user.is_authenticated:
context["tournaments"] = \
Tournament.objects if self.request.user.admin else self.request.user.organized_tournaments
return context
def get_queryset(self):
qs = super().get_queryset()
if self.request.user.is_authenticated and not self.request.user.admin:
if self.request.user in Tournament.get_final().organizers.all():
qs = qs.filter(Q(team__tournament__organizers=self.request.user)
| Q(team__pools__juries=self.request.user)
| Q(final=True))
else:
qs = qs.filter(Q(team__tournament__organizers=self.request.user)
| Q(team__pools__juries=self.request.user))
elif not self.request.user.is_authenticated:
pool = Pool.objects.filter(extra_access_token=self.request.session["extra_access_token"])
if pool.exists():
pool = pool.get()
qs = qs.filter(team__pools=pool, final=pool.tournament.final)
else:
qs = qs.none()
return qs.order_by('final', 'team__tournament__date_start', 'team__tournament__name', 'team__trigram',
'round', 'source',).distinct()
class PoolListView(SingleTableView):
"""
View the list of visible pools.
Admins see all, juries see their own pools, organizers see the pools of their tournaments.
"""
model = Pool
table_class = PoolTable
extra_context = dict(title=_("Pools"))
def get_queryset(self):
qs = super().get_queryset()
user = self.request.user
if user.is_authenticated:
if not user.admin and user.organizes:
qs = qs.filter(Q(juries=user) | Q(teams__tournament__organizers=user))
elif user.participates:
qs = qs.filter(teams=user.team)
else:
qs = qs.filter(extra_access_token=self.request.session["extra_access_token"])
qs = qs.distinct().order_by('id')
return qs
class PoolCreateView(AdminMixin, CreateView):
"""
Create a pool manually.
This page should not be used: prefer send automatically data from the drawing bot.
"""
model = Pool
form_class = PoolForm
extra_context = dict(title=_("Create pool"))
def get_success_url(self):
return reverse_lazy("tournament:pools")
class PoolDetailView(DetailView):
"""
See the detail of a pool.
Teams and juries can download here defended solutions of the pool.
If this is the second round, teams can't download solutions of the other teams before the date when they
should be available.
Juries see also syntheses. They see of course solutions immediately.
This is also true for organizers and admins.
All can be downloaded as a ZIP archive.
"""
model = Pool
extra_context = dict(title=_("Pool detail"))
def get_queryset(self):
qs = super().get_queryset()
user = self.request.user
if user.is_authenticated:
if not user.admin and user.organizes:
qs = qs.filter(Q(juries=user) | Q(teams__tournament__organizers=user))
elif user.participates:
qs = qs.filter(teams=user.team)
else:
qs = qs.filter(extra_access_token=self.request.session["extra_access_token"])
return qs.distinct()
def post(self, request, *args, **kwargs):
user = request.user
pool = self.get_object()
if "solutions_zip" in request.POST:
if user.is_authenticated and user.participates and pool.round == 2\
and pool.tournament.date_solutions_2 > timezone.now():
raise PermissionDenied
out = BytesIO()
zf = zipfile.ZipFile(out, "w")
for solution in pool.solutions.all():
zf.write(solution.file.path, str(solution) + ".pdf")
zf.close()
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
resp['Content-Disposition'] = 'attachment; filename={}' \
.format(_("Solutions of a pool for the round {round} of the tournament {tournament}.zip")
.format(round=pool.round, tournament=str(pool.tournament)).replace(" ", "%20"))
return resp
elif "syntheses_zip" in request.POST and (not user.is_authenticated or user.organizes):
out = BytesIO()
zf = zipfile.ZipFile(out, "w")
for synthesis in pool.syntheses.all():
zf.write(synthesis.file.path, str(synthesis) + ".pdf")
zf.close()
resp = HttpResponse(out.getvalue(), content_type="application/x-zip-compressed")
resp['Content-Disposition'] = 'attachment; filename={}' \
.format(_("Syntheses of a pool for the round {round} of the tournament {tournament}.zip")
.format(round=pool.round, tournament=str(pool.tournament)).replace(" ", "%20"))
return resp
return self.get(request, *args, **kwargs)

13
entrypoint.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/sh
python manage.py compilemessages
python manage.py makemigrations
python manage.py migrate
nginx
if [ "$TFJM_STAGE" = "prod" ]; then
gunicorn -b 0.0.0.0:8000 --workers=2 --threads=4 --worker-class=gthread tfjm.wsgi --access-logfile '-' --error-logfile '-';
else
./manage.py runserver 0.0.0.0:8000;
fi

File diff suppressed because it is too large Load Diff

21
manage.py Executable file
View File

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tfjm.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

19
nginx_tfjm.conf Normal file
View File

@ -0,0 +1,19 @@
upstream tfjm {
server 127.0.0.1:8000;
}
server {
listen 80;
server_name tfjm;
location / {
proxy_pass http://tfjm;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_redirect off;
}
location /static {
alias /code/static/;
}
}

14
requirements.txt Normal file
View File

@ -0,0 +1,14 @@
bcrypt
Django~=3.0
django-allauth
django-crispy-forms
django-extensions
django-filter
django-polymorphic
django-tables2
djangorestframework
django-rest-polymorphic
mysqlclient
psycopg2-binary
ptpython
gunicorn

View File

@ -0,0 +1,113 @@
\documentclass[a4paper,french,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[frenchb]{babel}
\usepackage{fancyhdr}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amssymb}
%\usepackage{anyfontsize}
\usepackage{fancybox}
\usepackage{eso-pic,graphicx}
\usepackage{xcolor}
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
% Page formating
\hoffset -1in
\voffset -1in
\textwidth 180 mm
\textheight 250 mm
\oddsidemargin 15mm
\evensidemargin 15mm
\pagestyle{fancy}
% Headers and footers
\fancyfoot{}
\lhead{}
\rhead{}
\renewcommand{\headrulewidth}{0pt}
\lfoot{\footnotesize 11 rue Pierre et Marie Curie, 75231 Paris Cedex 05\\ Numéro siret 431 598 366 00018}
\rfoot{\footnotesize Association agréée par\\le Ministère de l'éducation nationale.}
\begin{document}
\includegraphics[height=2cm]{assets/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
\vfill
\begin{center}
\LARGE
Autorisation d'enregistrement et de diffusion de l'image ({TOURNAMENT_NAME})
\end{center}
\normalsize
\thispagestyle{empty}
\bigskip
Je soussign\'e {PARTICIPANT_NAME}\\
demeurant au {ADDRESS}
\medskip
Cochez la/les cases correspondantes.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ du {START_DATE} au {END_DATE} {YEAR} à : {PLACE}, \`a me photographier ou \`a me filmer et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion sur son site et sur les sites partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit dutiliser mon image sur tous ses supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente, cessionnaire des droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
\medskip
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la publication et la diffusion de l'image ainsi que des commentaires l'accompagnant ne portent pas atteinte \`a la vie priv\'ee, \`a la dignit\'e et \`a la r\'eputation de la personne photographiée.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise la diffusion dans les medias (Presse, T\'el\'evision, Internet) de photographies prises \`a l'occasion dune \'eventuelle m\'ediatisation de cet événement.\\
\medskip
Conform\'ement \`a la loi informatique et libert\'es du 6 janvier 1978, vous disposez d'un droit de libre acc\`es, de rectification, de modification et de suppression des donn\'ees qui vous concernent.
Cette autorisation est donc r\'evocable \`a tout moment sur volont\'e express\'ement manifest\'ee par lettre recommand\'ee avec accus\'e de r\'eception adress\'ee \`a Animath, IHP, 11 rue Pierre et Marie Curie, 75231 Paris cedex 05.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise Animath à conserver mes données personnelles, dans le cadre défini par la loi n 78-17 du 6 janvier 1978 relative à l'informatique, aux fichiers et aux libertés et les textes la modifiant, pendant une durée de quatre ans à compter de ma dernière participation à un événement organisé par Animath.\\
\medskip
\fbox{\textcolor{white}{A}} J'accepte d'être tenu informé d'autres activités organisées par l'association et ses partenaires.
\bigskip
Signature pr\'ec\'ed\'ee de la mention \og lu et approuv\'e \fg{}
\medskip
\begin{minipage}[c]{0.5\textwidth}
\underline{L'\'el\`eve :}\\
Fait \`a :\\
le
\end{minipage}
\vfill
\vfill
\begin{minipage}[c]{0.5\textwidth}
\footnotesize 11 rue Pierre et Marie Curie, 75231 Paris Cedex 05\\ Numéro siret 431 598 366 00018
\end{minipage}
\begin{minipage}[c]{0.5\textwidth}
\footnotesize
\begin{flushright}
Association agréée par\\le Ministère de l'éducation nationale.
\end{flushright}
\end{minipage}
\end{document}

View File

@ -0,0 +1,122 @@
\documentclass[a4paper,french,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[frenchb]{babel}
\usepackage{fancyhdr}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amssymb}
%\usepackage{anyfontsize}
\usepackage{fancybox}
\usepackage{eso-pic,graphicx}
\usepackage{xcolor}
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
% Page formating
\hoffset -1in
\voffset -1in
\textwidth 180 mm
\textheight 250 mm
\oddsidemargin 15mm
\evensidemargin 15mm
\pagestyle{fancy}
% Headers and footers
\fancyfoot{}
\lhead{}
\rhead{}
\renewcommand{\headrulewidth}{0pt}
\lfoot{\footnotesize 11 rue Pierre et Marie Curie, 75231 Paris Cedex 05\\ Numéro siret 431 598 366 00018}
\rfoot{\footnotesize Association agréée par\\le Ministère de l'éducation nationale.}
\begin{document}
\includegraphics[height=2cm]{assets/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
\vfill
\begin{center}
\LARGE
Autorisation d'enregistrement et de diffusion de l'image
({TOURNAMENT_NAME})
\end{center}
\normalsize
\thispagestyle{empty}
\bigskip
Je soussign\'e \dotfill (p\`ere, m\`ere, responsable l\'egal) \\
agissant en qualit\'e de repr\'esentant de {PARTICIPANT_NAME}\\
demeurant au {ADDRESS}
\medskip
Cochez la/les cases correspondantes.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise l'association Animath, \`a l'occasion du $\mathbb{TFJM}^2$ du {START_DATE} au {END_DATE} {YEAR} à : {PLACE}, \`a photographier ou \`a filmer l'enfant et \`a diffuser les photos et/ou les vid\'eos r\'ealis\'ees \`a cette occasion sur son site et sur les sites partenaires. D\'eclare c\'eder \`a titre gracieux \`a Animath le droit dutiliser l'image de l'enfant sur tous ses supports d'information : brochures, sites web, r\'eseaux sociaux. Animath devient, par la pr\'esente, cessionnaire des droits pendant toute la dur\'ee pour laquelle ont \'et\'e acquis les droits d'auteur de ces photographies.\\
\medskip
Animath s'engage, conform\'ement aux dispositions l\'egales en vigueur relatives au droit \`a l'image, \`a ce que la publication et la diffusion de l'image de l'enfant ainsi que des commentaires l'accompagnant ne portent pas atteinte \`a la vie priv\'ee, \`a la dignit\'e et \`a la r\'eputation de lenfant.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise la diffusion dans les medias (Presse, T\'el\'evision, Internet) de photographies de mon enfant prises \`a l'occasion dune \'eventuelle m\'ediatisation de cet événement.\\
\medskip
Conform\'ement \`a la loi informatique et libert\'es du 6 janvier 1978, vous disposez d'un droit de libre acc\`es, de rectification, de modification et de suppression des donn\'ees qui vous concernent.
Cette autorisation est donc r\'evocable \`a tout moment sur volont\'e express\'ement manifest\'ee par lettre recommand\'ee avec accus\'e de r\'eception adress\'ee \`a Animath, IHP, 11 rue Pierre et Marie Curie, 75231 Paris cedex 05.\\
\medskip
\fbox{\textcolor{white}{A}} Autorise Animath à conserver mes données personnelles, dans le cadre défini par la loi n 78-17 du 6 janvier 1978 relative à l'informatique, aux fichiers et aux libertés et les textes la modifiant, pendant une durée de quatre ans à compter de ma dernière participation à un événement organisé par Animath.\\
\medskip
\fbox{\textcolor{white}{A}} J'accepte d'être tenu informé d'autres activités organisées par l'association et ses partenaires.
\bigskip
Signatures pr\'ec\'ed\'ees de la mention \og lu et approuv\'e \fg{}
\medskip
\begin{minipage}[c]{0.5\textwidth}
\underline{Le responsable l\'egal :}\\
Fait \`a :\\
le :
\end{minipage}
\begin{minipage}[c]{0.5\textwidth}
\underline{L'\'el\`eve :}\\
Fait \`a :\\
le
\end{minipage}
\vfill
\vfill
\begin{minipage}[c]{0.5\textwidth}
\footnotesize 11 rue Pierre et Marie Curie, 75231 Paris Cedex 05\\ Numéro siret 431 598 366 00018
\end{minipage}
\begin{minipage}[c]{0.5\textwidth}
\footnotesize
\begin{flushright}
Association agréée par\\le Ministère de l'éducation nationale.
\end{flushright}
\end{minipage}
\end{document}

View File

@ -0,0 +1,66 @@
\documentclass[a4paper,french,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[french]{babel}
\usepackage{fancyhdr}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amssymb}
%\usepackage{anyfontsize}
\usepackage{fancybox}
\usepackage{eso-pic,graphicx}
\usepackage{xcolor}
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
% Page formating
\hoffset -1in
\voffset -1in
\textwidth 180 mm
\textheight 250 mm
\oddsidemargin 15mm
\evensidemargin 15mm
\pagestyle{fancy}
% Headers and footers
\fancyfoot{}
\lhead{}
\rhead{}
\renewcommand{\headrulewidth}{0pt}
\lfoot{\footnotesize 11 rue Pierre et Marie Curie, 75231 Paris Cedex 05\\ Numéro siret 431 598 366 00018}
\rfoot{\footnotesize Association agréée par\\le Ministère de l'éducation nationale.}
\begin{document}
\includegraphics[height=2cm]{assets/logo_animath.png}\hfill{\fontsize{55pt}{55pt}{$\mathbb{TFJM}^2$}}
\vfill
\begin{center}
\Large \bf Autorisation parentale pour les mineurs ({TOURNAMENT_NAME})
\end{center}
Je soussigné(e) \hrulefill,\\
responsable légal, demeurant \writingsep\hrulefill\\
\writingsep\hrulefill,\\
\writingsep autorise {PARTICIPANT_NAME},\\
né(e) le {BIRTHDAY},
à participer au Tournoi Français des Jeunes Mathématiciennes et Mathématiciens ($\mathbb{TFJM}^2$) organisé \`a : {PLACE}, du {START_DATE} au {END_DATE} {YEAR}.
{PRONOUN} se rendra au lieu indiqu\'e ci-dessus le vendredi matin et quittera les lieux l'après-midi du dimanche par ses propres moyens et sous la responsabilité du représentant légal.
\vspace{8ex}
Fait à \vrule width 10cm height 0pt depth 0.4pt, le \phantom{232323}/\phantom{XXX}/{YEAR},
\vfill
\vfill
\end{document}

BIN
static/Fiche synthèse.pdf Normal file

Binary file not shown.

194
static/Fiche synthèse.tex Normal file
View File

@ -0,0 +1,194 @@
\documentclass{article}
\usepackage[utf8]{inputenc}
\usepackage[french]{babel}
\usepackage{graphicx}
\usepackage[left=2cm,right=2cm,top=2cm,bottom=2cm]{geometry} % marges
\usepackage{amsthm}
\usepackage{amsmath}
\usepackage{amsfonts}
\usepackage{amssymb}
\usepackage{tikz}
\newcommand{\N}{{\bf N}}
\newcommand{\Z}{{\bf Z}}
\newcommand{\Q}{{\bf Q}}
\newcommand{\R}{{\bf R}}
\newcommand{\C}{{\bf C}}
\newcommand{\A}{{\bf A}}
\newtheorem{theo}{Théorème}
\newtheorem{theo-defi}[theo]{Théorème-Définition}
\newtheorem{defi}[theo]{Définition}
\newtheorem{lemme}[theo]{Lemme}
\newtheorem{slemme}[theo]{Sous-lemme}
\newtheorem{prop}[theo]{Proposition}
\newtheorem{coro}[theo]{Corollaire}
\newtheorem{conj}[theo]{Conjecture}
\title{Note de synthèse}
\begin{document}
\pagestyle{empty}
\begin{center}
\begin{Huge}
$\mathbb{TFJM}^2$
\end{Huge}
\bigskip
\begin{Large}
NOTE DE SYNTHESE
\end{Large}
\end{center}
Tour \underline{~~~~} poule \underline{~~~~}
\medskip
Problème \underline{~~~~} défendu par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~}
\medskip
Synthèse par l'équipe \underline{~~~~~~~~~~~~~~~~~~~~~~~~} dans le rôle de : ~ $\square$ Opposant ~ $\square$ Rapporteur
\section*{Questions traitées}
\begin{tabular}{r c l}
\begin{tabular}{|c|c|c|c|c|c|}
\hline
Question ~ & ER & ~PR~ & QE & NT \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
\end{tabular}
& ~~ &
\begin{tabular}{|c|c|c|c|c|c|}
\hline
Question ~ & ER & ~PR~ & QE & NT \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
& & & & \\
\hline
\end{tabular} \\
& & \\
ER : entièrement résolue & & PR : partiellement résolue \\
\smallskip
QE : quelques éléments de réponse & & NT : non traitée
\end{tabular}
~
\smallskip
Remarque : il est possible de cocher entre les cases pour un cas intermédiaire.
\section*{Evaluation qualitative de la solution}
Donnez votre avis concernant la solution. Mettez notamment en valeur les points positifs (des idées
importantes, originales, etc.) et précisez ce qui aurait pu améliorer la solution.
\vfill
\textbf{Evaluation générale :} ~ $\square$ Excellente ~ $\square$ Bonne ~ $\square$ Suffisante ~ $\square$ Passable
\newpage
\section*{Erreurs et imprécisions}
Listez ci-dessous les cinq erreurs et/ou imprécisions les plus importantes selon vous, par ordre d'importance, en précisant la
question concernée, la page, le paragraphe et le type de remarque.
\bigskip
1. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
2. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
3. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
4. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
5. Question \underline{~~~~} Page \underline{~~~~} Paragraphe \underline{~~~~}
$\square$ Erreur majeure ~ $\square$ Erreur mineure ~ $\square$ Imprécision ~ $\square$ Autre : \underline{~~~~~~~~}
Description :
\vfill
\section*{Remarques formelles (facultatif)}
Donnez votre avis concernant la présentation de la solution (lisibilité, etc.).
\vfill
\end{document}

BIN
static/Fiche_sanitaire.pdf Normal file

Binary file not shown.

88
static/Instructions.tex Normal file
View File

@ -0,0 +1,88 @@
\documentclass[a4paper,french,11pt]{article}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{lmodern}
\usepackage[frenchb]{babel}
\usepackage{fancyhdr}
\usepackage{graphicx}
\usepackage{amsmath}
\usepackage{amssymb}
%\usepackage{anyfontsize}
\usepackage{fancybox}
\usepackage{eso-pic,graphicx}
\usepackage{xcolor}
\usepackage{hyperref}
% Specials
\newcommand{\writingsep}{\vrule height 4ex width 0pt}
% Page formating
\hoffset -1in
\voffset -1in
\textwidth 180 mm
\textheight 250 mm
\oddsidemargin 15mm
\evensidemargin 15mm
\pagestyle{fancy}
% Headers and footers
\fancyfoot{}
\lhead{}
\rhead{}
\renewcommand{\headrulewidth}{0pt}
\lfoot{\footnotesize 11 rue Pierre et Marie Curie, 75231 Paris Cedex 05\\ Numéro siret 431 598 366 00018}
\rfoot{\footnotesize Association agréée par\\le Ministère de l'éducation nationale.}
\begin{document}
\includegraphics[height=2cm]{assets/logo_animath.png}\hfill{\fontsize{50pt}{50pt}{$\mathbb{TFJM}^2$}}
\begin{center}
\Large \bf Instructions ({TOURNAMENT_NAME})
\end{center}
\section{Documents}
\subsection{Autorisation parentale}
Elle est nécessaire si l'élève est mineur au moment du tournoi (y compris si son anniversaire est pendant le tournoi).
\subsection{Autorisation de prise de vue}
Si l'élève est mineur \textbf{au moment de la signature}, il convient de remplir l'autorisation pour les mineurs. En revanche, s'il est majeur \textbf{au moment de la signature}, il convient de remplir la fiche pour majeur.
\subsection{Fiche sanitaire}
Elle est nécessaire si l'élève est mineur au moment du tournoi (y compris si son anniversaire est pendant le tournoi).
\section{Paiement}
\subsection{Montant}
Les frais d'inscription sont fixés à {PRICE} euros. Vous devez vous en acquitter \textbf{avant le {END_PAYMENT_DATE} {YEAR}}. Si l'élève est boursier, il en est dispensé, vous devez alors fournir une copie de sa notification de bourse directement sur la plateforme \textbf{avant le {END_PAYMENT_DATE} {YEAR}}.
\subsection{Procédure}
Si le paiement de plusieurs élèves est fait en une seule opération, merci de contacter \href{mailto: contact@tfjm.org}{contact@tfjm.org} \textbf{avant le paiement} pour garantir l'identification de ce dernier
\subsubsection*{Carte bancaire (uniquement les cartes françaises)}
Le paiement s'effectue en ligne via la plateforme à l'adresse : \url{https://www.helloasso.com/associations/animath/evenements/tfjm-2020}
Vous devez impérativement indiquer dans le champ "Référence" la mention "TFJMpu" suivie des noms et prénoms \textbf{de l'élève}.
\subsubsection*{Virement}
\textbf{Si vous ne pouvez pas utiliser le paiement par carte}, vous pouvez faire un virement sur le compte ci-dessous en indiquant bien dans le champ "motif" (ou autre champ propre à votre banque dont le contenu est communiqué au destinataire) la mention "TFJMpu" suivie des noms et prénoms \textbf{de l'élève}.
IBAN FR76 1027 8065 0000 0206 4290 127
BIC CMCIFR2A
\subsubsection*{Autre}
Si aucune de ces procédures n'est possible pour vous, envoyez un mail à \href{mailto: contact@tfjm.org}{contact@tfjm.org} pour que nous trouvions une solution à vos difficultés.
\end{document}

View File

@ -0,0 +1,121 @@
@font-face {
font-family: 'Glyphicons Halflings';
src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot');
src: url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff2') format('woff2'),
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.woff') format('woff'),
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.ttf') format('truetype'),
url('//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');
}
.glyphicon {
position: relative;
top: 1px;
display: inline-block;
font-family: 'Glyphicons Halflings';
font-style: normal;
font-weight: normal;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.glyphicon-time:before {
content: "\e023";
}
.glyphicon-chevron-left:before {
content: "\e079";
}
.glyphicon-chevron-right:before {
content: "\e080";
}
.glyphicon-chevron-up:before {
content: "\e113";
}
.glyphicon-chevron-down:before {
content: "\e114";
}
.glyphicon-calendar:before {
content: "\e109";
}
.glyphicon-screenshot:before {
content: "\e087";
}
.glyphicon-trash:before {
content: "\e020";
}
.glyphicon-remove:before {
content: "\e014";
}
.bootstrap-datetimepicker-widget .btn {
display: inline-block;
padding: 6px 12px;
margin-bottom: 0;
font-size: 14px;
font-weight: normal;
line-height: 1.42857143;
text-align: center;
white-space: nowrap;
vertical-align: middle;
-ms-touch-action: manipulation;
touch-action: manipulation;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-image: none;
border: 1px solid transparent;
border-radius: 4px;
}
.bootstrap-datetimepicker-widget.dropdown-menu {
position: absolute;
left: 0;
z-index: 1000;
display: none;
float: left;
min-width: 160px;
padding: 5px 0;
margin: 2px 0 0;
font-size: 14px;
text-align: left;
list-style: none;
background-color: #fff;
-webkit-background-clip: padding-box;
background-clip: padding-box;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, .15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
box-shadow: 0 6px 12px rgba(0, 0, 0, .175);
}
.bootstrap-datetimepicker-widget .list-unstyled {
padding-left: 0;
list-style: none;
}
.bootstrap-datetimepicker-widget .collapse {
display: none;
}
.bootstrap-datetimepicker-widget .collapse.in {
display: block;
}
/* fix for bootstrap4 */
.bootstrap-datetimepicker-widget .table-condensed > thead > tr > th,
.bootstrap-datetimepicker-widget .table-condensed > tbody > tr > td,
.bootstrap-datetimepicker-widget .table-condensed > tfoot > tr > td {
padding: 5px;
}

View File

@ -0,0 +1,55 @@
jQuery(function ($) {
var datepickerDict = {};
var isBootstrap4 = $.fn.collapse.Constructor.VERSION.split('.').shift() == "4";
function fixMonthEndDate(e, picker) {
e.date && picker.val().length && picker.val(e.date.endOf('month').format('YYYY-MM-DD'));
}
$("[dp_config]:not([disabled])").each(function (i, element) {
var $element = $(element), data = {};
try {
data = JSON.parse($element.attr('dp_config'));
}
catch (x) { }
if (data.id && data.options) {
data.$element = $element.datetimepicker(data.options);
data.datepickerdata = $element.data("DateTimePicker");
datepickerDict[data.id] = data;
data.$element.next('.input-group-addon').on('click', function(){
data.datepickerdata.show();
});
if(isBootstrap4){
data.$element.on("dp.show", function (e) {
$('.collapse.in').addClass('show');
});
}
}
});
$.each(datepickerDict, function (id, to_picker) {
if (to_picker.linked_to) {
var from_picker = datepickerDict[to_picker.linked_to];
from_picker.datepickerdata.maxDate(to_picker.datepickerdata.date() || false);
to_picker.datepickerdata.minDate(from_picker.datepickerdata.date() || false);
from_picker.$element.on("dp.change", function (e) {
to_picker.datepickerdata.minDate(e.date || false);
});
to_picker.$element.on("dp.change", function (e) {
if (to_picker.picker_type == 'MONTH') fixMonthEndDate(e, to_picker.$element);
from_picker.datepickerdata.maxDate(e.date || false);
});
if (to_picker.picker_type == 'MONTH') {
to_picker.$element.on("dp.hide", function (e) {
fixMonthEndDate(e, to_picker.$element);
});
fixMonthEndDate({ date: to_picker.datepickerdata.date() }, to_picker.$element);
}
}
});
if(isBootstrap4) {
$('body').on('show.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){
$(e.target).addClass('in');
});
$('body').on('hidden.bs.collapse','.bootstrap-datetimepicker-widget .collapse',function(e){
$(e.target).removeClass('in');
});
}
});

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

114
static/logo.svg Normal file
View File

@ -0,0 +1,114 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="209.843 -2.284 30.311995 9.7779996"
version="1.1"
id="svg27"
sodipodi:docname="logo.svg"
width="30.311995"
height="9.7779999"
style="fill:black"
inkscape:version="0.92.2 2405546, 2018-03-11">
<metadata
id="metadata31">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1055"
id="namedview29"
showgrid="false"
inkscape:zoom="41.779237"
inkscape:cx="15.215997"
inkscape:cy="4.3644999"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg27" />
<defs
id="defs15">
<path
id="b"
d="m 2.58,-3.347 c 0.409,0 1.405,0.02 1.485,1.135 0.01,0.12 0.02,0.25 0.18,0.25 0.168,0 0.168,-0.14 0.168,-0.32 v -2.7 c 0,-0.159 0,-0.318 -0.169,-0.318 -0.13,0 -0.17,0.1 -0.18,0.21 -0.059,1.155 -0.756,1.354 -1.484,1.384 v -2.102 c 0,-0.668 0.19,-0.668 0.429,-0.668 h 0.468 c 1.275,0 1.923,0.688 1.983,1.375 0.01,0.08 0.02,0.23 0.179,0.23 0.17,0 0.17,-0.16 0.17,-0.33 v -1.295 c 0,-0.308 -0.02,-0.328 -0.33,-0.328 h -5 c -0.18,0 -0.34,0 -0.34,0.179 0,0.17 0.19,0.17 0.27,0.17 0.567,0 0.607,0.079 0.607,0.567 v 4.991 c 0,0.469 -0.03,0.568 -0.558,0.568 -0.15,0 -0.319,0 -0.319,0.17 C 0.14,0 0.3,0 0.48,0 h 2.878 c 0.18,0 0.33,0 0.33,-0.18 0,-0.169 -0.17,-0.169 -0.3,-0.169 -0.767,0 -0.807,-0.07 -0.807,-0.597 v -2.401 z m 2.88,-3.129 v 0.469 A 2.557,2.557 0 0 0 4.922,-6.476 Z M 4.065,-3.158 A 1.51,1.51 0 0 0 3.537,-3.547 c 0.189,-0.09 0.388,-0.249 0.528,-0.418 z m -2.7,-2.77 c 0,-0.12 0,-0.368 -0.08,-0.548 h 1.056 c -0.11,0.23 -0.11,0.558 -0.11,0.648 v 4.901 c 0,0.15 0,0.389 0.1,0.578 H 1.285 c 0.08,-0.179 0.08,-0.428 0.08,-0.548 v -5.03 z"
inkscape:connector-curvature="0" />
<path
id="c"
d="m 1.564,-6.824 c -0.18,0 -0.339,0 -0.339,0.179 0,0.17 0.18,0.17 0.29,0.17 0.687,0 0.727,0.069 0.727,0.577 v 5.59 c 0,0.169 0,0.358 -0.17,0.527 -0.08,0.07 -0.239,0.18 -0.478,0.18 -0.07,0 -0.369,0 -0.369,-0.11 0,-0.08 0.04,-0.12 0.09,-0.17 A 0.704,0.704 0 0 0 0.777,-1.057 0.704,0.704 0 0 0 0.06,-0.359 c 0,0.629 0.637,1.106 1.604,1.106 1.106,0 2.042,-0.387 2.192,-1.614 0.01,-0.09 0.01,-0.647 0.01,-0.966 v -4.184 c 0,-0.449 0.139,-0.449 0.707,-0.459 0.09,0 0.17,-0.08 0.17,-0.17 0,-0.178 -0.15,-0.178 -0.33,-0.178 z M 0.867,0.239 C 0.767,0.19 0.408,0.02 0.408,-0.349 c 0,-0.259 0.22,-0.358 0.37,-0.358 0.168,0 0.368,0.12 0.368,0.348 0,0.15 -0.08,0.24 -0.12,0.27 -0.04,0.04 -0.13,0.139 -0.16,0.328 z M 2.59,-5.918 c 0,-0.11 0,-0.378 -0.09,-0.558 h 1.097 c -0.08,0.18 -0.08,0.369 -0.08,0.708 v 4.015 c 0,0.298 0,0.797 -0.01,0.896 C 3.427,-0.349 3.198,0.11 2.44,0.31 2.59,0.08 2.59,-0.109 2.59,-0.288 v -5.629 z"
inkscape:connector-curvature="0" />
<path
id="d"
d="M 4.643,-2.092 2.74,-6.625 c -0.08,-0.2 -0.09,-0.2 -0.359,-0.2 H 0.528 c -0.18,0 -0.329,0 -0.329,0.18 0,0.17 0.18,0.17 0.23,0.17 0.119,0 0.388,0.02 0.607,0.099 v 5.32 c 0,0.21 0,0.648 -0.677,0.707 -0.19,0.02 -0.19,0.16 -0.19,0.17 C 0.17,0 0.33,0 0.51,0 h 1.543 c 0.18,0 0.33,0 0.33,-0.18 0,-0.089 -0.08,-0.159 -0.16,-0.169 -0.767,-0.06 -0.767,-0.478 -0.767,-0.707 v -4.961 l 0.01,-0.01 2.429,5.817 c 0.08,0.18 0.15,0.209 0.21,0.209 0.12,0 0.149,-0.08 0.199,-0.2 l 2.44,-5.827 0.01,0.01 v 4.961 c 0,0.21 0,0.648 -0.677,0.707 -0.19,0.02 -0.19,0.16 -0.19,0.17 0,0.179 0.16,0.179 0.34,0.179 h 2.66 c 0.179,0 0.328,0 0.328,-0.18 C 9.215,-0.27 9.135,-0.34 9.056,-0.35 8.289,-0.41 8.289,-0.828 8.289,-1.057 v -4.712 c 0,-0.21 0,-0.648 0.677,-0.708 0.1,-0.01 0.19,-0.06 0.19,-0.17 0,-0.178 -0.15,-0.178 -0.33,-0.178 H 6.905 c -0.259,0 -0.279,0 -0.369,0.209 z m -0.3,0.18 c 0.08,0.169 0.09,0.178 0.21,0.218 L 4.115,-0.638 H 4.095 L 1.823,-6.058 C 1.773,-6.187 1.693,-6.356 1.554,-6.476 h 0.867 l 1.923,4.563 z M 1.336,-0.35 h -0.17 c 0.02,-0.03 0.04,-0.06 0.06,-0.08 0.01,-0.01 0.01,-0.02 0.02,-0.03 z M 7.104,-6.477 H 8.16 c -0.219,0.25 -0.219,0.508 -0.219,0.688 v 4.752 c 0,0.18 0,0.438 0.23,0.687 H 6.883 c 0.22,-0.249 0.22,-0.508 0.22,-0.687 v -5.44 z"
inkscape:connector-curvature="0" />
<path
id="a"
d="m 4.135,-6.466 c 1.305,0.07 1.793,0.917 1.833,1.385 0.01,0.15 0.02,0.299 0.179,0.299 0.18,0 0.18,-0.17 0.18,-0.359 v -1.325 c 0,-0.348 -0.04,-0.358 -0.34,-0.358 H 0.658 c -0.308,0 -0.328,0.02 -0.328,0.318 V -5.1 c 0,0.16 0,0.319 0.17,0.319 0.17,0 0.178,-0.18 0.178,-0.2 0.04,-0.826 0.788,-1.424 1.834,-1.484 v 5.54 c 0,0.498 -0.04,0.577 -0.668,0.577 -0.12,0 -0.299,0 -0.299,0.17 0,0.179 0.16,0.179 0.339,0.179 h 2.89 C 4.95,0 5.1,0 5.1,-0.18 c 0,-0.169 -0.17,-0.169 -0.28,-0.169 -0.647,0 -0.686,-0.07 -0.686,-0.578 v -5.539 z m -3.458,-0.01 h 0.598 c -0.249,0.15 -0.458,0.349 -0.598,0.518 z m 5.3,0 v 0.528 A 2.606,2.606 0 0 0 5.37,-6.476 H 5.978 Z M 2.77,-0.349 c 0.09,-0.179 0.09,-0.428 0.09,-0.558 v -5.569 h 0.926 v 5.57 c 0,0.129 0,0.378 0.09,0.557 H 2.77 Z"
inkscape:connector-curvature="0" />
<path
id="e"
d="M 3.522,-1.27 H 3.285 c -0.021,0.154 -0.091,0.566 -0.182,0.635 -0.055,0.042 -0.592,0.042 -0.69,0.042 H 1.13 c 0.732,-0.648 0.976,-0.844 1.395,-1.171 0.516,-0.412 0.997,-0.844 0.997,-1.507 0,-0.844 -0.74,-1.36 -1.632,-1.36 -0.865,0 -1.45,0.607 -1.45,1.249 0,0.355 0.3,0.39 0.369,0.39 0.167,0 0.37,-0.118 0.37,-0.37 0,-0.125 -0.05,-0.369 -0.412,-0.369 0.216,-0.495 0.69,-0.649 1.018,-0.649 0.698,0 1.06,0.544 1.06,1.11 0,0.606 -0.432,1.087 -0.655,1.338 l -1.68,1.66 C 0.44,-0.209 0.44,-0.195 0.44,0 h 2.873 z"
inkscape:connector-curvature="0" />
</defs>
<use
x="209.843"
y="6.6110001"
xlink:href="#a"
id="use17"
width="100%"
height="100%"
transform="translate(-0.33000232,0.13600003)" />
<use
x="216.485"
y="6.6110001"
xlink:href="#b"
id="use19"
width="100%"
height="100%"
transform="translate(-0.33000232,0.13600003)" />
<use
x="222.573"
y="6.6110001"
xlink:href="#c"
id="use21"
width="100%"
height="100%"
transform="translate(-0.33000232,0.13600003)" />
<use
x="227.554"
y="6.6110001"
xlink:href="#d"
id="use23"
width="100%"
height="100%"
transform="translate(-0.33000232,0.13600003)" />
<use
x="236.963"
y="2.211"
xlink:href="#e"
id="use25"
width="100%"
height="100%"
transform="translate(-0.33000232,0.13600003)" />
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
static/logo_animath.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

47
static/style.css Normal file
View File

@ -0,0 +1,47 @@
html, body {
height: 100%;
margin: 0;
}
:root {
--navbar-height: 32px;
}
.container {
min-height: 78%;
}
.inner {
margin: 20px;
}
.alert {
text-align: justify;
}
footer .alert {
text-align: center;
}
#navbar-logo {
height: var(--navbar-height);
display: block;
}
ul .deroule {
display: none;
position: absolute;
background: #f8f9fa !important;
list-style-type: none;
padding: 20px;
z-index: 42;
}
li:hover ul.deroule {
display:block;
}
a.nav-link:hover {
background-color: #d8d9da;
}

8
templates/400.html Normal file
View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Bad request" %}</h1>
{% blocktrans %}Sorry, your request was bad. Don't know what could be wrong. An email has been sent to webmasters with the details of the error. You can now drink a coke.{% endblocktrans %}
{% endblock %}

13
templates/403.html Normal file
View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Permission denied" %}</h1>
{% blocktrans %}You don't have the right to perform this request.{% endblocktrans %}
{% if exception %}
<div>
{% trans "Exception message:" %} {{ exception }}
</div>
{% endif %}
{% endblock %}

13
templates/404.html Normal file
View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Page not found" %}</h1>
{% blocktrans %}The requested path <code>{{ request_path }}</code> was not found on the server.{% endblocktrans %}
{% if exception != "Resolver404" %}
<div>
{% trans "Exception message:" %} {{ exception }}
</div>
{% endif %}
{% endblock %}

8
templates/500.html Normal file
View File

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
<h1>{% trans "Server error" %}</h1>
{% blocktrans %}Sorry, an error occurred when processing your request. An email has been sent to webmasters with the detail of the error, and this will be fixed soon. You can now drink a beer.{% endblocktrans %}
{% endblock %}

View File

@ -0,0 +1,11 @@
<div class="input-group">
<input class="form-control mx-auto d-block" type="number" min="0" step="0.01"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
<div class="input-group-append">
<span class="input-group-text"></span>
</div>
</div>

View File

@ -0,0 +1,9 @@
<input type="hidden" name="{{ widget.name }}" {% if widget.attrs.model_pk %}value="{{ widget.attrs.model_pk }}"{% endif %} id="{{ widget.attrs.id }}_pk">
<input type="text"
{% if widget.value != None and widget.value != "" %}value="{{ widget.value }}"{% endif %}
name="{{ widget.name }}_name" autocomplete="off"
{% for name, value in widget.attrs.items %}
{% ifnotequal value False %}{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}{% endifnotequal %}
{% endfor %}>
<ul class="list-group list-group-flush" id="{{ widget.attrs.id }}_list">
</ul>

234
templates/base.html Normal file
View File

@ -0,0 +1,234 @@
{% load static i18n static getconfig %}
<!DOCTYPE html>
<html lang="en" class="position-relative h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>
{% block title %}{{ title }}{% endblock title %} - Inscription au TFJM²
</title>
<meta name="description" content="{% trans "The inscription site of the TFJM²." %}">
{# Favicon #}
<link rel="shortcut icon" href="{% static "favicon.ico" %}">
<meta name="theme-color" content="#ffffff">
{% if no_cache %}
<meta name="turbolinks-cache-control" content="no-cache">
{% endif %}
{# Bootstrap CSS #}
<link rel="stylesheet"
href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css"
integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh"
crossorigin="anonymous">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/all.css">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.13.0/css/v4-shims.css">
{# Custom CSS #}
<link rel="stylesheet" type="text/css" href="{% static "style.css" %}">
{# JQuery, Bootstrap and Turbolinks JavaScript #}
<script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha384-vk5WoKIaW/vJyUAd9n/wmopsmNhiy+L2Z+SBxGYnUkunIxVxAv/UtMOhba/xskxh"
crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"
integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"
integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/turbolinks/5.2.0/turbolinks.js"
crossorigin="anonymous"></script>
{# Si un formulaire requiert des données supplémentaires (notamment JS), les données sont chargées #}
{% if form.media %}
{{ form.media }}
{% endif %}
<style>
.validate:hover {
cursor: pointer;
text-decoration: underline;
}
</style>
{% block extracss %}{% endblock %}
</head>
<body class="d-flex w-100 h-100 flex-column">
<main class="mb-auto">
<nav class="navbar navbar-expand-md navbar-light bg-light fixed-navbar shadow-sm">
<a class="navbar-brand" href="https://tfjm.org/">
<img src="{% static "logo.svg" %}" alt="Logo TFJM²" id="navbar-logo">
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarNavDropdown"
aria-controls="navbarNavDropdown" aria-expanded="false"
aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div id="navbarNavDropdown" class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item active">
<a href="{% url "index" %}" class="nav-link"><i class="fas fa-home"></i> {% trans "Home" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:list" %}"><i class="fas fa-calendar"></i> {% trans "Tournament list" %}</a>
{% if user.organizes %}
<ul class="deroule">
{% if user.admin %}
<li class="nav-item active"><a class="nav-link" href="{% url "member:orphaned_profiles" %}"><i class="fas fa-user"></i> {% trans "Orphaned profiles" %}</a></li>
<li class="nav-item active"><a class="nav-link" href="{% url "member:all_profiles" %}"><i class="fas fa-users"></i> {% trans "All profiles" %}</a></li>
{% endif %}
<li class="nav-item active"><a class="nav-link" href="{% url "member:organizers" %}"><i class="fas fa-user-tie"></i> {% trans "Organizers" %}</a></li>
</ul>
{% endif %}
</li>
{% if user.is_authenticated %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:my_account" %}"><i class="fas fa-user"></i> {% trans "My account" %}</a>
</li>
{% if user.participates %}
{% if not user.team %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:add_team" %}"><i class="fas fa-folder-plus"></i> {% trans "Add a team" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "member:join_team" %}"><i class="fas fa-users"></i> {% trans "Join a team" %}</a>
</li>
{% else %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:my_team" %}"><i class="fas fa-users-cog"></i> {% trans "My team" %}</a>
</li>
{% if user.team.valid %}
<!-- <li class="nav-item active">
<a class="nav-link" href="/paiement">Paiement</a>
</li> -->
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:solutions" %}"><i class="fas fa-lightbulb"></i> {% trans "Solutions" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li>
{% endif %}
{% endif %}
{% endif %}
{% if user.organizes %}
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_solutions" %}"><i class="fas fa-lightbulb"></i> {% trans "Solutions" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:pools" %}"><i class="fas fa-swimming-pool"></i> {% trans "Pools" %}</a>
</li>
{% endif %}
{% if not user.is_authenticated and request.session.extra_access_token %}
{# Juries can access to pool data without logging in. #}
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_solutions" %}"><i class="fas fa-lightbulb"></i> {% trans "Solutions" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:all_syntheses" %}"><i class="fas fa-feather"></i> {% trans "Syntheses" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "tournament:pools" %}"><i class="fas fa-swimming-pool"></i> {% trans "Pools" %}</a>
</li>
{% endif %}
<li class="nav-item active">
<a class="nav-link" href="https://www.helloasso.com/associations/animath/formulaires/5/widget"><i
class="fas fa-hand-holding-heart"></i> {% trans "Make a gift" %}</a>
</li>
{% if user.admin %}
<li class="nav-item active">
<a data-turbolinks="false" class="nav-link" href="{% url "admin:index" %}"><i class="fas fa-cog"></i> {% trans "Administration" %}</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
{% if "_fake_user_id" in request.session %}
<li class="nav-item active">
<a class="nav-link" href="{% url "member:reset_admin" %}?path={{ request.path }}"><i class="fas fa-tools"></i> {% trans "Return to admin view" %}</a>
</li>
{% endif %}
{% if not user.is_authenticated %}
<li class="nav-item active">
<a class="nav-link" href="{% url "login" %}"><i class="fas fa-sign-in-alt"></i> {% trans "Log in" %}</a>
</li>
<li class="nav-item active">
<a class="nav-link" href="{% url "member:signup" %}"><i class="fas fa-user-plus"></i> {% trans "Sign up" %}</a>
</li>
{% else %}
<li class="nav-item active">
<a class="nav-link" href="{% url "logout" %}"><i class="fas fa-sign-out-alt"></i> {% trans "Log out" %}</a>
</li>
{% endif %}
</ul>
</div>
</nav>
<div class="container-fluid my-3" style="max-width: 1600px;">
{% block contenttitle %}<h1>{{ title }}</h1>{% endblock %}
<div id="messages"></div>
{% block content %}
<p>Default content...</p>
{% endblock content %}
</div>
</main>
<footer class="bg-light mt-auto py-2">
<div class="container-fluid">
<div class="row">
<div class="col-sm">
<form action="{% url 'set_language' %}" method="post"
class="form-inline">
<span class="text-muted mr-1">
𝕋𝔽𝕁𝕄² &mdash;
<a href="mailto:contact@tfjm.org"
class="text-muted">Nous contacter</a> &mdash;
</span>
{% csrf_token %}
<select title="language" name="language"
class="custom-select custom-select-sm"
onchange="this.form.submit()">
{% get_current_language as LANGUAGE_CODE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"
{% if language.code == LANGUAGE_CODE %}
selected{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
<noscript>
<input type="submit">
</noscript>
</form>
</div>
<div class="text-center text-secondary">
<em>Ce site a été conçu pour Animath, avec amour et passion. Il est récent et il est possible que
certaines pages ne fonctionnent pas correctement. Si vous remarquez des bugs, merci de les signaler
à
l'adresse <a href="mailto:contact@tfjm.org">contact@tfjm.org</a>.</em><br/>
© {{ "TFJM_YEAR"|get_env }} Tournoi Français des Jeunes Mathématiciennes et Mathématiciens
</div>
<div class="col-sm text-right">
<a href="#" class="text-muted">Retour en haut</a>
</div>
</div>
</div>
</footer>
<script>
CSRF_TOKEN = "{{ csrf_token }}";
</script>
{% block extrajavascript %}
{% endblock extrajavascript %}
</body>
</html>

View File

@ -0,0 +1,6 @@
<div class="input-group date">
{% include "bootstrap_datepicker_plus/input.html" %}
<div class="input-group-addon input-group-append" data-target="#datetimepicker1" data-toggle="datetimepickerv">
<div class="input-group-text"><i class="glyphicon glyphicon-calendar"></i></div>
</div>
</div>

View File

@ -0,0 +1,4 @@
<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None and widget.value != "" %}
value="{{ widget.value }}"{% endif %}{% for name, value in widget.attrs.items %}{% ifnotequal value False %}
{{ name }}{% ifnotequal value True %}="{{ value|stringformat:'s' }}"{% endifnotequal %}
{% endifnotequal %}{% endfor %}/>

View File

@ -0,0 +1,6 @@
<div class="input-group date">
{% include "bootstrap_datepicker_plus/input.html" %}
<div class="input-group-addon input-group-append" data-target="#datetimepicker1" data-toggle="datetimepickerv">
<div class="input-group-text"><i class="glyphicon glyphicon-time"></i></div>
</div>
</div>

View File

@ -0,0 +1,8 @@
<input type="text"
id="{{ widget.attrs.id }}"
class="form-control colorfield_field jscolor"
name="{{ widget.name }}"
value="{% firstof widget.value widget.attrs.default '' %}"
placeholder="{% firstof widget.value widget.attrs.default '' %}"
data-jscolor="{hash:true,width:225,height:150,required:{% if widget.attrs.required %}true{% else %}false{% endif %}}"
{% if widget.attrs.required %}required{% endif %} />

View File

@ -0,0 +1,5 @@
{% load crispy_forms_tags %}
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
{% crispy filter.form %}

View File

@ -0,0 +1,6 @@
{% load i18n %}
<h2>{% trans "Field filters" %}</h2>
<form class="form" action="" method="get">
{{ filter.form.as_p }}
<button type="submit" class="btn btn-primary">{% trans "Submit" %}</button>
</form>

View File

@ -0,0 +1 @@
{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}

9
templates/index.html Normal file
View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% load getconfig %}
{% block content %}
{% autoescape off %}
{{ "index_page"|get_config|safe }}
{% endautoescape %}
{% endblock %}

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Organisateur du TFJM²</title>
</head>
<body>
Bonjour {{ user }},<br />
<br />
Vous recevez ce message (envoyé automatiquement) car vous êtes organisateur d'un des tournois du TFJM<sup>2</sup>.<br /><br />
Un compte organisateur vous a été créé par l'un des administrateurs. Avant de vous connecter, vous devez réinitialiser votre
mot de passe sur le lien suivant : <a href="https://inscription.tfjm.org{% url "password_reset" %}">https://inscription.tfjm.org{% url "password_reset" %}</a>.
<br />
Une fois le mot de passe changé, vous pourrez vous <a href="https://inscription.tfjm.org{% url "login" %}">connecter sur la plateforme</a>.<br />
<br />
Merci beaucoup pour votre aide !<br />
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,12 @@
Bonjour {{ user }},
Vous recevez ce message (envoyé automatiquement) car vous êtes organisateur d'un des tournois du TFJM².
Un compte organisateur vous a été créé par l'un des administrateurs. Avant de vous connecter, vous devez réinitialiser votre
mot de passe sur le lien suivant : https://inscription.tfjm.org{% url "password_reset" %}.
Une fois le mot de passe changé, vous pourrez vous connecter sur la plateforme : https://inscription.tfjm.org{% url "login" %}.
Merci beaucoup pour votre aide !
Le comité national d'organisation du TFJM²

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Organisateur du tournoi de {TOURNAMENT_NAME} TFJM²</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Vous venez d'être promu organisateur du tournoi <a href="{URL_BASE}/tournoi/{TOURNAMENT_NAME}">{TOURNAMENT_NAME}</a> du TFJM<sup>2</sup> {YEAR}.<br />
Ce message vous a été envoyé automatiquement. En cas de problème, merci de répondre à ce message.
<br />
Avec toute notre bienveillance,<br />
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Nouvelle équipe TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Vous venez de créer l'équipe « {TEAM_NAME} » ({TRIGRAM}) pour le TFJM<sup>2</sup> de {TOURNAMENT_NAME} et nous vous en remercions.<br />
Afin de permettre aux autres membres de votre équipe de vous rejoindre, veuillez leur transmettre le code d'accès :
{ACCESS_CODE}<br/>
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Changement d'adresse e-mail TFJM²</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Vous venez de changer votre adresse e-mail. Veuillez désormais la confirmer en cliquant ici : <a
href="{URL_BASE}/confirmer_mail/{TOKEN}">{URL_BASE}/confirmer_mail/{TOKEN}</a><br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mot de passe changé TFJM²</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Nous vous informons que votre mot de passe vient d'être modifié. Si vous n'êtes pas à l'origine de cette manipulation,
veuillez immédiatement vérifier vos accès à votre boîte mail et changer votre mot de passe sur la plateforme
d'inscription.<br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Inscription au TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Vous êtes inscrit au TFJM<sup>2</sup> {YEAR} et nous vous en remercions.<br/>
Pour valider votre adresse e-mail, veuillez cliquer sur le lien : <a href="{URL_BASE}/confirmer_mail/{TOKEN}">{URL_BASE}/confirmer_mail/{TOKEN}</a><br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Mot de passe oublié TFJM²</title>
</head>
<body>
Bonjour,<br/>
<br/>
Vous avez indiqué avoir oublié votre mot de passe. Veuillez cliquer ici pour le réinitialiser : <a
href="{URL_BASE}/connexion/reinitialiser_mdp/{TOKEN}">{URL_BASE}/connexion/reinitialiser_mdp/{TOKEN}</a><br/>
<br/>
Si vous n'êtes pas à l'origine de cette manipulation, vous pouvez ignorer ce message.<br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Équipe rejointe TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br/>
<br/>
Vous venez de rejoindre l'équipe « {TEAM_NAME} » ({TRIGRAM}) pour le TFJM² de {TOURNAMENT_NAME} et nous vous en
remercions.<br/>
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Inscription au TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Vous venez de vous inscrire au TFJM<sup>2</sup> {YEAR} et nous vous en remercions.<br />
Pour valider votre adresse e-mail, veuillez cliquer sur le lien : <a href="{URL_BASE}/confirmer_mail/{TOKEN}">{URL_BASE}/confirmer_mail/{TOKEN}</a><br />
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Demande de validation de paiement pour le TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
{USER_FIRST_NAME} {USER_SURNAME} de l'équipe {TEAM_NAME} ({TRIGRAM}) annonce avoir réglé sa participation pour le tournoi {TOURNAMENT_NAME}.
Les informations suivantes ont été communiquées :<br /><br />
<strong>Équipe :</strong> {TEAM_NAME} ({TRIGRAM})<br />
<strong>Tournoi :</strong> {TOURNAMENT_NAME}<br />
<strong>Moyen de paiement :</strong> {PAYMENT_METHOD}<br />
<strong>Montant :</strong> {AMOUNT} €<br />
<strong>Informations sur le paiement :</strong> {PAYMENT_INFOS}<br />
<br />
Vous pouvez désormais vérifier ces informations, puis valider (ou non) le paiement sur
<a href="{URL_BASE}/informations/{USER_ID}/">la page associée à ce participant</a>.
<br />
Avec toute notre bienveillance,
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Demande de validation - TFJM²</title>
</head>
<body>
Bonjour {{ user }},<br />
<br />
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer au tournoi
{{ tournament }} 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://inscription.tfjm.org{% url "tournament:team_detail" pk=team.pk %}">https://inscription.tfjm.org{% url "tournament:team_detail" pk=team.pk %}</a><br/>
<br/>
Avec toute notre bienveillance,<br/>
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,9 @@
Bonjour {{ user }},
L'équipe « {{ team.name }} » ({{ team.trigram }}) vient de demander à valider son équipe pour participer au tournoi
{{ tournament }} du TFJM². Vous pouvez décider d'accepter ou de refuser l'équipe en vous rendant sur la page de l'équipe :
https://inscription.tfjm.org{% url "tournament:team_detail" pk=team.pk %}.
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Sélection pour la finale - TFJM²</title>
</head>
<body>
Bonjour {{ user }},<br>
<br>
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est sélectionnée pour la finale nationale !<br>
<br>
La finale aura lieu du {{ final.date_start }} au {{ final.date_end }}. Vous pouvez peaufiner vos solutions
si vous le souhaitez jusqu'au {{ final.date_solutions }}.<br>
<br>
Bravo encore !<br>
<br>
Avec toute notre bienveillance,<br>
<br>
Le comité national d'organisation du TFJM²
</body>
</html>

View File

@ -0,0 +1,12 @@
Bonjour {{ user }},
Félicitations ! Votre équipe « {{ team.name }} » ({{ team.trigram }}) est sélectionnée pour la finale nationale !
La finale aura lieu du {{ final.date_start }} au {{ final.date_end }}. Vous pouvez peaufiner vos solutions
si vous le souhaitez jusqu'au {{ final.date_solutions }}.
Bravo encore !
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Non-validation du paiement pour le TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Votre paiement pour le TFJM² {YEAR} a malheureusement été rejeté. Pour rappel, vous aviez fourni ces informations :<br /><br />
<strong>Équipe :</strong> {TEAM_NAME} ({TRIGRAM})<br />
<strong>Tournoi :</strong> {TOURNAMENT_NAME}<br />
<strong>Moyen de paiement :</strong> {PAYMENT_METHOD}<br />
<strong>Montant :</strong> {AMOUNT} €<br />
<strong>Informations sur le paiement :</strong> {PAYMENT_INFOS}<br />
<br />
{MESSAGE}
<br />
Avec toute notre bienveillance,
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<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 %}
<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/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,15 @@
Bonjour {{ user }},
Maleureusement, votre équipe « {{ team.name }} » ({{ team.trigram }}) n'a pas été validée. Veuillez vérifier que vos autorisations sont correctes.
{% if message %}
Le CNO vous adresse le message suivant :
{{ message }}
{% endif %}
N'hésitez pas à nous contacter à l'adresse contact@tfjm.org pour plus d'informations.
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<!--suppress HtmlUnknownTarget -->
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Validation du paiement pour le TFJM² {YEAR}</title>
</head>
<body>
Bonjour {FIRST_NAME} {SURNAME},<br />
<br />
Votre paiement pour le TFJM² {YEAR} a bien été validé. Pour rappel, vous aviez fourni ces informations :<br /><br />
<strong>Équipe :</strong> {TEAM_NAME} ({TRIGRAM})<br />
<strong>Tournoi :</strong> {TOURNAMENT_NAME}<br />
<strong>Moyen de paiement :</strong> {PAYMENT_METHOD}<br />
<strong>Montant :</strong> {AMOUNT} €<br />
<strong>Informations sur le paiement :</strong> {PAYMENT_INFOS}<br />
<br />
{MESSAGE}
<br />
Avec toute notre bienveillance,
<br />
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<title>Équipe validée TFJM² {YEAR}</title>
</head>
<body>
Bonjour {{ user }},<br/>
<br/>
Félicitations ! Votre équipe « {{ team }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte à travailler sur
vos problèmes et publier vos solutions sur la plateforme.
{% if message %}
<p>
Le CNO vous adresse le message suivant :
<div>
{{ message }}
</div>
</p>
{% endif %}
<br/>
Avec toute notre bienveillance,<br/>
<br/>
Le comité national d'organisation du TFJM<sup>2</sup>
</body>
</html>

View File

@ -0,0 +1,13 @@
Bonjour {{ user }},
Félicitations ! Votre équipe « {{ team }} » ({{ team.trigram }}) est désormais validée ! Vous êtes désormais apte à travailler sur
vos problèmes et publier vos solutions sur la plateforme.
{% if message %}
Le CNO vous adresse le message suivant :
{{ message }}
{% endif %}
Avec toute notre bienveillance,
Le comité national d'organisation du TFJM²

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load i18n crispy_forms_filters %}
{% block content %}
<form method="post">
{% csrf_token %}
{{ form|crispy }}
<input type="submit" class="btn btn-primary btn-block" value="{% trans "Submit" %}">
</form>
<hr>
<a class="btn btn-secondary btn-block" href="{% url "password_change" %}">{% trans "Update my password" %}</a>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "base.html" %}
{% load django_tables2 i18n %}
{% block content %}
{% render_table table %}
{% if type == "organizers" and user.admin %}
<hr>
<a class="btn btn-block btn-secondary" href="{% url "tournament:add_organizer" %}"><i class="fas fa-user-plus"></i> {% trans "Add an organizer" %}</a>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% load getconfig i18n django_tables2 static %}
{% block content %}
<div class="card bg-light shadow">
<div class="card-header text-center">
<h4>{{ tfjmuser }}</h4>
</div>
<div class="card-body">
<dl class="row">
<dt class="col-xl-6 text-right">{% trans 'role'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.get_role_display }}</dd>
{% if tfjmuser.team %}
<dt class="col-xl-6 text-right">{% trans 'team'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{% url "tournament:team_detail" pk=tfjmuser.team.pk %}">{{ tfjmuser.team }}</a></dd>
{% endif %}
{% if tfjmuser.birth_date %}
<dt class="col-xl-6 text-right">{% trans 'birth date'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.birth_date }}</dd>
{% endif %}
{% if tfjmuser.participates %}
<dt class="col-xl-6 text-right">{% trans 'gender'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.get_gender_display }}</dd>
{% endif %}
{% if tfjmuser.address %}
<dt class="col-xl-6 text-right">{% trans 'address'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.address }}, {{ tfjmuser.postal_code }}, {{ tfjmuser.city }}{% if tfjmuser.country != "France" %}, {{ tfjmuser.country }}{% endif %}</dd>
{% endif %}
<dt class="col-xl-6 text-right">{% trans 'email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="mailto:{{ tfjmuser.email }}">{{ tfjmuser.email }}</a></dd>
{% if tfjmuser.phone_number %}
<dt class="col-xl-6 text-right">{% trans 'phone number'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.phone_number }}</dd>
{% endif %}
{% if tfjmuser.role == '3participant' %}
<dt class="col-xl-6 text-right">{% trans 'school'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.school }}</dd>
<dt class="col-xl-6 text-right">{% trans 'class'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.get_student_class_display }}</dd>
{% if tfjmuser.responsible_name %}
<dt class="col-xl-6 text-right">{% trans 'responsible name'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.responsible_name }}</dd>
{% endif %}
{% if tfjmuser.responsible_phone %}
<dt class="col-xl-6 text-right">{% trans 'responsible phone'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.responsible_phone }}</dd>
{% endif %}
{% if tfjmuser.responsible_email %}
<dt class="col-xl-6 text-right">{% trans 'responsible email'|capfirst %}</dt>
<dd class="col-xl-6"><a href="{{ tfjmuser.responsible_email }}">{{ tfjmuser.responsible_email }}</a></dd>
{% endif %}
{% endif %}
{% if tfjmuser.role != '3participant' %}
<dt class="col-xl-6 text-right">{% trans 'description'|capfirst %}</dt>
<dd class="col-xl-6">{{ tfjmuser.description|default_if_none:"" }}</dd>
{% endif %}
</dl>
</div>
</div>
<hr>
<h4>{% trans "Documents" %}</h4>
{# TODO Display documents #}
{% if request.user.is_superuser %}
<hr>
<form method="post">
{% csrf_token %}
<button name="view_as" class="btn btn-block btn-warning">{% blocktrans %}View site as {{ tfjmuser }}{% endblocktrans %}</button>
</form>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load i18n %}
{% block content %}
{% if validlink %}
{% trans "Your email have successfully been validated." %}
{% if user_object.profile.registration_valid %}
{% blocktrans %}You can now <a href="{{ login_url }}">log in</a>.{% endblocktrans %}
{% else %}
{% trans "You must pay now your membership in the Kfet to complete your registration." %}
{% endif %}
{% else %}
{% trans "The link was invalid. The token may have expired. Please send us an email to activate your account." %}
{% endif %}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends "base.html" %}
{% block content %}
<h2>Account Activation</h2>
An email has been sent. Please click on the link to activate your account.
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<p>{% trans "Thanks for spending some quality time with the Web site today." %}</p>
<p><a href="{% url 'index' %}">{% trans 'Log in again' %}</a></p>
{% endblock %}

View File

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-2.0-or-later
{% endcomment %}
{% load i18n crispy_forms_filters %}
{% block title %}{% trans "Log in" %}{% endblock %}
{% block contenttitle %}<h1>{% trans "Log in" %}</h1>{% endblock %}
{% block content %}
{% if user.is_authenticated %}
<p class="errornote">
{% blocktrans trimmed %}
You are authenticated as {{ user }}, but are not authorized to
access this page. Would you like to login to a different account?
{% endblocktrans %}
</p>
{% endif %}
<form method="post" id="login-form">
{% csrf_token %}
{{ form | crispy }}
<input type="submit" value="{% trans 'Log in' %}" class="btn btn-primary">
<a href="{% url 'password_reset' %}" class="badge badge-light">{% trans 'Forgotten your password or username?' %}</a>
</form>
{% endblock %}

View File

@ -0,0 +1,15 @@
{% load i18n %}
{% trans "Hi" %} {{ user.username }},
{% trans "You recently registered on the Note Kfet. Please click on the link below to confirm your registration." %}
https://{{ domain }}{% url 'registration:email_validation' uidb64=uid token=token %}
{% trans "This link is only valid for a couple of days, after that you will need to contact us to validate your email." %}
{% trans "After that, you'll have to wait that someone validates your account before you can log in. You will need to pay your membership in the Kfet." %}
{% trans "Thanks" %},
{% trans "The Note Kfet team." %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<p>{% trans 'Your password was changed.' %}</p>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n crispy_forms_tags %}
{% block content %}
<form method="post">{% csrf_token %}
<p>{% trans "Please enter your old password, for security's sake, and then enter your new password twice so we can verify you typed it in correctly." %}</p>
{{ form | crispy }}
<input class="btn btn-primary" type="submit" value="{% trans 'Change my password' %}">
</form>
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% comment %}
SPDX-License-Identifier: GPL-3.0-or-later
{% endcomment %}
{% load i18n %}
{% block content %}
<p>{% trans "Your password has been set. You may go ahead and log in now." %}</p>
<p>
<a href="{{ login_url }}" class="btn btn-success">{% trans 'Log in' %}</a>
</p>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More