8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-22 03:13:12 +00:00

Merge branch 'choose_password_during_user_creation' into 'dev'

Allow users to choose password during account creation

See merge request re2o/re2o!496
This commit is contained in:
chirac 2020-04-18 01:46:22 +02:00
commit f8b6044938
37 changed files with 1689 additions and 776 deletions

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-19 23:43+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2019-01-07 01:37+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-20 01:24+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language: fr_FR\n"
@ -37,7 +37,7 @@ msgstr "Vous n'avez pas le droit de voir cette application."
msgid "Select a payment method"
msgstr "Sélectionnez un moyen de paiement"
#: cotisations/forms.py:73 cotisations/models.py:676
#: cotisations/forms.py:73 cotisations/models.py:682
msgid "Member"
msgstr "Adhérent"
@ -154,8 +154,8 @@ msgstr "Peut voir un objet facture"
msgid "Can edit all the previous invoices"
msgstr "Peut modifier toutes les factures précédentes"
#: cotisations/models.py:145 cotisations/models.py:431 cotisations/views.py:378
#: cotisations/views.py:573
#: cotisations/models.py:145 cotisations/models.py:456 cotisations/views.py:376
#: cotisations/views.py:571
msgid "invoice"
msgstr "facture"
@ -217,115 +217,115 @@ msgstr "Il n'y a pas de moyens de paiement que vous puissiez utiliser."
msgid "There are no articles that you can buy."
msgstr "Il n'y a pas d'articles que vous puissiez acheter."
#: cotisations/models.py:347
#: cotisations/models.py:372
msgid "Can view a custom invoice object"
msgstr "Peut voir un objet facture personnalisée"
#: cotisations/models.py:349
#: cotisations/models.py:374
msgid "recipient"
msgstr "destinataire"
#: cotisations/models.py:350
#: cotisations/models.py:375
msgid "payment type"
msgstr "type de paiement"
#: cotisations/models.py:351
#: cotisations/models.py:376
msgid "address"
msgstr "adresse"
#: cotisations/models.py:352
#: cotisations/models.py:377
msgid "paid"
msgstr "payé"
#: cotisations/models.py:353
#: cotisations/models.py:378
msgid "remark"
msgstr "remarque"
#: cotisations/models.py:358
#: cotisations/models.py:383
msgid "Can view a cost estimate object"
msgstr "Peut voir un objet devis"
#: cotisations/models.py:361
#: cotisations/models.py:386
msgid "period of validity"
msgstr "période de validité"
#: cotisations/models.py:396
#: cotisations/models.py:421
msgid "You don't have the right to delete a cost estimate."
msgstr "Vous n'avez pas le droit de supprimer un devis."
#: cotisations/models.py:402
#: cotisations/models.py:427
msgid "The cost estimate has an invoice and can't be deleted."
msgstr "Le devis a une facture et ne peut pas être supprimé."
#: cotisations/models.py:424 cotisations/models.py:682
#: cotisations/models.py:940
#: cotisations/models.py:449 cotisations/models.py:688
#: cotisations/models.py:946
msgid "Connection"
msgstr "Connexion"
#: cotisations/models.py:425 cotisations/models.py:683
#: cotisations/models.py:941
#: cotisations/models.py:450 cotisations/models.py:689
#: cotisations/models.py:947
msgid "Membership"
msgstr "Adhésion"
#: cotisations/models.py:426 cotisations/models.py:678
#: cotisations/models.py:684 cotisations/models.py:942
#: cotisations/models.py:451 cotisations/models.py:684
#: cotisations/models.py:690 cotisations/models.py:948
msgid "Both of them"
msgstr "Les deux"
#: cotisations/models.py:435
#: cotisations/models.py:460
msgid "amount"
msgstr "montant"
#: cotisations/models.py:438
#: cotisations/models.py:463
msgid "article"
msgstr "article"
#: cotisations/models.py:441
#: cotisations/models.py:466
msgid "price"
msgstr "prix"
#: cotisations/models.py:444 cotisations/models.py:696
#: cotisations/models.py:469 cotisations/models.py:702
msgid "duration (in months)"
msgstr "durée (en mois)"
#: cotisations/models.py:450 cotisations/models.py:702
#: cotisations/models.py:475 cotisations/models.py:708
msgid "duration (in days, will be added to duration in months)"
msgstr "durée (en jours, sera ajoutée à la durée en mois)"
#: cotisations/models.py:458 cotisations/models.py:716
#: cotisations/models.py:953
#: cotisations/models.py:483 cotisations/models.py:722
#: cotisations/models.py:959
msgid "subscription type"
msgstr "type de cotisation"
#: cotisations/models.py:463
#: cotisations/models.py:488
msgid "Can view a purchase object"
msgstr "Peut voir un objet achat"
#: cotisations/models.py:464
#: cotisations/models.py:489
msgid "Can edit all the previous purchases"
msgstr "Peut modifier tous les achats précédents"
#: cotisations/models.py:466 cotisations/models.py:947
#: cotisations/models.py:491 cotisations/models.py:953
msgid "purchase"
msgstr "achat"
#: cotisations/models.py:467
#: cotisations/models.py:492
msgid "purchases"
msgstr "achats"
#: cotisations/models.py:539 cotisations/models.py:736
#: cotisations/models.py:545 cotisations/models.py:742
msgid "Duration must be specified for a subscription."
msgstr "La durée doit être renseignée pour une cotisation."
#: cotisations/models.py:550
#: cotisations/models.py:556
msgid "You don't have the right to edit a purchase."
msgstr "Vous n'avez pas le droit de modifier un achat."
#: cotisations/models.py:556
#: cotisations/models.py:562
msgid "You don't have the right to edit this user's purchases."
msgstr "Vous n'avez pas le droit de modifier les achats de cet utilisateur."
#: cotisations/models.py:565
#: cotisations/models.py:571
msgid ""
"You don't have the right to edit a purchase already controlled or "
"invalidated."
@ -333,15 +333,15 @@ msgstr ""
"Vous n'avez pas le droit de modifier un achat précédemment contrôlé ou "
"invalidé."
#: cotisations/models.py:580
#: cotisations/models.py:586
msgid "You don't have the right to delete a purchase."
msgstr "Vous n'avez pas le droit de supprimer un achat."
#: cotisations/models.py:586
#: cotisations/models.py:592
msgid "You don't have the right to delete this user's purchases."
msgstr "Vous n'avez pas le droit de supprimer les achats de cet utilisateur."
#: cotisations/models.py:593
#: cotisations/models.py:599
msgid ""
"You don't have the right to delete a purchase already controlled or "
"invalidated."
@ -349,134 +349,134 @@ msgstr ""
"Vous n'avez pas le droit de supprimer un achat précédemment contrôlé ou "
"invalidé."
#: cotisations/models.py:609
#: cotisations/models.py:615
msgid "You don't have the right to view someone else's purchase history."
msgstr ""
"Vous n'avez pas le droit de voir l'historique des achats d'un autre "
"utilisateur."
#: cotisations/models.py:677
#: cotisations/models.py:683
msgid "Club"
msgstr "Club"
#: cotisations/models.py:687
#: cotisations/models.py:693
msgid "designation"
msgstr "désignation"
#: cotisations/models.py:690
#: cotisations/models.py:696
msgid "unit price"
msgstr "prix unitaire"
#: cotisations/models.py:708
#: cotisations/models.py:714
msgid "type of users concerned"
msgstr "type d'utilisateurs concernés"
#: cotisations/models.py:719 cotisations/models.py:820
#: cotisations/models.py:725 cotisations/models.py:826
msgid "is available for every user"
msgstr "est disponible pour chaque utilisateur"
#: cotisations/models.py:726
#: cotisations/models.py:732
msgid "Can view an article object"
msgstr "Peut voir un objet article"
#: cotisations/models.py:727
#: cotisations/models.py:733
msgid "Can buy every article"
msgstr "Peut acheter chaque article"
#: cotisations/models.py:734
#: cotisations/models.py:740
msgid "Solde is a reserved article name."
msgstr "Solde est un nom d'article réservé."
#: cotisations/models.py:759
#: cotisations/models.py:765
msgid "You can't buy this article."
msgstr "Vous ne pouvez pas acheter cet article."
#: cotisations/models.py:800
#: cotisations/models.py:806
msgid "Can view a bank object"
msgstr "Peut voir un objet banque"
#: cotisations/models.py:801
#: cotisations/models.py:807
msgid "bank"
msgstr "banque"
#: cotisations/models.py:802
#: cotisations/models.py:808
msgid "banks"
msgstr "banques"
#: cotisations/models.py:818
#: cotisations/models.py:824
msgid "method"
msgstr "moyen"
#: cotisations/models.py:825
#: cotisations/models.py:831
msgid "is user balance"
msgstr "est solde utilisateur"
#: cotisations/models.py:826
#: cotisations/models.py:832
msgid "There should be only one balance payment method."
msgstr "Il ne devrait y avoir qu'un moyen de paiement solde."
#: cotisations/models.py:832
#: cotisations/models.py:838
msgid "Can view a payment method object"
msgstr "Peut voir un objet moyen de paiement"
#: cotisations/models.py:833
#: cotisations/models.py:839
msgid "Can use every payment method"
msgstr "Peut utiliser chaque moyen de paiement"
#: cotisations/models.py:835
#: cotisations/models.py:841
msgid "payment method"
msgstr "moyen de paiement"
#: cotisations/models.py:836
#: cotisations/models.py:842
msgid "payment methods"
msgstr "moyens de paiement"
#: cotisations/models.py:875 cotisations/payment_methods/comnpay/views.py:62
#: cotisations/models.py:881 cotisations/payment_methods/comnpay/views.py:62
#, python-format
msgid "The subscription of %(member_name)s was extended to %(end_date)s."
msgstr "La cotisation de %(member_name)s a été étendue au %(end_date)s."
#: cotisations/models.py:885
#: cotisations/models.py:891
msgid "The invoice was created."
msgstr "La facture a été créée."
#: cotisations/models.py:905
#: cotisations/models.py:911
msgid "You can't use this payment method."
msgstr "Vous ne pouvez pas utiliser ce moyen de paiement."
#: cotisations/models.py:924
#: cotisations/models.py:930
msgid "No custom payment methods."
msgstr "Pas de moyens de paiement personnalisés."
#: cotisations/models.py:955
#: cotisations/models.py:961
msgid "start date"
msgstr "date de début"
#: cotisations/models.py:956
#: cotisations/models.py:962
msgid "end date"
msgstr "date de fin"
#: cotisations/models.py:960
#: cotisations/models.py:966
msgid "Can view a subscription object"
msgstr "Peut voir un objet cotisation"
#: cotisations/models.py:961
#: cotisations/models.py:967
msgid "Can edit the previous subscriptions"
msgstr "Peut modifier les cotisations précédentes"
#: cotisations/models.py:963
#: cotisations/models.py:969
msgid "subscription"
msgstr "cotisation"
#: cotisations/models.py:964
#: cotisations/models.py:970
msgid "subscriptions"
msgstr "cotisations"
#: cotisations/models.py:970
#: cotisations/models.py:976
msgid "You don't have the right to edit a subscription."
msgstr "Vous n'avez pas le droit de modifier une cotisation."
#: cotisations/models.py:979
#: cotisations/models.py:985
msgid ""
"You don't have the right to edit a subscription already controlled or "
"invalidated."
@ -484,11 +484,11 @@ msgstr ""
"Vous n'avez pas le droit de modifier une cotisation précédemment contrôlée "
"ou invalidée."
#: cotisations/models.py:991
#: cotisations/models.py:997
msgid "You don't have the right to delete a subscription."
msgstr "Vous n'avez pas le droit de supprimer une cotisation."
#: cotisations/models.py:998
#: cotisations/models.py:1004
msgid ""
"You don't have the right to delete a subscription already controlled or "
"invalidated."
@ -496,7 +496,7 @@ msgstr ""
"Vous n'avez pas le droit de supprimer une cotisation précédemment contrôlée "
"ou invalidée."
#: cotisations/models.py:1014
#: cotisations/models.py:1020
msgid "You don't have the right to view someone else's subscription history."
msgstr ""
"Vous n'avez pas le droit de voir l'historique des cotisations d'un autre "
@ -784,7 +784,7 @@ msgstr "Contrôlé"
#: cotisations/templates/cotisations/control.html:107
#: cotisations/templates/cotisations/delete.html:38
#: cotisations/templates/cotisations/edit_facture.html:64
#: cotisations/views.py:168 cotisations/views.py:222 cotisations/views.py:278
#: cotisations/views.py:170 cotisations/views.py:222 cotisations/views.py:276
msgid "Confirm"
msgstr "Confirmer"
@ -921,7 +921,7 @@ msgstr "Rechargement de solde"
msgid "Pay %(amount)s €"
msgstr "Payer %(amount)s €"
#: cotisations/templates/cotisations/payment.html:42 cotisations/views.py:1028
#: cotisations/templates/cotisations/payment.html:42 cotisations/views.py:1026
msgid "Pay"
msgstr "Payer"
@ -933,11 +933,11 @@ msgstr "Créer une facture"
msgid "Control the invoices"
msgstr "Contrôler les factures"
#: cotisations/views.py:155
#: cotisations/views.py:157
msgid "You need to choose at least one article."
msgstr "Vous devez choisir au moins un article."
#: cotisations/views.py:169
#: cotisations/views.py:171
msgid "New invoice"
msgstr "Nouvelle facture"
@ -949,104 +949,104 @@ msgstr "Le devis a été créé."
msgid "New cost estimate"
msgstr "Nouveau devis"
#: cotisations/views.py:272
#: cotisations/views.py:270
msgid "The custom invoice was created."
msgstr "La facture personnalisée a été créée."
#: cotisations/views.py:282
#: cotisations/views.py:280
msgid "New custom invoice"
msgstr "Nouvelle facture personnalisée"
#: cotisations/views.py:357 cotisations/views.py:438
#: cotisations/views.py:355 cotisations/views.py:436
msgid "The invoice was edited."
msgstr "La facture a été modifiée."
#: cotisations/views.py:375 cotisations/views.py:570
#: cotisations/views.py:373 cotisations/views.py:568
msgid "The invoice was deleted."
msgstr "La facture a été supprimée."
#: cotisations/views.py:398
#: cotisations/views.py:396
msgid "The cost estimate was edited."
msgstr "Le devis a été modifié."
#: cotisations/views.py:405
#: cotisations/views.py:403
msgid "Edit cost estimate"
msgstr "Modifier le devis"
#: cotisations/views.py:419
#: cotisations/views.py:417
msgid "An invoice was successfully created from your cost estimate."
msgstr "Une facture a bien été créée à partir de votre devis."
#: cotisations/views.py:445
#: cotisations/views.py:443
msgid "Edit custom invoice"
msgstr "Modifier la facture personnalisée"
#: cotisations/views.py:507
#: cotisations/views.py:505
msgid "The cost estimate was deleted."
msgstr "Le devis a été supprimé."
#: cotisations/views.py:510
#: cotisations/views.py:508
msgid "cost estimate"
msgstr "devis"
#: cotisations/views.py:594
#: cotisations/views.py:592
msgid "The article was created."
msgstr "L'article a été créé."
#: cotisations/views.py:599 cotisations/views.py:673 cotisations/views.py:767
#: cotisations/views.py:597 cotisations/views.py:671 cotisations/views.py:765
msgid "Add"
msgstr "Ajouter"
#: cotisations/views.py:600
#: cotisations/views.py:598
msgid "New article"
msgstr "Nouvel article"
#: cotisations/views.py:617
#: cotisations/views.py:615
msgid "The article was edited."
msgstr "L'article a été modifié."
#: cotisations/views.py:622 cotisations/views.py:705 cotisations/views.py:791
#: cotisations/views.py:620 cotisations/views.py:703 cotisations/views.py:789
msgid "Edit"
msgstr "Modifier"
#: cotisations/views.py:623
#: cotisations/views.py:621
msgid "Edit article"
msgstr "Modifier l'article"
#: cotisations/views.py:640
#: cotisations/views.py:638
msgid "The articles were deleted."
msgstr "Les articles ont été supprimés."
#: cotisations/views.py:645 cotisations/views.py:744 cotisations/views.py:829
#: cotisations/views.py:643 cotisations/views.py:742 cotisations/views.py:827
msgid "Delete"
msgstr "Supprimer"
#: cotisations/views.py:646
#: cotisations/views.py:644
msgid "Delete article"
msgstr "Supprimer l'article"
#: cotisations/views.py:667
#: cotisations/views.py:665
msgid "The payment method was created."
msgstr "Le moyen de paiment a été créé."
#: cotisations/views.py:674
#: cotisations/views.py:672
msgid "New payment method"
msgstr "Nouveau moyen de paiement"
#: cotisations/views.py:699
#: cotisations/views.py:697
msgid "The payment method was edited."
msgstr "Le moyen de paiment a été modifié."
#: cotisations/views.py:706
#: cotisations/views.py:704
msgid "Edit payment method"
msgstr "Modifier le moyen de paiement"
#: cotisations/views.py:728
#: cotisations/views.py:726
#, python-format
msgid "The payment method %(method_name)s was deleted."
msgstr "Le moyen de paiement %(method_name)s a été supprimé."
#: cotisations/views.py:735
#: cotisations/views.py:733
#, python-format
msgid ""
"The payment method %(method_name)s can't be deleted because there are "
@ -1055,32 +1055,32 @@ msgstr ""
"Le moyen de paiement %(method_name)s ne peut pas être supprimé car il y a "
"des factures qui l'utilisent."
#: cotisations/views.py:745
#: cotisations/views.py:743
msgid "Delete payment method"
msgstr "Supprimer le moyen de paiement"
#: cotisations/views.py:762
#: cotisations/views.py:760
msgid "The bank was created."
msgstr "La banque a été créée."
#: cotisations/views.py:768
#: cotisations/views.py:766
msgid "New bank"
msgstr "Nouvelle banque"
#: cotisations/views.py:786
#: cotisations/views.py:784
msgid "The bank was edited."
msgstr "La banque a été modifiée."
#: cotisations/views.py:792
#: cotisations/views.py:790
msgid "Edit bank"
msgstr "Modifier la banque"
#: cotisations/views.py:814
#: cotisations/views.py:812
#, python-format
msgid "The bank %(bank_name)s was deleted."
msgstr "La banque %(bank_name)s a été supprimée."
#: cotisations/views.py:820
#: cotisations/views.py:818
#, python-format
msgid ""
"The bank %(bank_name)s can't be deleted because there are invoices using it."
@ -1088,22 +1088,22 @@ msgstr ""
"La banque %(bank_name)s ne peut pas être supprimée car il y a des factures "
"qui l'utilisent."
#: cotisations/views.py:830
#: cotisations/views.py:828
msgid "Delete bank"
msgstr "Supprimer la banque"
#: cotisations/views.py:864
#: cotisations/views.py:862
msgid "Your changes have been properly taken into account."
msgstr "Vos modifications ont correctement été prises en compte."
#: cotisations/views.py:996
#: cotisations/views.py:994
msgid "You are not allowed to credit your balance."
msgstr "Vous n'êtes pas autorisé à créditer votre solde."
#: cotisations/views.py:1027
#: cotisations/views.py:1025
msgid "Refill your balance"
msgstr "Recharger votre solde"
#: cotisations/views.py:1046
#: cotisations/views.py:1044
msgid "Could not find a voucher for that invoice."
msgstr "Impossible de trouver un reçu pour cette facture."

View file

@ -480,6 +480,21 @@ def decide_vlan_switch(nas_machine, nas_type, port_number, mac_address):
RadiusOption.get_cached_value("banned") != RadiusOption.REJECT,
RadiusOption.get_attributes("banned_attributes", attributes_kwargs),
)
elif user.email_state == User.EMAIL_STATE_UNVERIFIED:
return (
sw_name,
room,
u"Utilisateur suspendu (mail non confirme)",
getattr(
RadiusOption.get_cached_value("non_member_vlan"),
"vlan_id",
None,
),
RadiusOption.get_cached_value("non_member") != RadiusOption.REJECT,
RadiusOption.get_attributes(
"non_member_attributes", attributes_kwargs
),
)
elif not (user.is_connected() or user.is_whitelisted()):
return (
sw_name,

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-19 23:43+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2018-06-23 16:01+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -159,7 +159,7 @@ msgid "Statistics"
msgstr "Statistiques"
#: logs/templates/logs/index.html:32 logs/templates/logs/stats_logs.html:32
#: logs/views.py:400
#: logs/views.py:418
msgid "Actions performed"
msgstr "Actions effectuées"
@ -260,65 +260,77 @@ msgid "Users benefiting from a free connection"
msgstr "Utilisateurs bénéficiant d'une connexion gratuite"
#: logs/views.py:288
msgid "Users with a confirmed email"
msgstr "Utilisateurs ayant un mail confirmé"
#: logs/views.py:294
msgid "Users with an unconfirmed email"
msgstr "Utilisateurs ayant un mail non confirmé"
#: logs/views.py:300
msgid "Users pending email confirmation"
msgstr "Utilisateurs en attente de confirmation du mail"
#: logs/views.py:306
msgid "Active interfaces (with access to the network)"
msgstr "Interfaces actives (ayant accès au réseau)"
#: logs/views.py:302
#: logs/views.py:320
msgid "Active interfaces assigned IPv4"
msgstr "Interfaces actives assignées IPv4"
#: logs/views.py:319
#: logs/views.py:337
msgid "IP range"
msgstr "Plage d'IP"
#: logs/views.py:320
#: logs/views.py:338
msgid "VLAN"
msgstr "VLAN"
#: logs/views.py:321
#: logs/views.py:339
msgid "Total number of IP addresses"
msgstr "Nombre total d'adresses IP"
#: logs/views.py:322
#: logs/views.py:340
msgid "Number of assigned IP addresses"
msgstr "Nombre d'adresses IP assignées"
#: logs/views.py:323
#: logs/views.py:341
msgid "Number of IP address assigned to an activated machine"
msgstr "Nombre d'adresses IP assignées à une machine activée"
#: logs/views.py:324
#: logs/views.py:342
msgid "Number of unassigned IP addresses"
msgstr "Nombre d'adresses IP non assignées"
#: logs/views.py:339
#: logs/views.py:357
msgid "Users (members and clubs)"
msgstr "Utilisateurs (adhérents et clubs)"
#: logs/views.py:385
#: logs/views.py:403
msgid "Topology"
msgstr "Topologie"
#: logs/views.py:401
#: logs/views.py:419
msgid "Number of actions"
msgstr "Nombre d'actions"
#: logs/views.py:426
#: logs/views.py:444
msgid "rights"
msgstr "droits"
#: logs/views.py:455
#: logs/views.py:473
msgid "actions"
msgstr "actions"
#: logs/views.py:486
#: logs/views.py:504
msgid "No model found."
msgstr "Aucun modèle trouvé."
#: logs/views.py:492
#: logs/views.py:510
msgid "Nonexistent entry."
msgstr "Entrée inexistante."
#: logs/views.py:499
#: logs/views.py:517
msgid "You don't have the right to access this menu."
msgstr "Vous n'avez pas le droit d'accéder à ce menu."

View file

@ -284,6 +284,24 @@ def stats_general(request):
_all_whitelisted.exclude(adherent__isnull=True).count(),
_all_whitelisted.exclude(club__isnull=True).count(),
],
"email_state_verified_users": [
_("Users with a confirmed email"),
User.objects.filter(email_state=User.EMAIL_STATE_VERIFIED).count(),
Adherent.objects.filter(email_state=User.EMAIL_STATE_VERIFIED).count(),
Club.objects.filter(email_state=User.EMAIL_STATE_VERIFIED).count(),
],
"email_state_unverified_users": [
_("Users with an unconfirmed email"),
User.objects.filter(email_state=User.EMAIL_STATE_UNVERIFIED).count(),
Adherent.objects.filter(email_state=User.EMAIL_STATE_UNVERIFIED).count(),
Club.objects.filter(email_state=User.EMAIL_STATE_UNVERIFIED).count(),
],
"email_state_pending_users": [
_("Users pending email confirmation"),
User.objects.filter(email_state=User.EMAIL_STATE_PENDING).count(),
Adherent.objects.filter(email_state=User.EMAIL_STATE_PENDING).count(),
Club.objects.filter(email_state=User.EMAIL_STATE_PENDING).count(),
],
"actives_interfaces": [
_("Active interfaces (with access to the network)"),
_all_active_interfaces_count.count(),

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-20 01:24+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2018-06-23 16:35+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -55,91 +55,91 @@ msgstr "Sélectionnez un type de machine"
msgid "Automatic IPv4 assignment"
msgstr "Assignation automatique IPv4"
#: machines/forms.py:177
#: machines/forms.py:172
msgid "Current aliases"
msgstr "Alias actuels"
#: machines/forms.py:199
#: machines/forms.py:194
msgid "Machine type to add"
msgstr "Type de machine à ajouter"
#: machines/forms.py:200
#: machines/forms.py:195
msgid "Related IP type"
msgstr "Type d'IP relié"
#: machines/forms.py:208
#: machines/forms.py:203
msgid "Current machine types"
msgstr "Types de machines actuels"
#: machines/forms.py:232
#: machines/forms.py:227
msgid "IP type to add"
msgstr "Type d'IP à ajouter"
#: machines/forms.py:260
#: machines/forms.py:255
msgid "Current IP types"
msgstr "Types d'IP actuels"
#: machines/forms.py:283
#: machines/forms.py:278
msgid "Extension to add"
msgstr "Extension à ajouter"
#: machines/forms.py:284 machines/templates/machines/aff_extension.html:37
#: machines/forms.py:279 machines/templates/machines/aff_extension.html:37
msgid "A record origin"
msgstr "Enregistrement A origin"
#: machines/forms.py:285 machines/templates/machines/aff_extension.html:39
#: machines/forms.py:280 machines/templates/machines/aff_extension.html:39
msgid "AAAA record origin"
msgstr "Enregistrement AAAA origin"
#: machines/forms.py:286
#: machines/forms.py:281
msgid "SOA record to use"
msgstr "Enregistrement SOA à utiliser"
#: machines/forms.py:287
#: machines/forms.py:282
msgid "Sign with DNSSEC"
msgstr "Signer avec DNSSEC"
#: machines/forms.py:295
#: machines/forms.py:290
msgid "Current extensions"
msgstr "Extensions actuelles"
#: machines/forms.py:337
#: machines/forms.py:332
msgid "Current SOA records"
msgstr "Enregistrements SOA actuels"
#: machines/forms.py:370
#: machines/forms.py:365
msgid "Current MX records"
msgstr "Enregistrements MX actuels"
#: machines/forms.py:405
#: machines/forms.py:400
msgid "Current NS records"
msgstr "Enregistrements NS actuels"
#: machines/forms.py:435
#: machines/forms.py:430
msgid "Current TXT records"
msgstr "Enregistrements TXT actuels"
#: machines/forms.py:465
#: machines/forms.py:460
msgid "Current DNAME records"
msgstr "Enregistrements DNAME actuels"
#: machines/forms.py:495
#: machines/forms.py:490
msgid "Current SRV records"
msgstr "Enregistrements SRV actuels"
#: machines/forms.py:526
#: machines/forms.py:521
msgid "Current NAS devices"
msgstr "Dispositifs NAS actuels"
#: machines/forms.py:559
#: machines/forms.py:554
msgid "Current roles"
msgstr "Rôles actuels"
#: machines/forms.py:601
#: machines/forms.py:596
msgid "Current services"
msgstr "Services actuels"
#: machines/forms.py:643
#: machines/forms.py:638
msgid "Current VLANs"
msgstr "VLANs actuels"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-20 01:24+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2019-11-16 00:22+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -66,8 +66,10 @@ class EditOptionalUserForm(ModelForm):
self.fields["gpg_fingerprint"].label = _("GPG fingerprint")
self.fields["all_can_create_club"].label = _("All can create a club")
self.fields["all_can_create_adherent"].label = _("All can create a member")
self.fields["disable_emailnotyetconfirmed"].label = _("Delay before disabling accounts without a verified email")
self.fields["self_adhesion"].label = _("Self registration")
self.fields["shell_default"].label = _("Default shell")
self.fields["allow_set_password_during_user_creation"].label = _("Allow directly setting a password during account creation")
class EditOptionalMachineForm(ModelForm):

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-16 17:06
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0067_auto_20191120_0159'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='allow_set_password_during_user_creation',
field=models.BooleanField(default=False, help_text='If True, users have the choice to receive an email containing a link to reset their password during creation, or to directly set their password in the page. If False, an email is always sent.'),
),
]

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-17 00:46
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0068_optionaluser_allow_set_password_during_user_creation'),
]
operations = [
migrations.AddField(
model_name='optionaluser',
name='disable_emailnotyetconfirmed',
field=models.IntegerField(default=2, help_text='Users with an email address not yet confirmed will be disabled after this number of days.')
),
]

View file

@ -107,6 +107,12 @@ class OptionalUser(AclMixin, PreferencesModel):
"Not yet active users will be deleted after this number of days."
),
)
disable_emailnotyetconfirmed = models.IntegerField(
default=2,
help_text=_(
"Users with an email address not yet confirmed will be disabled after this number of days."
),
)
self_adhesion = models.BooleanField(
default=False, help_text=_("A new user can create their account on Re2o.")
)
@ -117,6 +123,15 @@ class OptionalUser(AclMixin, PreferencesModel):
" If False, only when a valid registration has been paid."
),
)
allow_set_password_during_user_creation = models.BooleanField(
default=False,
help_text=_(
"If True, users have the choice to receive an email containing"
" a link to reset their password during creation, or to directly"
" set their password in the page."
" If False, an email is always sent."
),
)
allow_archived_connexion = models.BooleanField(
default=False, help_text=_("If True, archived users are allowed to connect.")
)

View file

@ -128,6 +128,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>{% trans "Allow archived users to log in" %}</th>
<td>{{ useroptions.allow_archived_connexion|tick }}</td>
</tr>
<tr>
<th>{% trans "Allow directly entering a password during account creation" %}</th>
<td>{{ useroptions.allow_set_password_during_user_creation|tick }}</td>
<th>{% trans "Delay before disabling accounts without a verified email" %}</th>
<td>{% blocktrans with disable_emailnotyetconfirmed=useroptions.disable_emailnotyetconfirmed %}{{ disable_emailnotyetconfirmed }} days{% endblocktrans %}</td>
</tr>
</table>
<h4 id="users">{% trans "Users general permissions" %}</h4>

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-20 01:24+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -117,6 +117,7 @@ def all_has_access(search_time=None, including_asso=True):
search_time = timezone.now()
filter_user = (
Q(state=User.STATE_ACTIVE)
& ~Q(email_state=User.EMAIL_STATE_UNVERIFIED)
& ~Q(
ban__in=Ban.objects.filter(
Q(date_start__lt=search_time) & Q(date_end__gt=search_time)

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-19 23:43+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2018-06-24 20:10+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -82,34 +82,33 @@ msgstr "Ports"
msgid "Switches"
msgstr "Commutateurs réseau"
#: search/forms.py:62 search/forms.py:77 search/templates/search/search.html:29
#: search/forms.py:62 search/forms.py:78 search/templates/search/search.html:29
#: search/templates/search/search.html:48
msgid "Search"
msgstr "Rechercher"
#: search/forms.py:65 search/forms.py:80
#: search/forms.py:65 search/forms.py:81
msgid ""
"Use « » and «,» to specify distinct words, «\"query\"» for"
" an exact search, «\\» to escape a character and «+» to"
" combine keywords."
"Use « » and «,» to specify distinct words, «\"query\"» for an exact search, "
"«\\» to escape a character and «+» to combine keywords."
msgstr ""
"Utilisez « » et «,» pour spécifier différents mots, «\"recherche\"» pour"
" une recherche exacte, «\\» pour échapper un caractère et «+» pour"
" combiner des mots clés."
"Utilisez « » et «,» pour spécifier différents mots, «\"recherche\"» pour une "
"recherche exacte, «\\» pour échapper un caractère et «+» pour combiner des "
"mots clés."
#: search/forms.py:88
#: search/forms.py:90
msgid "Users filter"
msgstr "Filtre utilisateurs"
#: search/forms.py:95
#: search/forms.py:97
msgid "Display filter"
msgstr "Filtre affichage"
#: search/forms.py:101
#: search/forms.py:103
msgid "Start date"
msgstr "Date de début"
#: search/forms.py:102
#: search/forms.py:104
msgid "End date"
msgstr "Date de fin"
@ -117,47 +116,47 @@ msgstr "Date de fin"
msgid "Search results"
msgstr "Résultats de la recherche"
#: search/templates/search/index.html:33
#: search/templates/search/index.html:34
msgid "Results among users:"
msgstr "Résultats parmi les utilisateurs :"
#: search/templates/search/index.html:37
#: search/templates/search/index.html:44
msgid "Results among clubs:"
msgstr "Résultats parmi les clubs :"
#: search/templates/search/index.html:41
#: search/templates/search/index.html:54
msgid "Results among machines:"
msgstr "Résultats parmi les machines :"
#: search/templates/search/index.html:45
#: search/templates/search/index.html:64
msgid "Results among invoices:"
msgstr "Résultats parmi les factures :"
#: search/templates/search/index.html:49
#: search/templates/search/index.html:74
msgid "Results among whitelists:"
msgstr "Résultats parmi les accès à titre gracieux :"
#: search/templates/search/index.html:53
#: search/templates/search/index.html:84
msgid "Results among bans:"
msgstr "Résultats parmi les bannissements :"
#: search/templates/search/index.html:57
#: search/templates/search/index.html:94
msgid "Results among rooms:"
msgstr "Résultats parmi les chambres :"
#: search/templates/search/index.html:61
#: search/templates/search/index.html:104
msgid "Results among ports:"
msgstr "Résultats parmi les ports :"
#: search/templates/search/index.html:65
#: search/templates/search/index.html:114
msgid "Results among switches:"
msgstr "Résultats parmi les commutateurs réseau :"
#: search/templates/search/index.html:69
#: search/templates/search/index.html:123
msgid "No result"
msgstr "Pas de résultat"
#: search/templates/search/index.html:71
#: search/templates/search/index.html:125
#, python-format
msgid "Only the first %(max_result)s results are displayed in each category."
msgstr ""
@ -171,3 +170,12 @@ msgstr "Recherche simple"
#: search/templates/search/sidebar.html:35
msgid "Advanced search"
msgstr "Recherche avancée"
#~ msgid "Verified"
#~ msgstr "Confirmé"
#~ msgid "Unverified"
#~ msgstr "Non-confirmé"
#~ msgid "Waiting for email confirmation"
#~ msgstr "En attente de confirmation du mail"

View file

@ -0,0 +1,24 @@
/** This makes an checkbox toggle the appeareance of the
* password and password confirmations fields.
*/
function toggle_show_password_chkbox() {
var password1 = document.getElementById('id_Adherent-password1');
var password2 = document.getElementById('id_Adherent-password2');
if (show_password_chkbox.checked) {
password1.parentElement.style.display = 'none';
password2.parentElement.style.display = 'none';
password1.required = false;
password2.required = false;
} else {
password1.parentElement.style.display = 'block';
password2.parentElement.style.display = 'block';
password1.required = true;
password2.required = true;
}
}
var show_password_chkbox = document.getElementById('id_Adherent-init_password_by_mail');
show_password_chkbox.onclick = toggle_show_password_chkbox;
toggle_show_password_chkbox();

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-19 23:43+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2018-03-31 16:09+0002\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"
@ -339,19 +339,19 @@ msgstr "Si vous n'avez aucune idée de ce que vous avez fait :"
msgid "Go back to a safe page"
msgstr "Retourner à une page sécurisée"
#: templates/pagination.html:34
#: templates/pagination.html:35
msgid "First"
msgstr "Première page"
#: templates/pagination.html:40
#: templates/pagination.html:41
msgid "Previous"
msgstr "Précédent"
#: templates/pagination.html:60
#: templates/pagination.html:61
msgid "Next"
msgstr "Suivant"
#: templates/pagination.html:66
#: templates/pagination.html:67
msgid "Last"
msgstr "Dernière page"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-20 01:24+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2019-11-16 00:35+0100\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -21,7 +21,7 @@ msgid ""
msgstr ""
"Project-Id-Version: 2.5\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-11-20 01:24+0100\n"
"POT-Creation-Date: 2020-04-18 01:38+0200\n"
"PO-Revision-Date: 2018-06-25 14:53+0200\n"
"Last-Translator: Laouen Fernet <laouen.fernet@supelec.fr>\n"
"Language-Team: \n"

View file

@ -3,9 +3,11 @@
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Lara Kermarec
# Copyright © 2017 Augustin Lemesle
# Copyright © 2017-2020 Gabriel Détraz
# Copyright © 2017-2020 Lara Kermarec
# Copyright © 2017-2020 Augustin Lemesle
# Copyright © 2017-2020 Hugo Levy--Falk
# Copyright © 2017-2020 Jean-Romain Garnier
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -113,6 +115,7 @@ class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm):
"""Changement du mot de passe"""
user = super(PassForm, self).save(commit=False)
user.set_password(self.cleaned_data.get("passwd1"))
user.state = User.STATE_NOT_YET_ACTIVE
user.set_active()
user.save()
@ -380,7 +383,42 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm):
class AdherentCreationForm(AdherentForm):
"""Formulaire de création d'un user.
AdherentForm auquel on ajoute une checkbox afin d'éviter les
doublons d'utilisateurs"""
doublons d'utilisateurs et, optionnellement,
un champ mot de passe"""
# Champ pour choisir si un lien est envoyé par mail pour le mot de passe
init_password_by_mail_info = _(
"If this options is set, you will receive a link to set"
" your initial password by email. If you do not have"
" any means of accessing your emails, you can disable"
" this option to set your password immediatly."
" You will still receive an email to confirm your address."
" Failure to confirm your address will result in an"
" automatic suspension of your account until you do."
)
init_password_by_mail = forms.BooleanField(
help_text=init_password_by_mail_info,
required=False,
initial=True
)
init_password_by_mail.label = _("Send password reset link by email.")
# Champs pour initialiser le mot de passe
# Validators are handled manually since theses fields aren't always required
password1 = forms.CharField(
required=False,
label=_("Password"),
widget=forms.PasswordInput,
#validators=[MinLengthValidator(8)],
max_length=255,
)
password2 = forms.CharField(
required=False,
label=_("Password confirmation"),
widget=forms.PasswordInput,
#validators=[MinLengthValidator(8)],
max_length=255,
)
# Champ permettant d'éviter au maxium les doublons d'utilisateurs
former_user_check_info = _(
@ -422,6 +460,53 @@ class AdherentCreationForm(AdherentForm):
)
)
# Remove password fields if option is disabled
if not OptionalUser.get_cached_value("allow_set_password_during_user_creation"):
self.fields.pop("init_password_by_mail")
self.fields.pop("password1")
self.fields.pop("password2")
def clean_password1(self):
"""Ignore ce champs si la case init_password_by_mail est décochée"""
send_email = self.cleaned_data.get("init_password_by_mail")
if send_email:
return None
password1 = self.cleaned_data.get("password1")
if len(password1) < 8:
raise forms.ValidationError(_("Password must contain at least 8 characters."))
return password1
def clean_password2(self):
"""Verifie que password1 et 2 sont identiques (si nécessaire)"""
send_email = self.cleaned_data.get("init_password_by_mail")
if send_email:
return None
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("The passwords don't match."))
return password2
def save(self, commit=True):
"""Set the user's password, if entered
Returns the user and a bool indicating whether
an email to init the password should be sent"""
# Save the provided password in hashed format
user = super(AdherentForm, self).save(commit=False)
is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation")
set_passwd = is_set_password_allowed and not self.cleaned_data.get("init_password_by_mail")
if set_passwd:
user.set_password(self.cleaned_data["password1"])
user.save()
return user
class AdherentEditForm(AdherentForm):
"""Formulaire d'édition d'un user.
@ -565,22 +650,17 @@ class EditServiceUserForm(ServiceUserForm):
class StateForm(FormRevMixin, ModelForm):
""" Changement de l'état d'un user"""
"""Change state of an user, and if its main email is verified or not"""
class Meta:
model = User
fields = ["state"]
fields = ["state", "email_state"]
def __init__(self, *args, **kwargs):
prefix = kwargs.pop("prefix", self.Meta.model.__name__)
super(StateForm, self).__init__(*args, prefix=prefix, **kwargs)
def save(self, commit=True):
user = super(StateForm, self).save(commit=False)
if self.cleaned_data["state"]:
user.state = self.cleaned_data.get("state")
user.state_sync()
user.save()
self.fields["state"].label = _("State")
self.fields["email_state"].label = _("Email state")
class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm):

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
from django.core.management.base import BaseCommand, CommandError
from django.db.models import Q
from users.models import User
from cotisations.models import Facture
@ -31,9 +32,9 @@ class Command(BaseCommand):
def handle(self, *args, **options):
"""First deleting invalid invoices, and then deleting the users"""
days = OptionalUser.get_cached_value("delete_notyetactive")
days = OptionalUser.get_cached_value("disable_emailnotyetconfirmed")
users_to_delete = (
User.objects.filter(state=User.STATE_NOT_YET_ACTIVE)
User.objects.filter(Q(state=User.STATE_NOT_YET_ACTIVE))
.filter(registered__lte=timezone.now() - timedelta(days=days))
.exclude(facture__valid=True)
.distinct()

View file

@ -0,0 +1,43 @@
# Copyright © 2017-2020 Jean-Romain Garnier
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
from django.core.management.base import BaseCommand, CommandError
from users.models import User
from cotisations.models import Facture
from preferences.models import OptionalUser
from datetime import timedelta
from django.utils import timezone
class Command(BaseCommand):
help = "Disable users who haven't confirmed their email."
def handle(self, *args, **options):
"""First deleting invalid invoices, and then deleting the users"""
days = OptionalUser.get_cached_value("disable_emailnotyetconfirmed")
users_to_disable = (
User.objects.filter(email_state=User.EMAIL_STATE_PENDING)
.filter(email_change_date__lte=timezone.now() - timedelta(days=days))
.distinct()
)
print("Disabling " + str(users_to_disable.count()) + " users.")
for user in users_to_disable:
user.email_state = User.EMAIL_STATE_UNVERIFIED
user.notif_disable()
user.save()

View file

@ -0,0 +1,29 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-16 22:31
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0084_auto_20191120_0159'),
]
def flag_verified(apps, schema_editor):
db_alias = schema_editor.connection.alias
users = apps.get_model("users", "User")
users.objects.using(db_alias).all().update(email_state=0)
def undo_flag_verified(apps, schema_editor):
return
operations = [
migrations.AddField(
model_name='user',
name='email_state',
field=models.IntegerField(choices=[(0, 'Verified'), (1, 'Unverified'), (2, 'Waiting for email confirmation')], default=2),
),
migrations.RunPython(flag_verified, undo_flag_verified),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-17 00:00
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0085_user_email_state'),
]
operations = [
migrations.AddField(
model_name='user',
name='email_change_date',
field=models.DateTimeField(default=None, null=True),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-17 20:10
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0086_user_email_change_date'),
]
operations = [
migrations.AddField(
model_name='request',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True),
),
]

View file

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-17 21:12
from __future__ import unicode_literals
import datetime
from django.db import migrations, models
from django.utils.timezone import utc
class Migration(migrations.Migration):
dependencies = [
('users', '0087_request_email'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email_change_date',
field=models.DateTimeField(auto_now_add=True, default=datetime.datetime(2020, 4, 17, 21, 12, 19, 739799, tzinfo=utc)),
preserve_default=False,
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.28 on 2020-04-17 23:12
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0088_auto_20200417_2312'),
]
operations = [
migrations.AlterField(
model_name='user',
name='email_state',
field=models.IntegerField(choices=[(0, 'Confirmed'), (1, 'Not confirmed'), (2, 'Waiting for email confirmation')], default=2),
),
]

View file

@ -3,9 +3,11 @@
# Il se veut agnostique au réseau considéré, de manière à être installable
# en quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Lara Kermarec
# Copyright © 2017 Augustin Lemesle
# Copyright © 2017-2020 Gabriel Détraz
# Copyright © 2017-2020 Lara Kermarec
# Copyright © 2017-2020 Augustin Lemesle
# Copyright © 2017-2020 Hugo Levy--Falk
# Copyright © 2017-2020 Jean-Romain Garnier
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -62,6 +64,7 @@ from django.core.mail import send_mail
from django.core.urlresolvers import reverse
from django.db import transaction
from django.utils import timezone
from datetime import timedelta
from django.contrib.auth.models import (
AbstractBaseUser,
BaseUserManager,
@ -144,6 +147,7 @@ class UserManager(BaseUserManager):
)
user.set_password(password)
user.confirm_mail()
if su:
user.is_superuser = True
user.save(using=self._db)
@ -184,6 +188,15 @@ class User(
(4, _("Fully archived")),
)
EMAIL_STATE_VERIFIED = 0
EMAIL_STATE_UNVERIFIED = 1
EMAIL_STATE_PENDING = 2
EMAIL_STATES = (
(0, _("Confirmed")),
(1, _("Not confirmed")),
(2, _("Waiting for email confirmation")),
)
surname = models.CharField(max_length=255)
pseudo = models.CharField(
max_length=32,
@ -217,6 +230,7 @@ class User(
)
pwd_ntlm = models.CharField(max_length=255)
state = models.IntegerField(choices=STATES, default=STATE_NOT_YET_ACTIVE)
email_state = models.IntegerField(choices=EMAIL_STATES, default=EMAIL_STATE_PENDING)
registered = models.DateTimeField(auto_now_add=True)
telephone = models.CharField(max_length=15, blank=True, null=True)
uid_number = models.PositiveIntegerField(default=get_fresh_user_uid, unique=True)
@ -224,6 +238,7 @@ class User(
shortcuts_enabled = models.BooleanField(
verbose_name=_("enable shortcuts on Re2o website"), default=True
)
email_change_date = models.DateTimeField(auto_now_add=True)
USERNAME_FIELD = "pseudo"
REQUIRED_FIELDS = ["surname", "email"]
@ -335,7 +350,7 @@ class User(
def set_active(self):
"""Enable this user if he subscribed successfully one time before
Reenable it if it was archived
Do nothing if disabed"""
Do nothing if disabled or waiting for email confirmation"""
if self.state == self.STATE_NOT_YET_ACTIVE:
if self.facture_set.filter(valid=True).filter(
Q(vente__type_cotisation="All") | Q(vente__type_cotisation="Adhesion")
@ -388,7 +403,7 @@ class User(
@cached_property
def get_shadow_expire(self):
"""Return the shadow_expire value for the user"""
if self.state == self.STATE_DISABLED:
if self.state == self.STATE_DISABLED or self.email_state == self.EMAIL_STATE_UNVERIFIED:
return str(0)
else:
return None
@ -481,6 +496,7 @@ class User(
""" Renvoie si un utilisateur a accès à internet """
return (
self.state == User.STATE_ACTIVE
and self.email_state != User.EMAIL_STATE_UNVERIFIED
and not self.is_ban()
and (self.is_connected() or self.is_whitelisted())
) or self == AssoOption.get_cached_value("utilisateur_asso")
@ -783,6 +799,90 @@ class User(
)
return
def send_confirm_email_if_necessary(self, request):
"""Update the user's email state:
* If the user changed email, it needs to be confirmed
* If they're not fully archived, send a confirmation email
Returns whether an email was sent"""
# Only update the state if the email changed
if self.__original_email == self.email:
return False
# If the user was previously in the PENDING or UNVERIFIED state,
# we can't update email_change_date otherwise it would push back
# their due date
# However, if the user is in the VERIFIED state, we reset the date
if self.email_state == self.EMAIL_STATE_VERIFIED:
self.email_change_date = timezone.now()
# Remember that the user needs to confirm their email address again
self.email_state = self.EMAIL_STATE_PENDING
self.save()
# Fully archived users shouldn't get an email, so stop here
if self.state == self.STATE_FULL_ARCHIVE:
return False
# Send the email
self.confirm_email_address_mail(request)
return True
def trigger_email_changed_state(self, request):
"""Trigger an email, and changed values after email_state been manually updated"""
if self.email_state == self.EMAIL_STATE_VERIFIED:
return False
self.email_change_date = timezone.now()
self.save()
self.confirm_email_address_mail(request)
return True
def confirm_email_before_date(self):
if self.email_state == self.EMAIL_STATE_VERIFIED:
return None
days = OptionalUser.get_cached_value("disable_emailnotyetconfirmed")
return self.email_change_date + timedelta(days=days)
def confirm_email_address_mail(self, request):
"""Prend en argument un request, envoie un mail pour
confirmer l'adresse"""
# Delete all older requests for this user, that aren't for this email
filter = Q(user=self) & Q(type=Request.EMAIL) & ~Q(email=self.email)
Request.objects.filter(filter).delete()
# Create the request and send the email
req = Request()
req.type = Request.EMAIL
req.user = self
req.email = self.email
req.save()
template = loader.get_template("users/email_confirmation_request")
context = {
"name": req.user.get_full_name(),
"asso": AssoOption.get_cached_value("name"),
"asso_mail": AssoOption.get_cached_value("contact"),
"site_name": GeneralOption.get_cached_value("site_name"),
"url": request.build_absolute_uri(
reverse("users:process", kwargs={"token": req.token})
),
"expire_in": str(GeneralOption.get_cached_value("req_expire_hrs")),
"confirm_before_fr": self.confirm_email_before_date().strftime("%d/%m/%Y"),
"confirm_before_en": self.confirm_email_before_date().strftime("%Y-%m-%d"),
}
send_mail(
"Confirmation du mail de %(name)s / Email confirmation for "
"%(name)s" % {"name": AssoOption.get_cached_value("name")},
template.render(context),
GeneralOption.get_cached_value("email_from"),
[req.user.email],
fail_silently=False,
)
return
def autoregister_machine(self, mac_address, nas_type):
""" Fonction appellée par freeradius. Enregistre la mac pour
une machine inconnue sur le compte de l'user"""
@ -836,6 +936,24 @@ class User(
)
return
def notif_disable(self):
"""Envoi un mail de notification informant que l'adresse mail n'a pas été confirmée"""
template = loader.get_template("users/email_disable_notif")
context = {
"name": self.get_full_name(),
"asso_name": AssoOption.get_cached_value("name"),
"asso_email": AssoOption.get_cached_value("contact"),
"site_name": GeneralOption.get_cached_value("site_name"),
}
send_mail(
"Suspension automatique / Automatic suspension",
template.render(context),
GeneralOption.get_cached_value("email_from"),
[self.email],
fail_silently=False,
)
return
def set_password(self, password):
""" A utiliser de préférence, set le password en hash courrant et
dans la version ntlm"""
@ -845,6 +963,10 @@ class User(
self.pwd_ntlm = hashNT(password)
return
def confirm_mail(self):
"""Marque l'email de l'utilisateur comme confirmé"""
self.email_state = self.EMAIL_STATE_VERIFIED
@cached_property
def email_address(self):
if (
@ -1190,6 +1312,7 @@ class User(
"room": self.can_change_room,
}
self.__original_state = self.state
self.__original_email = self.email
def clean(self, *args, **kwargs):
"""Check if this pseudo is already used by any mailalias.
@ -1771,6 +1894,7 @@ class Request(models.Model):
type = models.CharField(max_length=2, choices=TYPE_CHOICES)
token = models.CharField(max_length=32)
user = models.ForeignKey("User", on_delete=models.CASCADE)
email = models.EmailField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True, editable=False)
expires_at = models.DateTimeField()

View file

@ -0,0 +1,46 @@
{% extends 'users/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017-2020 Gabriel Détraz
Copyright © 2017-2020 Lara Kermarec
Copyright © 2017-2020 Augustin Lemesle
Copyright © 2017-2020 Hugo Levy--Falk
Copyright © 2017-2020 Jean-Romain Garnier
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load i18n %}
{% block title %}{% trans "Confirmation email" %}{% endblock %}
{% block content %}
<form class="form" method="post">
{% csrf_token %}
<h4>{% blocktrans %}Confirmation email{% endblocktrans %}</h4>
<p>{% blocktrans %}Confirm the email{% endblocktrans %} <a href="mailto:{{ email }}">{{ email }}</a> {% blocktrans %}for {{ name }}.{% endblocktrans %}</p>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon="ok" button_class="btn-success" %}
</form>
<br />
<br />
<br />
{% endblock %}

View file

@ -0,0 +1,39 @@
Bonjour {{ name }},
Vous trouverez ci-dessous une URL permettant de confirmer votre
adresse mail pour votre compte {{ site_name }}. Celui-ci vous permet de gérer l'ensemble
de vos équipements, votre compte, vos factures, et tous les services proposés sur le réseau.
{{ url }}
Contactez les administrateurs si vous n'êtes pas à l'origine de cette requête.
Ce lien expirera dans {{ expire_in }} heures.
S'il a expiré, vous pouvez renvoyer un mail de confirmation depuis votre compte {{ site_name }}.
/!\ Attention : Si vous ne confirmez pas votre email avant le {{ confirm_before_fr }}, votre compte sera suspendu.
Respectueusement,
L'équipe de {{ asso }} (contact : {{ asso_mail }}).
---
Hello {{ name }},
You will find below an URL allowing you to confirm the email address of your account
on {{ site_name }}. It enables you to manage your devices, your account, your invoices, and all
the services offered on the network.
{{ url }}
Contact the administrators if you didn't request this.
This link will expire in {{ expire_in }} hours.
If it has expired, you can send a new confirmation email from your account on {{ site_name }}.
/!\ Warning: If you do not confirm your email before {{ confirm_before_en }}, your account will be suspended.
Regards,
The {{ asso }} team (contact: {{ asso_mail }}).

View file

@ -0,0 +1,20 @@
Bonjour {{ name }},
Votre connexion a été automatiquement suspendue car votre adresse mail n'a pas été confirmée. Vous pouvez renvoyer un mail de confirmation sur votre compte {{ site_name }} pour réactiver votre connexion.
Pour de plus amples renseignements, contactez {{ asso_name }} à l'adresse {{ asso_mail }}.
Respectueusement,
L'équipe de {{ asso_name }}.
---
Hello {{ name }},
Your connection has automatically been suspended because you have not confirmed your email address. You can ask for a new confirmation email to be sent on your profil at {{ site_name }} to enable your connection.
For more information, contactez {{ asso_name }} at {{ asso_mail }}.
Regards,
The {{ asso_name }} team.

View file

@ -4,9 +4,11 @@ Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Lara Kermarec
Copyright © 2017 Augustin Lemesle
Copyright © 2017-2020 Gabriel Détraz
Copyright © 2017-2020 Lara Kermarec
Copyright © 2017-2020 Augustin Lemesle
Copyright © 2017-2020 Hugo Levy--Falk
Copyright © 2017-2020 Jean-Romain Garnier
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@ -38,6 +40,23 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<h2>{% blocktrans with name=users.name surname=users.surname %}Profile of {{ name }} {{ surname }}{% endblocktrans %}</h2>
{% endif %}
</div>
{% if users.email_state == users.EMAIL_STATE_PENDING %}
<div class="alert alert-warning">
{% blocktrans with confirm_before_date=users.confirm_email_before_date|date:"DATE_FORMAT" %}Please confirm your email address before {{ confirm_before_date }}, or your account will be suspended.{% endblocktrans %}
<br/>
<a href="{% url 'users:resend-confirmation-email' users.id %}">
{% blocktrans %}Didn't receive the email?{% endblocktrans %}
</a>
</div>
{% elif users.email_state == users.EMAIL_STATE_UNVERIFIED %}
<div class="alert alert-danger">
{% blocktrans %}Your account has been suspended, please confirm your email address.{% endblocktrans %}
</div>
{% endif %}
<div class="dashboard_container">
<div class="row">
<div class="col-sm-6 {% if solde_activated %}col-md-4{% else %}col_md-6{% endif %}">
@ -51,14 +70,21 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% elif not users.has_access %}
<div class="panel panel-danger">
<div class="panel-heading dashboard">{% trans "No connection" %}</div>
<div class="panel-body dashboard">
{% can_create Facture %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}">
<i class="fa fa-sign-in"></i> {% trans "Pay for a connection" %}
</a>
{% acl_else %}
{% trans "Ask someone with the appropriate rights to pay for a connection." %}
{% acl_end %}
{% if users.email_state == users.EMAIL_STATE_UNVERIFIED %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'users:resend-confirmation-email' users.id %}">
<i class="fa fa-sign-in"></i> {% trans "Resend the email" %}
</a>
{% else %}
{% can_create Facture %}
<a class="btn btn-danger btn-sm" role="button" href="{% url 'cotisations:new-facture' users.id %}">
<i class="fa fa-sign-in"></i> {% trans "Pay for a connection" %}
</a>
{% acl_else %}
{% trans "Ask someone with the appropriate rights to pay for a connection." %}
{% acl_end %}
{% endif %}
</div>
</div>
{% else %}
@ -181,7 +207,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<div class="col-md-6">
<dt>{% trans "Email address" %}</dt>
<dd><a href="mailto:{{ users.email }}">{{ users.email }}</a></dd>
<dd><a href="mailto:{{ users.email }}">{{ users.email }}</a>{% if users.email_state != users.EMAIL_STATE_VERIFIED %}<br/><i class="text-warning">{% trans "Pending confirmation..." %}</i>{% endif %}</dd>
</div>
<div class="col-md-6">

View file

@ -0,0 +1,42 @@
{% extends 'users/sidebar.html' %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2020 Jean-Romain Garnier
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% load i18n %}
{% block title %}{% trans "Confirmation email" %}{% endblock %}
{% block content %}
<form class="form" method="post">
{% csrf_token %}
<h4>{% blocktrans %}Re-send confirmation email?{% endblocktrans %}</h4>
<p>{% blocktrans %}The confirmation email will be sent to{% endblocktrans %} <a href="mailto:{{ email }}">{{ email }}</a>.</p>
{% trans "Confirm" as tr_confirm %}
{% bootstrap_button tr_confirm button_type="submit" icon="ok" button_class="btn-success" %}
</form>
<br />
<br />
<br />
{% endblock %}

View file

@ -3,9 +3,11 @@
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Lara Kermarec
# Copyright © 2017 Augustin Lemesle
# Copyright © 2017-2020 Gabriel Détraz
# Copyright © 2017-2020 Lara Kermarec
# Copyright © 2017-2020 Augustin Lemesle
# Copyright © 2017-2020 Hugo Levy--Falk
# Copyright © 2017-2020 Jean-Romain Garnier
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -42,6 +44,7 @@ urlpatterns = [
url(r"^state/(?P<userid>[0-9]+)$", views.state, name="state"),
url(r"^groups/(?P<userid>[0-9]+)$", views.groups, name="groups"),
url(r"^password/(?P<userid>[0-9]+)$", views.password, name="password"),
url(r"^confirm_email/(?P<userid>[0-9]+)$", views.resend_confirmation_email, name="resend-confirmation-email"),
url(
r"^del_group/(?P<userid>[0-9]+)/(?P<listrightid>[0-9]+)$",
views.del_group,

View file

@ -3,9 +3,11 @@
# se veut agnostique au réseau considéré, de manière à être installable en
# quelques clics.
#
# Copyright © 2017 Gabriel Détraz
# Copyright © 2017 Lara Kermarec
# Copyright © 2017 Augustin Lemesle
# Copyright © 2017-2020 Gabriel Détraz
# Copyright © 2017-2020 Lara Kermarec
# Copyright © 2017-2020 Augustin Lemesle
# Copyright © 2017-2020 Hugo Levy--Falk
# Copyright © 2017-2020 Jean-Romain Garnier
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -119,26 +121,42 @@ def new_user(request):
user = AdherentCreationForm(request.POST or None, user=request.user)
GTU_sum_up = GeneralOption.get_cached_value("GTU_sum_up")
GTU = GeneralOption.get_cached_value("GTU")
is_set_password_allowed = OptionalUser.get_cached_value("allow_set_password_during_user_creation")
if user.is_valid():
user = user.save()
user.reset_passwd_mail(request)
messages.success(
request,
_("The user %s was created, an email to set the password was sent.")
% user.pseudo,
)
if user.password:
user.send_confirm_email_if_necessary(request)
messages.success(
request,
_("The user %s was created, a confirmation email was sent.")
% user.pseudo,
)
else:
user.reset_passwd_mail(request)
messages.success(
request,
_("The user %s was created, an email to set the password was sent.")
% user.pseudo,
)
return redirect(reverse("users:profil", kwargs={"userid": str(user.id)}))
return form(
{
"userform": user,
"GTU_sum_up": GTU_sum_up,
"GTU": GTU,
"showCGU": True,
"action_name": _("Commit"),
},
"users/user.html",
request,
)
# Anonymous users are allowed to create new accounts
# but they should be treated differently
params = {
"userform": user,
"GTU_sum_up": GTU_sum_up,
"GTU": GTU,
"showCGU": True,
"action_name": _("Commit"),
}
if is_set_password_allowed:
params["load_js_file"] = "/static/js/toggle_password_fields.js"
return form(params, "users/user.html", request)
@login_required
@ -204,8 +222,12 @@ def edit_info(request, user, userid):
)
if user_form.is_valid():
if user_form.changed_data:
user_form.save()
user = user_form.save()
messages.success(request, _("The user was edited."))
if user.send_confirm_email_if_necessary(request):
messages.success(request, _("Sent a new confirmation email."))
return redirect(reverse("users:profil", kwargs={"userid": str(userid)}))
return form(
{"userform": user_form, "action_name": _("Edit")},
@ -221,8 +243,10 @@ def state(request, user, userid):
state_form = StateForm(request.POST or None, instance=user)
if state_form.is_valid():
if state_form.changed_data:
state_form.save()
messages.success(request, _("The state was edited."))
user_instance = state_form.save()
messages.success(request, _("The states were edited."))
if user_instance.trigger_email_changed_state(request):
messages.success(request, _("An email to confirm the address was sent."))
return redirect(reverse("users:profil", kwargs={"userid": str(userid)}))
return form(
{"userform": state_form, "action_name": _("Edit")},
@ -522,6 +546,10 @@ def edit_email_settings(request, user_instance, **_kwargs):
if email_settings.changed_data:
email_settings.save()
messages.success(request, _("The email settings were edited."))
if user_instance.send_confirm_email_if_necessary(request):
messages.success(request, _("An email to confirm your address was sent."))
return redirect(
reverse("users:profil", kwargs={"userid": str(user_instance.id)})
)
@ -974,12 +1002,15 @@ def reset_password(request):
def process(request, token):
"""Process, lien pour la reinitialisation du mot de passe"""
"""Process, lien pour la reinitialisation du mot de passe
et la confirmation de l'email"""
valid_reqs = Request.objects.filter(expires_at__gt=timezone.now())
req = get_object_or_404(valid_reqs, token=token)
if req.type == Request.PASSWD:
return process_passwd(request, req)
elif req.type == Request.EMAIL:
return process_email(request, req)
else:
messages.error(request, _("Error: please contact an admin."))
redirect(reverse("index"))
@ -992,9 +1023,12 @@ def process_passwd(request, req):
u_form = PassForm(request.POST or None, instance=user, user=request.user)
if u_form.is_valid():
with transaction.atomic(), reversion.create_revision():
user.confirm_mail()
u_form.save()
reversion.set_comment("Password reset")
req.delete()
# Delete all remaining requests
Request.objects.filter(user=user, type=Request.PASSWD).delete()
messages.success(request, _("The password was changed."))
return redirect(reverse("index"))
return form(
@ -1004,6 +1038,52 @@ def process_passwd(request, req):
)
def process_email(request, req):
"""Process la confirmation de mail, renvoie le formulaire
de validation"""
user = req.user
if request.method == "POST":
with transaction.atomic(), reversion.create_revision():
user.confirm_mail()
user.save()
reversion.set_comment("Email confirmation")
# Delete all remaining requests
Request.objects.filter(user=user, type=Request.EMAIL).delete()
messages.success(request, _("The %s address was confirmed." % user.email))
return redirect(reverse("index"))
return form(
{"email": user.email, "name": user.get_full_name()},
"users/confirm_email.html",
request
)
@login_required
@can_edit(User)
def resend_confirmation_email(request, logged_user, userid):
""" Renvoi du mail de confirmation """
try:
user = User.objects.get(
id=userid,
email_state__in=[User.EMAIL_STATE_PENDING, User.EMAIL_STATE_UNVERIFIED],
)
except User.DoesNotExist:
messages.error(request, _("The user doesn't exist."))
if request.method == "POST":
user.confirm_email_address_mail(request)
messages.success(request, _("An email to confirm your address was sent."))
return redirect(reverse("users:profil", kwargs={"userid": userid}))
return form(
{"email": user.email},
"users/resend_confirmation_email.html",
request
)
@login_required
def initial_register(request):
switch_ip = request.GET.get("switch_ip", None)