{{ title }}
{% endblock %} + + {% block content %} +Default content...
+ {% endblock content %} +diff --git a/.bashrc b/.bashrc
new file mode 100644
index 0000000..654e266
--- /dev/null
+++ b/.bashrc
@@ -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:
\ No newline at end of file
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..12143a1
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,4 @@
+__pycache__
+media
+import_olddb
+db.sqlite3
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..aa0a2fc
--- /dev/null
+++ b/.gitignore
@@ -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/
diff --git a/Dockerfile b/Dockerfile
index 7bfdd64..71017de 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..a9459c3
--- /dev/null
+++ b/README.md
@@ -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.
\ No newline at end of file
diff --git a/apps/api/__init__.py b/apps/api/__init__.py
new file mode 100644
index 0000000..08884cb
--- /dev/null
+++ b/apps/api/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'api.apps.APIConfig'
diff --git a/apps/api/apps.py b/apps/api/apps.py
new file mode 100644
index 0000000..6e03468
--- /dev/null
+++ b/apps/api/apps.py
@@ -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')
diff --git a/apps/api/serializers.py b/apps/api/serializers.py
new file mode 100644
index 0000000..1685020
--- /dev/null
+++ b/apps/api/serializers.py
@@ -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__"
diff --git a/apps/api/urls.py b/apps/api/urls.py
new file mode 100644
index 0000000..b2e617f
--- /dev/null
+++ b/apps/api/urls.py
@@ -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')),
+]
diff --git a/apps/api/viewsets.py b/apps/api/viewsets.py
new file mode 100644
index 0000000..785e446
--- /dev/null
+++ b/apps/api/viewsets.py
@@ -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)
\ No newline at end of file
diff --git a/apps/member/__init__.py b/apps/member/__init__.py
new file mode 100644
index 0000000..6bb559b
--- /dev/null
+++ b/apps/member/__init__.py
@@ -0,0 +1 @@
+default_app_config = 'member.apps.MemberConfig'
diff --git a/apps/member/admin.py b/apps/member/admin.py
new file mode 100644
index 0000000..a41bb92
--- /dev/null
+++ b/apps/member/admin.py
@@ -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.
+ """
diff --git a/apps/member/apps.py b/apps/member/apps.py
new file mode 100644
index 0000000..61c9ae8
--- /dev/null
+++ b/apps/member/apps.py
@@ -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')
diff --git a/apps/member/forms.py b/apps/member/forms.py
new file mode 100644
index 0000000..083b7b4
--- /dev/null
+++ b/apps/member/forms.py
@@ -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',)
diff --git a/apps/member/management/__init__.py b/apps/member/management/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/member/management/commands/__init__.py b/apps/member/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/member/management/commands/create_su.py b/apps/member/management/commands/create_su.py
new file mode 100644
index 0000000..93ec091
--- /dev/null
+++ b/apps/member/management/commands/create_su.py
@@ -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()
diff --git a/apps/member/management/commands/extract_solutions.py b/apps/member/management/commands/extract_solutions.py
new file mode 100644
index 0000000..7b59ad1
--- /dev/null
+++ b/apps/member/management/commands/extract_solutions.py
@@ -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!"))
diff --git a/apps/member/management/commands/import_olddb.py b/apps/member/management/commands/import_olddb.py
new file mode 100644
index 0000000..d3ff94f
--- /dev/null
+++ b/apps/member/management/commands/import_olddb.py
@@ -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"))
diff --git a/apps/member/migrations/__init__.py b/apps/member/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/member/models.py b/apps/member/models.py
new file mode 100644
index 0000000..82da7dd
--- /dev/null
+++ b/apps/member/models.py
@@ -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")
diff --git a/apps/member/tables.py b/apps/member/tables.py
new file mode 100644
index 0000000..779dc47
--- /dev/null
+++ b/apps/member/tables.py
@@ -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'
+ }
diff --git a/apps/member/templatetags/__init__.py b/apps/member/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/apps/member/templatetags/getconfig.py b/apps/member/templatetags/getconfig.py
new file mode 100644
index 0000000..0c6d776
--- /dev/null
+++ b/apps/member/templatetags/getconfig.py
@@ -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)
diff --git a/apps/member/urls.py b/apps/member/urls.py
new file mode 100644
index 0000000..073f057
--- /dev/null
+++ b/apps/member/urls.py
@@ -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/ Default content...
+ Le CNO vous adresse le message suivant :
+ %(request_path)s
was not found on the server."
+msgstr ""
+"Le chemin demandé %(request_path)s
n'a pas été trouvé sur le "
+"serveur."
+
+#: templates/500.html:6
+msgid "Server error"
+msgstr "Erreur du serveur"
+
+#: templates/500.html:7
+msgid ""
+"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."
+msgstr ""
+"Désolé, votre requête comporte une erreur. Aucune idée de ce qui a pu se "
+"passer. Un email a été envoyé au développeur avec les détails de l'erreur. "
+"Vous pouvez désormais aller chercher une bière."
+
+#: templates/base.html:11
+msgid "The inscription site of the TFJM²."
+msgstr "Le site d'inscription au TFJM²."
+
+#: templates/base.html:73
+msgid "Home"
+msgstr "Accueil"
+
+#: templates/base.html:76
+msgid "Tournament list"
+msgstr "Liste des tournois"
+
+#: templates/base.html:89
+msgid "My account"
+msgstr "Mon compte"
+
+#: templates/base.html:94
+msgid "Add a team"
+msgstr "Ajouter une équipe"
+
+#: templates/base.html:97
+msgid "Join a team"
+msgstr "Rejoindre une équipe"
+
+#: templates/base.html:101
+msgid "My team"
+msgstr "Mon équipe"
+
+#: templates/base.html:144
+msgid "Make a gift"
+msgstr "Faire un don"
+
+#: templates/base.html:148
+msgid "Administration"
+msgstr "Administration"
+
+#: templates/base.html:155
+msgid "Return to admin view"
+msgstr "Retour à l'interface administrateur"
+
+#: templates/base.html:160 templates/registration/login.html:7
+#: templates/registration/login.html:8 templates/registration/login.html:22
+#: templates/registration/password_reset_complete.html:10
+msgid "Log in"
+msgstr "Connexion"
+
+#: templates/base.html:163 templates/registration/signup.html:5
+#: templates/registration/signup.html:8 templates/registration/signup.html:14
+msgid "Sign up"
+msgstr "S'inscrire"
+
+#: templates/base.html:167
+msgid "Log out"
+msgstr "Déconnexion"
+
+#: templates/django_filters/rest_framework/crispy_form.html:4
+#: templates/django_filters/rest_framework/form.html:2
+msgid "Field filters"
+msgstr "Filtres"
+
+#: templates/django_filters/rest_framework/form.html:5
+#: templates/member/my_account.html:9 templates/tournament/add_organizer.html:9
+#: templates/tournament/pool_form.html:9
+#: templates/tournament/solutions_list.html:24
+#: templates/tournament/syntheses_list.html:40
+#: templates/tournament/team_form.html:9
+#: templates/tournament/tournament_form.html:9
+msgid "Submit"
+msgstr "Envoyer"
+
+#: templates/member/my_account.html:14
+msgid "Update my password"
+msgstr "Changer mon mot de passe"
+
+#: templates/member/profile_list.html:9
+msgid "Add an organizer"
+msgstr "Ajouter un organisateur"
+
+#: templates/member/tfjmuser_detail.html:12
+msgid "role"
+msgstr "rôle"
+
+#: templates/member/tfjmuser_detail.html:47
+msgid "class"
+msgstr "classe"
+
+#: templates/member/tfjmuser_detail.html:76
+#: templates/tournament/team_detail.html:129
+msgid "Documents"
+msgstr "Documents"
+
+#: templates/member/tfjmuser_detail.html:84
+#, python-format
+msgid "View site as %(tfjmuser)s"
+msgstr "Voir le site en tant que %(tfjmuser)s"
+
+#: templates/registration/email_validation_complete.html:6
+msgid "Your email have successfully been validated."
+msgstr "Votre adresse e-mail a bien été validée."
+
+#: templates/registration/email_validation_complete.html:8
+#, python-format
+msgid "You can now log in."
+msgstr "Vous pouvez désormais vous connecter"
+
+#: templates/registration/email_validation_complete.html:10
+msgid ""
+"You must pay now your membership in the Kfet to complete your registration."
+msgstr ""
+
+#: templates/registration/email_validation_complete.html:13
+msgid ""
+"The link was invalid. The token may have expired. Please send us an email to "
+"activate your account."
+msgstr ""
+"Le lien est invalide. Le jeton a du expirer. Merci de nous envoyer un mail "
+"afin d'activer votre compte."
+
+#: templates/registration/logged_out.html:8
+msgid "Thanks for spending some quality time with the Web site today."
+msgstr "Merci d'avoir utilisé la plateforme du TFJM²."
+
+#: templates/registration/logged_out.html:9
+msgid "Log in again"
+msgstr "Se connecter à nouveau"
+
+#: templates/registration/login.html:13
+#, python-format
+msgid ""
+"You are authenticated as %(user)s, but are not authorized to access this "
+"page. Would you like to login to a different account?"
+msgstr ""
+"Vous êtes déjà connecté sous le nom %(user)s, mais vous n'êtes pas autorisés "
+"à accéder à cette page. Souhaitez-vous vous connecter sous un compte "
+"différent ?"
+
+#: templates/registration/login.html:23
+msgid "Forgotten your password or username?"
+msgstr "Mot de passe oublié ?"
+
+#: templates/registration/mails/email_validation_email.html:3
+msgid "Hi"
+msgstr "Bonjour"
+
+#: templates/registration/mails/email_validation_email.html:5
+msgid ""
+"You recently registered on the Note Kfet. Please click on the link below to "
+"confirm your registration."
+msgstr ""
+
+#: templates/registration/mails/email_validation_email.html:9
+msgid ""
+"This link is only valid for a couple of days, after that you will need to "
+"contact us to validate your email."
+msgstr ""
+
+#: templates/registration/mails/email_validation_email.html:11
+msgid ""
+"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."
+msgstr ""
+
+#: templates/registration/mails/email_validation_email.html:13
+msgid "Thanks"
+msgstr "Merci"
+
+#: templates/registration/mails/email_validation_email.html:15
+msgid "The Note Kfet team."
+msgstr ""
+
+#: templates/registration/password_change_done.html:8
+msgid "Your password was changed."
+msgstr "Votre mot de passe a été changé"
+
+#: templates/registration/password_change_form.html:9
+msgid ""
+"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."
+msgstr ""
+"Veuillez entrer votre ancien mot de passe, pour des raisons de sécurité, "
+"puis entrer votre mot de passe deux fois afin de vérifier que vous l'avez "
+"tapé correctement."
+
+#: templates/registration/password_change_form.html:11
+#: templates/registration/password_reset_confirm.html:12
+msgid "Change my password"
+msgstr "Changer mon mot de passe"
+
+#: templates/registration/password_reset_complete.html:8
+msgid "Your password has been set. You may go ahead and log in now."
+msgstr "Votre mot de passe a été changé. Vous pouvez désormais vous connecter."
+
+#: templates/registration/password_reset_confirm.html:9
+msgid ""
+"Please enter your new password twice so we can verify you typed it in "
+"correctly."
+msgstr ""
+"Veuillez taper votre nouveau mot de passe deux fois afin de s'assurer que "
+"vous l'ayez tapé correctement."
+
+#: templates/registration/password_reset_confirm.html:15
+msgid ""
+"The password reset link was invalid, possibly because it has already been "
+"used. Please request a new password reset."
+msgstr ""
+"Le lien de réinitialisation du mot de passe est invalide, sans doute parce "
+"qu'il a été déjà utilisé. Veuillez demander une nouvelle demande de "
+"réinitialisation."
+
+#: templates/registration/password_reset_done.html:8
+msgid ""
+"We've emailed you instructions for setting your password, if an account "
+"exists with the email you entered. You should receive them shortly."
+msgstr ""
+"Nous vous avons envoyé des instructions pour réinitialiser votre mot de "
+"passe, si un compte existe avec l'adresse email entrée. Vous devriez les "
+"recevoir d'ici peu."
+
+#: templates/registration/password_reset_done.html:9
+msgid ""
+"If you don't receive an email, please make sure you've entered the address "
+"you registered with, and check your spam folder."
+msgstr ""
+"Si vous n'avez pas reçu d'email, merci de vérifier que vous avez entré "
+"l'adresse avec laquelle vous êtes inscrits, et vérifier vos spams."
+
+#: templates/registration/password_reset_form.html:8
+msgid ""
+"Forgotten your password? Enter your email address below, and we'll email "
+"instructions for setting a new one."
+msgstr ""
+"Mot de passe oublié ? Entrez votre adresse email ci-dessous, et nous vous "
+"enverrons des instructions pour en définir un nouveau."
+
+#: templates/registration/password_reset_form.html:11
+msgid "Reset my password"
+msgstr "Réinitialiser mon mot de passe"
+
+#: templates/tournament/pool_detail.html:36
+msgid "Solutions will be available here for teams from:"
+msgstr "Les solutions seront disponibles ici pour les équipes à partir du :"
+
+#: templates/tournament/pool_detail.html:49
+#: templates/tournament/pool_detail.html:73
+msgid "Download ZIP archive"
+msgstr "Télécharger l'archive ZIP"
+
+#: templates/tournament/pool_detail.html:61
+#: templates/tournament/syntheses_list.html:7
+msgid "Templates for syntheses are available here:"
+msgstr "Le modèle de note de synthèse est disponible ici :"
+
+#: templates/tournament/pool_detail.html:83
+msgid "Pool list"
+msgstr "Liste des poules"
+
+#: templates/tournament/pool_detail.html:89
+msgid ""
+"Give this link to juries to access this page (warning: should stay "
+"confidential and only given to juries of this pool):"
+msgstr ""
+"Donnez ce lien aux jurys pour leur permettre d'accéder à cette page "
+"(attention : ce lien doit rester confidentiel et ne doit être donné "
+"exclusivement qu'à des jurys) :"
+
+#: templates/tournament/pool_list.html:10
+msgid "Add pool"
+msgstr "Ajouter une poule"
+
+#: templates/tournament/solutions_list.html:9
+#, python-format
+msgid "You can upload your solutions until %(deadline)s."
+msgstr "Vous pouvez envoyer vos solutions jusqu'au %(deadline)s."
+
+#: templates/tournament/solutions_list.html:14
+msgid ""
+"The deadline to send your solutions is reached. However, you have an extra "
+"time of 30 minutes to send your papers, no panic :)"
+msgstr ""
+"La date limite pour envoyer vos solutions est dépassée. Toutefois, vous avez "
+"droit à un délai supplémentaire de 30 minutes pour envoyer vos papiers, pas "
+"de panique :)"
+
+#: templates/tournament/solutions_list.html:16
+msgid "You can't upload your solutions anymore."
+msgstr "Vous ne pouvez plus publier vos solutions."
+
+#: templates/tournament/solutions_orga_list.html:14
+#: templates/tournament/syntheses_orga_list.html:14
+#, python-format
+msgid "%(tournament)s — ZIP"
+msgstr "%(tournament)s — ZIP"
+
+#: templates/tournament/syntheses_list.html:14
+#: templates/tournament/syntheses_list.html:26
+#, python-format
+msgid "You can upload your syntheses for round %(round)s until %(deadline)s."
+msgstr ""
+"Vous pouvez envoyer vos notes de synthèses pour le tour %(round)s jusqu'au "
+"%(deadline)s."
+
+#: templates/tournament/syntheses_list.html:18
+#: templates/tournament/syntheses_list.html:30
+#, python-format
+msgid ""
+"The deadline to send your syntheses for the round %(round)s is reached. "
+"However, you have an extra time of 30 minutes to send your papers, no "
+"panic :)"
+msgstr ""
+"La date limite pour envoyer vos notes de synthèses pour le tour %(round)s "
+"est dépassée. Toutefois, vous avez droit à un délai supplémentaire de 30 "
+"minutes pour envoyer vos papiers, pas de panique :)"
+
+#: templates/tournament/syntheses_list.html:22
+#: templates/tournament/syntheses_list.html:34
+#, python-format
+msgid "You can't upload your syntheses for the round %(round)s anymore."
+msgstr ""
+"Vous ne pouvez plus publier vos notes de synthèses pour le tour %(round)s."
+
+#: templates/tournament/team_detail.html:8
+msgid "Team"
+msgstr "Équipe"
+
+#: templates/tournament/team_detail.html:25
+msgid "coachs"
+msgstr "encadrants"
+
+#: templates/tournament/team_detail.html:28
+msgid "participants"
+msgstr "participants"
+
+#: templates/tournament/team_detail.html:39
+msgid "Send a mail to people in this team"
+msgstr "Envoyer un mail à toutes les personnes de cette équipe"
+
+#: templates/tournament/team_detail.html:49
+msgid "Edit team"
+msgstr "Modifier l'équipe"
+
+#: templates/tournament/team_detail.html:53
+msgid "Select for final"
+msgstr "Sélectionner pour la finale"
+
+#: templates/tournament/team_detail.html:59
+msgid "Delete team"
+msgstr "Supprimer l'équipe"
+
+#: templates/tournament/team_detail.html:61
+msgid "Leave this team"
+msgstr "Quitter l'équipe"
+
+#: templates/tournament/team_detail.html:105
+msgid "The team is waiting about validation."
+msgstr "L'équipe est en attente de validation"
+
+#: templates/tournament/team_detail.html:112
+msgid "Message addressed to the team:"
+msgstr "Message adressé à l'équipe :"
+
+#: templates/tournament/team_detail.html:114
+msgid "Message..."
+msgstr "Message ..."
+
+#: templates/tournament/team_detail.html:119
+msgid "Invalidate team"
+msgstr "Invalider l'équipe"
+
+#: templates/tournament/team_detail.html:120
+msgid "Validate team"
+msgstr "Valider l'équipe"
+
+#: templates/tournament/team_detail.html:133
+msgid "Motivation letter:"
+msgstr "Lettre de motivation :"
+
+#: templates/tournament/team_detail.html:152
+msgid "Download solutions as ZIP"
+msgstr "Télécharger les solutions en archive ZIP"
+
+#: templates/tournament/tournament_detail.html:22
+msgid "Free"
+msgstr "Gratuit"
+
+#: templates/tournament/tournament_detail.html:25
+msgid "From"
+msgstr "Du"
+
+#: templates/tournament/tournament_detail.html:25
+msgid "to"
+msgstr "à"
+
+#: templates/tournament/tournament_detail.html:48
+msgid "Send a mail to all people in this tournament"
+msgstr "Envoyer un mail à toutes les personnes du tournoi"
+
+#: templates/tournament/tournament_detail.html:49
+msgid "Send a mail to all people in this tournament that are in a valid team"
+msgstr ""
+"Envoyer un mail à toutes les personnes du tournoi dans une équipe valide"
+
+#: templates/tournament/tournament_detail.html:56
+msgid "Edit tournament"
+msgstr "Modifier le tournoi"
+
+#: templates/tournament/tournament_detail.html:63
+msgid "Teams"
+msgstr "Équipes"
+
+#: templates/tournament/tournament_list.html:8
+msgid "Send a mail to all people that are in a team"
+msgstr "Envoyer un mail à toutes les personnes dans une équipe"
+
+#: templates/tournament/tournament_list.html:9
+msgid "Send a mail to all people that are in a valid team"
+msgstr "Envoyer un mail à toutes les personnes dans une équipe validée"
+
+#: templates/tournament/tournament_list.html:15
+msgid "Add a tournament"
+msgstr "Ajouter un tournoi"
+
+#: tfjm/settings.py:147
+msgid "English"
+msgstr "Anglais"
+
+#: tfjm/settings.py:148
+msgid "French"
+msgstr "Français"
diff --git a/manage.py b/manage.py
new file mode 100755
index 0000000..2bf0cbf
--- /dev/null
+++ b/manage.py
@@ -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()
diff --git a/nginx_tfjm.conf b/nginx_tfjm.conf
new file mode 100644
index 0000000..be143ce
--- /dev/null
+++ b/nginx_tfjm.conf
@@ -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/;
+ }
+}
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4338302
--- /dev/null
+++ b/requirements.txt
@@ -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
\ No newline at end of file
diff --git a/static/Autorisation_droit_image_majeur.tex b/static/Autorisation_droit_image_majeur.tex
new file mode 100644
index 0000000..7cb1727
--- /dev/null
+++ b/static/Autorisation_droit_image_majeur.tex
@@ -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 d’utiliser 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 d’une \'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}
diff --git a/static/Autorisation_droit_image_mineur.tex b/static/Autorisation_droit_image_mineur.tex
new file mode 100644
index 0000000..4f14a43
--- /dev/null
+++ b/static/Autorisation_droit_image_mineur.tex
@@ -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 d’utiliser 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 l’enfant.\\
+
+\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 d’une \'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}
diff --git a/static/Autorisation_parentale.tex b/static/Autorisation_parentale.tex
new file mode 100644
index 0000000..6c56ac4
--- /dev/null
+++ b/static/Autorisation_parentale.tex
@@ -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}
diff --git a/static/Fiche synthèse.pdf b/static/Fiche synthèse.pdf
new file mode 100644
index 0000000..af8ed1c
Binary files /dev/null and b/static/Fiche synthèse.pdf differ
diff --git a/static/Fiche synthèse.tex b/static/Fiche synthèse.tex
new file mode 100644
index 0000000..bc2daa9
--- /dev/null
+++ b/static/Fiche synthèse.tex
@@ -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}
diff --git a/static/Fiche_sanitaire.pdf b/static/Fiche_sanitaire.pdf
new file mode 100644
index 0000000..b828b9d
Binary files /dev/null and b/static/Fiche_sanitaire.pdf differ
diff --git a/static/Instructions.tex b/static/Instructions.tex
new file mode 100644
index 0000000..da293ef
--- /dev/null
+++ b/static/Instructions.tex
@@ -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}
diff --git a/static/bootstrap_datepicker_plus/css/datepicker-widget.css b/static/bootstrap_datepicker_plus/css/datepicker-widget.css
new file mode 100644
index 0000000..baeec50
--- /dev/null
+++ b/static/bootstrap_datepicker_plus/css/datepicker-widget.css
@@ -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;
+}
diff --git a/static/bootstrap_datepicker_plus/js/datepicker-widget.js b/static/bootstrap_datepicker_plus/js/datepicker-widget.js
new file mode 100644
index 0000000..2288b46
--- /dev/null
+++ b/static/bootstrap_datepicker_plus/js/datepicker-widget.js
@@ -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');
+ });
+ }
+});
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..97757d3
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/logo.svg b/static/logo.svg
new file mode 100644
index 0000000..699316b
--- /dev/null
+++ b/static/logo.svg
@@ -0,0 +1,114 @@
+
+
diff --git a/static/logo_animath.png b/static/logo_animath.png
new file mode 100644
index 0000000..da4533e
Binary files /dev/null and b/static/logo_animath.png differ
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..5c8d3ff
--- /dev/null
+++ b/static/style.css
@@ -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;
+}
diff --git a/templates/400.html b/templates/400.html
new file mode 100644
index 0000000..3560652
--- /dev/null
+++ b/templates/400.html
@@ -0,0 +1,8 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+
+{% block content %}
+ {% trans "Bad request" %}
+ {% 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 %}
\ No newline at end of file
diff --git a/templates/403.html b/templates/403.html
new file mode 100644
index 0000000..317865f
--- /dev/null
+++ b/templates/403.html
@@ -0,0 +1,13 @@
+{% extends "base.html" %}
+
+{% load i18n %}
+
+{% block content %}
+ {% trans "Permission denied" %}
+ {% blocktrans %}You don't have the right to perform this request.{% endblocktrans %}
+ {% if exception %}
+ {% trans "Page not found" %}
+ {% blocktrans %}The requested path {{ request_path }}
was not found on the server.{% endblocktrans %}
+ {% if exception != "Resolver404" %}
+ {% trans "Server error" %}
+ {% 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 %}
diff --git a/templates/amount_input.html b/templates/amount_input.html
new file mode 100644
index 0000000..6ef4a53
--- /dev/null
+++ b/templates/amount_input.html
@@ -0,0 +1,11 @@
+
+
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..d52e7be
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,234 @@
+{% load static i18n static getconfig %}
+
+
+
+
+
+
+ {{ title }}
{% endblock %}
+
+ {% block content %}
+ {% trans "Field filters" %}
+{% crispy filter.form %}
diff --git a/templates/django_filters/rest_framework/form.html b/templates/django_filters/rest_framework/form.html
new file mode 100644
index 0000000..b116e35
--- /dev/null
+++ b/templates/django_filters/rest_framework/form.html
@@ -0,0 +1,6 @@
+{% load i18n %}
+{% trans "Field filters" %}
+
diff --git a/templates/django_filters/widgets/multiwidget.html b/templates/django_filters/widgets/multiwidget.html
new file mode 100644
index 0000000..089ddb2
--- /dev/null
+++ b/templates/django_filters/widgets/multiwidget.html
@@ -0,0 +1 @@
+{% for widget in widget.subwidgets %}{% include widget.template_name %}{% if forloop.first %}-{% endif %}{% endfor %}
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..9455dc1
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,9 @@
+{% extends "base.html" %}
+
+{% load getconfig %}
+
+{% block content %}
+ {% autoescape off %}
+ {{ "index_page"|get_config|safe }}
+ {% endautoescape %}
+{% endblock %}
diff --git a/templates/mail_templates/add_organizer.html b/templates/mail_templates/add_organizer.html
new file mode 100644
index 0000000..06cc500
--- /dev/null
+++ b/templates/mail_templates/add_organizer.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+Vous recevez ce message (envoyé automatiquement) car vous êtes organisateur d'un des tournois du TFJM2.
+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.
+
+Merci beaucoup pour votre aide !
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/add_organizer.txt b/templates/mail_templates/add_organizer.txt
new file mode 100644
index 0000000..2a4cde4
--- /dev/null
+++ b/templates/mail_templates/add_organizer.txt
@@ -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²
diff --git a/templates/mail_templates/add_organizer_for_tournament.html b/templates/mail_templates/add_organizer_for_tournament.html
new file mode 100644
index 0000000..ad9aa90
--- /dev/null
+++ b/templates/mail_templates/add_organizer_for_tournament.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+Vous venez d'être promu organisateur du tournoi {TOURNAMENT_NAME} du TFJM2 {YEAR}.
+Ce message vous a été envoyé automatiquement. En cas de problème, merci de répondre à ce message.
+
+Avec toute notre bienveillance,
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/add_team.html b/templates/mail_templates/add_team.html
new file mode 100644
index 0000000..bb69db0
--- /dev/null
+++ b/templates/mail_templates/add_team.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+Vous venez de créer l'équipe « {TEAM_NAME} » ({TRIGRAM}) pour le TFJM2 de {TOURNAMENT_NAME} et nous vous en remercions.
+Afin de permettre aux autres membres de votre équipe de vous rejoindre, veuillez leur transmettre le code d'accès :
+{ACCESS_CODE}
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/change_email_address.html b/templates/mail_templates/change_email_address.html
new file mode 100644
index 0000000..d04ed90
--- /dev/null
+++ b/templates/mail_templates/change_email_address.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+Vous venez de changer votre adresse e-mail. Veuillez désormais la confirmer en cliquant ici : {URL_BASE}/confirmer_mail/{TOKEN}
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/change_password.html b/templates/mail_templates/change_password.html
new file mode 100644
index 0000000..673e80f
--- /dev/null
+++ b/templates/mail_templates/change_password.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+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.
+
+Avec toute notre bienveillance,
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/confirm_email.html b/templates/mail_templates/confirm_email.html
new file mode 100644
index 0000000..d247377
--- /dev/null
+++ b/templates/mail_templates/confirm_email.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+Vous êtes inscrit au TFJM2 {YEAR} et nous vous en remercions.
+Pour valider votre adresse e-mail, veuillez cliquer sur le lien : {URL_BASE}/confirmer_mail/{TOKEN}
+
+Avec toute notre bienveillance,
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/forgotten_password.html b/templates/mail_templates/forgotten_password.html
new file mode 100644
index 0000000..717cc8c
--- /dev/null
+++ b/templates/mail_templates/forgotten_password.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+Vous avez indiqué avoir oublié votre mot de passe. Veuillez cliquer ici pour le réinitialiser : {URL_BASE}/connexion/reinitialiser_mdp/{TOKEN}
+
+Si vous n'êtes pas à l'origine de cette manipulation, vous pouvez ignorer ce message.
+
+Avec toute notre bienveillance,
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/join_team.html b/templates/mail_templates/join_team.html
new file mode 100644
index 0000000..d5628c0
--- /dev/null
+++ b/templates/mail_templates/join_team.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+Vous venez de rejoindre l'équipe « {TEAM_NAME} » ({TRIGRAM}) pour le TFJM² de {TOURNAMENT_NAME} et nous vous en
+remercions.
+
+Avec toute notre bienveillance,
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/register.html b/templates/mail_templates/register.html
new file mode 100644
index 0000000..bc4123b
--- /dev/null
+++ b/templates/mail_templates/register.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+Vous venez de vous inscrire au TFJM2 {YEAR} et nous vous en remercions.
+Pour valider votre adresse e-mail, veuillez cliquer sur le lien : {URL_BASE}/confirmer_mail/{TOKEN}
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/request_payment_validation.html b/templates/mail_templates/request_payment_validation.html
new file mode 100644
index 0000000..913e490
--- /dev/null
+++ b/templates/mail_templates/request_payment_validation.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+{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 :
+Équipe : {TEAM_NAME} ({TRIGRAM})
+Tournoi : {TOURNAMENT_NAME}
+Moyen de paiement : {PAYMENT_METHOD}
+Montant : {AMOUNT} €
+Informations sur le paiement : {PAYMENT_INFOS}
+
+Vous pouvez désormais vérifier ces informations, puis valider (ou non) le paiement sur
+la page associée à ce participant.
+
+Avec toute notre bienveillance,
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/request_validation.html b/templates/mail_templates/request_validation.html
new file mode 100644
index 0000000..7a05122
--- /dev/null
+++ b/templates/mail_templates/request_validation.html
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+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 TFJM2
+
+
diff --git a/templates/mail_templates/request_validation.txt b/templates/mail_templates/request_validation.txt
new file mode 100644
index 0000000..88d463b
--- /dev/null
+++ b/templates/mail_templates/request_validation.txt
@@ -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²
diff --git a/templates/mail_templates/select_for_final.html b/templates/mail_templates/select_for_final.html
new file mode 100644
index 0000000..383b527
--- /dev/null
+++ b/templates/mail_templates/select_for_final.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+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²
+
+
\ No newline at end of file
diff --git a/templates/mail_templates/select_for_final.txt b/templates/mail_templates/select_for_final.txt
new file mode 100644
index 0000000..a000c22
--- /dev/null
+++ b/templates/mail_templates/select_for_final.txt
@@ -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²
diff --git a/templates/mail_templates/unvalidate_payment.html b/templates/mail_templates/unvalidate_payment.html
new file mode 100644
index 0000000..7273282
--- /dev/null
+++ b/templates/mail_templates/unvalidate_payment.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+Votre paiement pour le TFJM² {YEAR} a malheureusement été rejeté. Pour rappel, vous aviez fourni ces informations :
+Équipe : {TEAM_NAME} ({TRIGRAM})
+Tournoi : {TOURNAMENT_NAME}
+Moyen de paiement : {PAYMENT_METHOD}
+Montant : {AMOUNT} €
+Informations sur le paiement : {PAYMENT_INFOS}
+
+{MESSAGE}
+
+Avec toute notre bienveillance,
+
+Le comité national d'organisation du TFJM2
+
+
diff --git a/templates/mail_templates/unvalidate_team.html b/templates/mail_templates/unvalidate_team.html
new file mode 100644
index 0000000..1ba3ce8
--- /dev/null
+++ b/templates/mail_templates/unvalidate_team.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+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 : +
{% trans "Thanks for spending some quality time with the Web site today." %}
+ +{% endblock %} diff --git a/templates/registration/login.html b/templates/registration/login.html new file mode 100644 index 0000000..64c5c26 --- /dev/null +++ b/templates/registration/login.html @@ -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 %}+ {% 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 %} +
+ {% endif %} + +{% endblock %} diff --git a/templates/registration/mails/email_validation_email.html b/templates/registration/mails/email_validation_email.html new file mode 100644 index 0000000..577c122 --- /dev/null +++ b/templates/registration/mails/email_validation_email.html @@ -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." %} diff --git a/templates/registration/password_change_done.html b/templates/registration/password_change_done.html new file mode 100644 index 0000000..150a00e --- /dev/null +++ b/templates/registration/password_change_done.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n %} + +{% block content %} +{% trans 'Your password was changed.' %}
+{% endblock %} diff --git a/templates/registration/password_change_form.html b/templates/registration/password_change_form.html new file mode 100644 index 0000000..01133e4 --- /dev/null +++ b/templates/registration/password_change_form.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} + +{% endblock %} \ No newline at end of file diff --git a/templates/registration/password_reset_complete.html b/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..bb91a3c --- /dev/null +++ b/templates/registration/password_reset_complete.html @@ -0,0 +1,12 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n %} + +{% block content %} +{% trans "Your password has been set. You may go ahead and log in now." %}
+ +{% endblock %} diff --git a/templates/registration/password_reset_confirm.html b/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000..5db0e81 --- /dev/null +++ b/templates/registration/password_reset_confirm.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} + {% if validlink %} +{% trans "Please enter your new password twice so we can verify you typed it in correctly." %}
+ + {% else %} +{% trans "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}
+ {% endif %} +{% endblock %} diff --git a/templates/registration/password_reset_done.html b/templates/registration/password_reset_done.html new file mode 100644 index 0000000..a215ab9 --- /dev/null +++ b/templates/registration/password_reset_done.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n %} + +{% block content %} +{% trans "We've emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly." %}
+{% trans "If you don't receive an email, please make sure you've entered the address you registered with, and check your spam folder." %}
+{% endblock %} diff --git a/templates/registration/password_reset_form.html b/templates/registration/password_reset_form.html new file mode 100644 index 0000000..61adaa9 --- /dev/null +++ b/templates/registration/password_reset_form.html @@ -0,0 +1,13 @@ +{% extends "base.html" %} +{% comment %} +SPDX-License-Identifier: GPL-3.0-or-later +{% endcomment %} +{% load i18n crispy_forms_tags %} + +{% block content %} +{% trans "Forgotten your password? Enter your email address below, and we'll email instructions for setting a new one." %}
+ +{% endblock %} diff --git a/templates/registration/signup.html b/templates/registration/signup.html new file mode 100644 index 0000000..ed100d0 --- /dev/null +++ b/templates/registration/signup.html @@ -0,0 +1,17 @@ + +{% extends 'base.html' %} +{% load crispy_forms_filters %} +{% load i18n %} +{% block title %}{% trans "Sign up" %}{% endblock %} + +{% block content %} +