diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b9020e1..fd4e0177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -120,3 +120,33 @@ Don't forget to run migrations, several settings previously in the `preferences` in their own Payment models. To have a closer look on how the payments works, please go to the wiki. + +## MR 182: Add role models + +Adds the Role model. +You need to ensure that your database character set is utf-8. +```sql +ALTER DATABASE re2o CHARACTER SET utf8; +``` + +## MR 247: Fix des comptes mails + +Fix several issues with email accounts, you need to collect the static files. + +```bash +./manage.py collectstatic +``` + +## MR 203 Add custom invoices + +The custom invoices are now stored in database. You need to migrate your database : + +```bash +python3 manage.py migrate +``` + +On some database engines (postgreSQL) you also need to update the id sequences: + +```bash +python3 manage.py sqlsequencereset cotisations | python3 manage.py dbshell +``` diff --git a/api/serializers.py b/api/serializers.py index 398f2b19..65a82eb6 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -338,6 +338,7 @@ class OptionalMachineSerializer(NamespacedHMSerializer): class OptionalTopologieSerializer(NamespacedHMSerializer): """Serialize `preferences.models.OptionalTopologie` objects. """ + class Meta: model = preferences.OptionalTopologie fields = ('radius_general_policy', 'vlan_decision_ok', @@ -469,10 +470,10 @@ class SwitchPortSerializer(NamespacedHMSerializer): class Meta: model = topologie.Port fields = ('switch', 'port', 'room', 'machine_interface', 'related', - 'radius', 'vlan_force', 'details', 'api_url') + 'custom_profile', 'state', 'details', 'api_url') extra_kwargs = { 'related': {'view_name': 'switchport-detail'}, - 'api_url': {'view_name': 'switchport-detail'} + 'api_url': {'view_name': 'switchport-detail'}, } @@ -484,6 +485,18 @@ class RoomSerializer(NamespacedHMSerializer): fields = ('name', 'details', 'api_url') +class PortProfileSerializer(NamespacedHMSerializer): + vlan_untagged = VlanSerializer(read_only=True) + + class Meta: + model = topologie.PortProfile + fields = ('name', 'profil_default', 'vlan_untagged', 'vlan_tagged', + 'radius_type', 'radius_mode', 'speed', 'mac_limit', + 'flow_control', 'dhcp_snooping', 'dhcpv6_snooping', + 'arp_protect', 'ra_guard', 'loop_protect', 'vlan_untagged', + 'vlan_tagged') + + # USERS @@ -534,11 +547,20 @@ class AdherentSerializer(NamespacedHMSerializer): fields = ('name', 'surname', 'pseudo', 'email', 'local_email_redirect', 'local_email_enabled', 'school', 'shell', 'comment', 'state', 'registered', 'telephone', 'room', 'solde', - 'access', 'end_access', 'uid', 'api_url') + 'access', 'end_access', 'uid', 'api_url','gid') extra_kwargs = { 'shell': {'view_name': 'shell-detail'} } +class HomeCreationSerializer(NamespacedHMSerializer): + """Serialize 'users.models.User' minimal infos to create home + """ + uid = serializers.IntegerField(source='uid_number') + gid = serializers.IntegerField(source='gid_number') + + class Meta: + model = users.User + fields = ('pseudo', 'uid', 'gid') class ServiceUserSerializer(NamespacedHMSerializer): """Serialize `users.models.ServiceUser` objects. @@ -599,7 +621,7 @@ class WhitelistSerializer(NamespacedHMSerializer): class EMailAddressSerializer(NamespacedHMSerializer): """Serialize `users.models.EMailAddress` objects. """ - + user = serializers.CharField(source='user.pseudo', read_only=True) class Meta: model = users.EMailAddress fields = ('user', 'local_part', 'complete_email_address', 'api_url') @@ -635,9 +657,42 @@ class LocalEmailUsersSerializer(NamespacedHMSerializer): class Meta: model = users.User fields = ('local_email_enabled', 'local_email_redirect', - 'email_address') + 'email_address', 'email') +#Firewall + +class FirewallPortListSerializer(serializers.ModelSerializer): + class Meta: + model = machines.OuverturePort + fields = ('begin', 'end', 'protocole', 'io', 'show_port') + +class FirewallOuverturePortListSerializer(serializers.ModelSerializer): + tcp_ports_in = FirewallPortListSerializer(many=True, read_only=True) + udp_ports_in = FirewallPortListSerializer(many=True, read_only=True) + tcp_ports_out = FirewallPortListSerializer(many=True, read_only=True) + udp_ports_out = FirewallPortListSerializer(many=True, read_only=True) + + class Meta: + model = machines.OuverturePortList + fields = ('tcp_ports_in', 'udp_ports_in', 'tcp_ports_out', 'udp_ports_out') + +class SubnetPortsOpenSerializer(serializers.ModelSerializer): + ouverture_ports = FirewallOuverturePortListSerializer(read_only=True) + + class Meta: + model = machines.IpType + fields = ('type', 'domaine_ip_start', 'domaine_ip_stop', 'complete_prefixv6', 'ouverture_ports') + +class InterfacePortsOpenSerializer(serializers.ModelSerializer): + port_lists = FirewallOuverturePortListSerializer(read_only=True, many=True) + ipv4 = serializers.CharField(source='ipv4.ipv4', read_only=True) + ipv6 = Ipv6ListSerializer(many=True, read_only=True) + + class Meta: + model = machines.Interface + fields = ('port_lists', 'ipv4', 'ipv6') + # DHCP @@ -679,7 +734,7 @@ class NSRecordSerializer(NsSerializer): """Serialize `machines.models.Ns` objects with the data needed to generate a NS DNS record. """ - target = serializers.CharField(source='ns.name', read_only=True) + target = serializers.CharField(source='ns', read_only=True) class Meta(NsSerializer.Meta): fields = ('target',) @@ -689,7 +744,7 @@ class MXRecordSerializer(MxSerializer): """Serialize `machines.models.Mx` objects with the data needed to generate a MX DNS record. """ - target = serializers.CharField(source='name.name', read_only=True) + target = serializers.CharField(source='name', read_only=True) class Meta(MxSerializer.Meta): fields = ('target', 'priority') @@ -761,13 +816,12 @@ class CNAMERecordSerializer(serializers.ModelSerializer): """Serialize `machines.models.Domain` objects with the data needed to generate a CNAME DNS record. """ - alias = serializers.CharField(source='cname.name', read_only=True) + alias = serializers.CharField(source='cname', read_only=True) hostname = serializers.CharField(source='name', read_only=True) - extension = serializers.CharField(source='extension.name', read_only=True) class Meta: model = machines.Domain - fields = ('alias', 'hostname', 'extension') + fields = ('alias', 'hostname') class DNSZonesSerializer(serializers.ModelSerializer): @@ -792,6 +846,25 @@ class DNSZonesSerializer(serializers.ModelSerializer): 'aaaa_records', 'cname_records', 'sshfp_records') +class DNSReverseZonesSerializer(serializers.ModelSerializer): + """Serialize the data about DNS Zones. + """ + soa = SOARecordSerializer(source='extension.soa') + extension = serializers.CharField(source='extension.name', read_only=True) + cidrs = serializers.ListField(child=serializers.CharField(), source='ip_set_cidrs_as_str', read_only=True) + ns_records = NSRecordSerializer(many=True, source='extension.ns_set') + mx_records = MXRecordSerializer(many=True, source='extension.mx_set') + txt_records = TXTRecordSerializer(many=True, source='extension.txt_set') + ptr_records = ARecordSerializer(many=True, source='get_associated_ptr_records') + ptr_v6_records = AAAARecordSerializer(many=True, source='get_associated_ptr_v6_records') + + + class Meta: + model = machines.IpType + fields = ('type', 'extension', 'soa', 'ns_records', 'mx_records', + 'txt_records', 'ptr_records', 'ptr_v6_records', 'cidrs', + 'prefix_v6', 'prefix_v6_length') + # MAILING @@ -799,7 +872,7 @@ class MailingMemberSerializer(UserSerializer): """Serialize the data about a mailing member. """ class Meta(UserSerializer.Meta): - fields = ('name', 'pseudo', 'email') + fields = ('name', 'pseudo', 'get_mail') class MailingSerializer(ClubSerializer): """Serialize the data about a mailing. diff --git a/api/urls.py b/api/urls.py index 7ee36073..abc466e1 100644 --- a/api/urls.py +++ b/api/urls.py @@ -81,10 +81,12 @@ router.register_viewset(r'topologie/modelswitch', views.ModelSwitchViewSet) router.register_viewset(r'topologie/constructorswitch', views.ConstructorSwitchViewSet) router.register_viewset(r'topologie/switchbay', views.SwitchBayViewSet) router.register_viewset(r'topologie/building', views.BuildingViewSet) -router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') +router.register(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') router.register_viewset(r'topologie/room', views.RoomViewSet) +router.register(r'topologie/portprofile', views.PortProfileViewSet) # USERS router.register_viewset(r'users/user', views.UserViewSet) +router.register_viewset(r'users/homecreation', views.HomeCreationViewSet) router.register_viewset(r'users/club', views.ClubViewSet) router.register_viewset(r'users/adherent', views.AdherentViewSet) router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet) @@ -100,8 +102,12 @@ router.register_viewset(r'services/regen', views.ServiceRegenViewSet, base_name= router.register_view(r'dhcp/hostmacip', views.HostMacIpView), # LOCAL EMAILS router.register_view(r'localemail/users', views.LocalEmailUsersView), +# Firewall +router.register_view(r'firewall/subnet-ports', views.SubnetPortsOpenView), +router.register_view(r'firewall/interface-ports', views.InterfacePortsOpenView), # DNS router.register_view(r'dns/zones', views.DNSZonesView), +router.register_view(r'dns/reverse-zones', views.DNSReverseZonesView), # MAILING router.register_view(r'mailing/standard', views.StandardMailingView), router.register_view(r'mailing/club', views.ClubMailingView), diff --git a/api/views.py b/api/views.py index ef083edf..0f6301bc 100644 --- a/api/views.py +++ b/api/views.py @@ -403,6 +403,12 @@ class RoomViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = serializers.RoomSerializer +class PortProfileViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.PortProfile` objects. + """ + queryset = topologie.PortProfile.objects.all() + serializer_class = serializers.PortProfileSerializer + # USER @@ -412,6 +418,11 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): queryset = users.User.objects.all() serializer_class = serializers.UserSerializer +class HomeCreationViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes infos of `users.models.Users` objects to create homes. + """ + queryset = users.User.objects.all() + serializer_class = serializers.HomeCreationSerializer class ClubViewSet(viewsets.ReadOnlyModelViewSet): """Exposes list and details of `users.models.Club` objects. @@ -532,11 +543,21 @@ class HostMacIpView(generics.ListAPIView): serializer_class = serializers.HostMacIpSerializer +#Firewall + +class SubnetPortsOpenView(generics.ListAPIView): + queryset = machines.IpType.objects.all() + serializer_class = serializers.SubnetPortsOpenSerializer + +class InterfacePortsOpenView(generics.ListAPIView): + queryset = machines.Interface.objects.filter(port_lists__isnull=False).distinct() + serializer_class = serializers.InterfacePortsOpenSerializer + # DNS class DNSZonesView(generics.ListAPIView): - """Exposes the detailed information about each extension (hostnames, + """Exposes the detailed information about each extension (hostnames, IPs, DNS records, etc.) in order to build the DNS zone files. """ queryset = (machines.Extension.objects @@ -549,6 +570,15 @@ class DNSZonesView(generics.ListAPIView): .all()) serializer_class = serializers.DNSZonesSerializer +class DNSReverseZonesView(generics.ListAPIView): + """Exposes the detailed information about each extension (hostnames, + IPs, DNS records, etc.) in order to build the DNS zone files. + """ + queryset = (machines.IpType.objects.all()) + serializer_class = serializers.DNSReverseZonesSerializer + + + # MAILING diff --git a/cotisations/acl.py b/cotisations/acl.py index aa98c32a..06c62fb8 100644 --- a/cotisations/acl.py +++ b/cotisations/acl.py @@ -42,4 +42,5 @@ def can_view(user): if can: return can, None else: - return can, _("You don't have the rights to see this application.") + return can, _("You don't have the right to view this application.") + diff --git a/cotisations/admin.py b/cotisations/admin.py index 587bc066..afe4621c 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,6 +30,7 @@ from django.contrib import admin from reversion.admin import VersionAdmin from .models import Facture, Article, Banque, Paiement, Cotisation, Vente +from .models import CustomInvoice class FactureAdmin(VersionAdmin): @@ -37,6 +38,11 @@ class FactureAdmin(VersionAdmin): pass +class CustomInvoiceAdmin(VersionAdmin): + """Admin class for custom invoices.""" + pass + + class VenteAdmin(VersionAdmin): """Class admin d'une vente, tous les champs (facture related)""" pass @@ -69,3 +75,4 @@ admin.site.register(Banque, BanqueAdmin) admin.site.register(Paiement, PaiementAdmin) admin.site.register(Vente, VenteAdmin) admin.site.register(Cotisation, CotisationAdmin) +admin.site.register(CustomInvoice, CustomInvoiceAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 7ad9e413..9194597a 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -40,13 +40,13 @@ from django import forms from django.db.models import Q from django.forms import ModelForm, Form from django.core.validators import MinValueValidator -from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_lazy as _l + +from django.utils.translation import ugettext_lazy as _ from django.shortcuts import get_object_or_404 from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin -from .models import Article, Paiement, Facture, Banque +from .models import Article, Paiement, Facture, Banque, CustomInvoice from .payment_methods import balance @@ -84,71 +84,36 @@ class FactureForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): return cleaned_data -class SelectUserArticleForm(FormRevMixin, Form): +class SelectArticleForm(FormRevMixin, Form): """ Form used to select an article during the creation of an invoice for a member. """ article = forms.ModelChoiceField( - queryset=Article.objects.filter( - Q(type_user='All') | Q(type_user='Adherent') - ), - label=_l("Article"), + queryset=Article.objects.none(), + label=_("Article"), required=True ) quantity = forms.IntegerField( - label=_l("Quantity"), + label=_("Quantity"), validators=[MinValueValidator(1)], required=True ) def __init__(self, *args, **kwargs): user = kwargs.pop('user') - super(SelectUserArticleForm, self).__init__(*args, **kwargs) - self.fields['article'].queryset = Article.find_allowed_articles(user) + target_user = kwargs.pop('target_user') + super(SelectArticleForm, self).__init__(*args, **kwargs) + self.fields['article'].queryset = Article.find_allowed_articles(user, target_user) -class SelectClubArticleForm(Form): +class CustomInvoiceForm(FormRevMixin, ModelForm): """ - Form used to select an article during the creation of an invoice for a - club. + Form used to create a custom invoice. """ - article = forms.ModelChoiceField( - queryset=Article.objects.filter( - Q(type_user='All') | Q(type_user='Club') - ), - label=_l("Article"), - required=True - ) - quantity = forms.IntegerField( - label=_l("Quantity"), - validators=[MinValueValidator(1)], - required=True - ) - - def __init__(self, user, *args, **kwargs): - super(SelectClubArticleForm, self).__init__(*args, **kwargs) - self.fields['article'].queryset = Article.find_allowed_articles(user) - - -# TODO : change Facture to Invoice -class NewFactureFormPdf(Form): - """ - Form used to create a custom PDF invoice. - """ - paid = forms.BooleanField(label=_l("Paid"), required=False) - # TODO : change dest field to recipient - dest = forms.CharField( - required=True, - max_length=255, - label=_l("Recipient") - ) - # TODO : change chambre field to address - chambre = forms.CharField( - required=False, - max_length=10, - label=_l("Address") - ) + class Meta: + model = CustomInvoice + fields = '__all__' class ArticleForm(FormRevMixin, ModelForm): @@ -172,7 +137,7 @@ class DelArticleForm(FormRevMixin, Form): """ articles = forms.ModelMultipleChoiceField( queryset=Article.objects.none(), - label=_l("Existing articles"), + label=_("Available articles"), widget=forms.CheckboxSelectMultiple ) @@ -212,7 +177,7 @@ class DelPaiementForm(FormRevMixin, Form): # TODO : change paiement to payment paiements = forms.ModelMultipleChoiceField( queryset=Paiement.objects.none(), - label=_l("Existing payment method"), + label=_("Available payment methods"), widget=forms.CheckboxSelectMultiple ) @@ -250,7 +215,7 @@ class DelBanqueForm(FormRevMixin, Form): # TODO : change banque to bank banques = forms.ModelMultipleChoiceField( queryset=Banque.objects.none(), - label=_l("Existing banks"), + label=_("Available banks"), widget=forms.CheckboxSelectMultiple ) @@ -269,21 +234,21 @@ class RechargeForm(FormRevMixin, Form): Form used to refill a user's balance """ value = forms.FloatField( - label=_l("Amount"), + label=_("Amount"), min_value=0.01, validators=[] ) payment = forms.ModelChoiceField( queryset=Paiement.objects.none(), - label=_l("Payment method") + label=_("Payment method") ) - def __init__(self, *args, user=None, **kwargs): + def __init__(self, *args, user=None, user_source=None, **kwargs): self.user = user super(RechargeForm, self).__init__(*args, **kwargs) self.fields['payment'].empty_label = \ _("Select a payment method") - self.fields['payment'].queryset = Paiement.find_allowed_payments(user) + self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True) def clean(self): """ @@ -301,3 +266,4 @@ class RechargeForm(FormRevMixin, Form): } ) return self.cleaned_data + diff --git a/cotisations/locale/fr/LC_MESSAGES/django.mo b/cotisations/locale/fr/LC_MESSAGES/django.mo index 6f295ccb..b5a65357 100644 Binary files a/cotisations/locale/fr/LC_MESSAGES/django.mo and b/cotisations/locale/fr/LC_MESSAGES/django.mo differ diff --git a/cotisations/locale/fr/LC_MESSAGES/django.po b/cotisations/locale/fr/LC_MESSAGES/django.po index f13cc8d4..129d4d72 100644 --- a/cotisations/locale/fr/LC_MESSAGES/django.po +++ b/cotisations/locale/fr/LC_MESSAGES/django.po @@ -21,430 +21,711 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-05-10 15:21-0500\n" +"POT-Creation-Date: 2018-08-18 13:17+0200\n" "PO-Revision-Date: 2018-03-31 16:09+0002\n" -"Last-Translator: Maël Kervella \n" -"Language-Team: \n" +"Last-Translator: Laouen Fernet \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: acl.py:45 -msgid "You don't have the rights to see this application." -msgstr "Vous n'avez pas les droits de voir cette application." +msgid "You don't have the right to view this application." +msgstr "Vous n'avez pas le droit de voir cette application." -#: forms.py:63 forms.py:321 -msgid "Cheque number" -msgstr "Numéro de chèque" - -#: forms.py:64 forms.py:322 -msgid "Not specified" -msgstr "Non renseigné" - -#: forms.py:66 forms.py:324 +#: forms.py:63 forms.py:274 msgid "Select a payment method" msgstr "Sélectionnez un moyen de paiement" -#: forms.py:83 forms.py:347 -msgid "A payment method must be specified." -msgstr "Un moyen de paiement doit être renseigné." - -#: forms.py:87 forms.py:352 -msgid "A cheque number and a bank must be specified." -msgstr "Un numéro de chèqe et une banque doivent être renseignés." - -#: forms.py:184 +#: forms.py:66 models.py:510 msgid "Member" msgstr "Adhérent" -#: forms.py:186 +#: forms.py:68 msgid "Select the proprietary member" msgstr "Sélectionnez l'adhérent propriétaire" -#: forms.py:187 +#: forms.py:69 msgid "Validated invoice" msgstr "Facture validée" -#: forms.py:201 +#: forms.py:82 +msgid "A payment method must be specified." +msgstr "Un moyen de paiement doit être renseigné." + +#: forms.py:96 forms.py:120 templates/cotisations/aff_article.html:33 +#: templates/cotisations/facture.html:61 +msgid "Article" +msgstr "Article" + +#: forms.py:100 forms.py:124 templates/cotisations/edit_facture.html:46 +msgid "Quantity" +msgstr "Quantité" + +#: forms.py:154 msgid "Article name" msgstr "Nom de l'article" -#: forms.py:239 +#: forms.py:164 templates/cotisations/sidebar.html:50 +msgid "Available articles" +msgstr "Articles disponibles" + +#: forms.py:192 msgid "Payment method name" msgstr "Nom du moyen de paiement" -#: forms.py:240 -msgid "Payment type" -msgstr "Type de paiement" +#: forms.py:204 +msgid "Available payment methods" +msgstr "Moyens de paiement disponibles" -#: forms.py:242 -msgid "" -"The payement type is used for specific behaviour. The \"cheque\" " -"type means a cheque number and a bank name may be added when " -"using this payment method." -msgstr "" -"Le type de paiement est utilisé pour des comportements spécifiques. Le type " -"\"chèque\" permet de spécifier un numéro de chèque et une banque lors de " -"l'utilisation de cette méthode." - -#: forms.py:282 +#: forms.py:230 msgid "Bank name" msgstr "Nom de la banque" -#: forms.py:380 -#, python-format -msgid "" -"Requested amount is too small. Minimum amount possible : " -"%(min_online_amount)s €." -msgstr "" -"Montant demandé est trop faible. Montant minimal possible : " -"%(min_online_amount)s €" +#: forms.py:242 +msgid "Available banks" +msgstr "Banques disponibles" -#: forms.py:390 +#: forms.py:261 +msgid "Amount" +msgstr "Montant" + +#: forms.py:267 templates/cotisations/aff_cotisations.html:44 +#: templates/cotisations/aff_custom_invoice.html:42 +#: templates/cotisations/control.html:66 +msgid "Payment method" +msgstr "Moyen de paiement" + +#: forms.py:287 #, python-format msgid "" "Requested amount is too high. Your balance can't exceed " "%(max_online_balance)s €." msgstr "" -"Montant demandé trop grand. Votre solde ne peut excéder " -"%(max_online_balance)s €" +"Le montant demandé trop grand. Votre solde ne peut excéder " +"%(max_online_balance)s €." -#: models.py:165 models.py:213 +#: models.py:60 templates/cotisations/aff_cotisations.html:48 +#: templates/cotisations/aff_custom_invoice.html:46 +#: templates/cotisations/control.html:70 +msgid "Date" +msgstr "Date" + +#: models.py:136 +msgid "cheque number" +msgstr "numéro de chèque" + +#: models.py:141 +msgid "validated" +msgstr "validée" + +#: models.py:146 +msgid "controlled" +msgstr "contrôlée" + +#: models.py:154 +msgid "Can edit the \"controlled\" state" +msgstr "Peut modifier l'état \"contrôlé\"" + +#: models.py:156 +msgid "Can view an invoice object" +msgstr "Peut voir un objet facture" + +#: models.py:158 +msgid "Can edit all the previous invoices" +msgstr "Peut modifier toutes les factures existantes" + +#: models.py:160 models.py:305 +msgid "invoice" +msgstr "facture" + +#: models.py:161 +msgid "invoices" +msgstr "factures" + +#: models.py:170 msgid "You don't have the right to edit an invoice." msgstr "Vous n'avez pas le droit de modifier une facture." -#: models.py:168 +#: models.py:173 msgid "You don't have the right to edit this user's invoices." -msgstr "Vous n'avez pas le droit de modifier les facture de cette utilisateur." +msgstr "Vous n'avez pas le droit de modifier les factures de cet utilisateur." -#: models.py:172 +#: models.py:177 msgid "" "You don't have the right to edit an invoice already controlled or " "invalidated." msgstr "" -"Vous n'avez pas le droit de modifier une facture précedement controllée ou " +"Vous n'avez pas le droit de modifier une facture précedemment contrôlée ou " "invalidée." -#: models.py:179 +#: models.py:184 msgid "You don't have the right to delete an invoice." msgstr "Vous n'avez pas le droit de supprimer une facture." -#: models.py:181 +#: models.py:186 msgid "You don't have the right to delete this user's invoices." msgstr "Vous n'avez pas le droit de supprimer les factures de cet utilisateur." -#: models.py:184 +#: models.py:189 msgid "" "You don't have the right to delete an invoice already controlled or " "invalidated." msgstr "" -"Vous n'avez pas le droit de supprimer une facture précedement controllée ou " +"Vous n'avez pas le droit de supprimer une facture précedement contrôlée ou " "invalidée." -#: models.py:192 -msgid "You don't have the right to see someone else's invoices history." +#: models.py:197 +msgid "You don't have the right to view someone else's invoices history." msgstr "" -"Vous n'avez pas le droit de voir l'historique de la facture de quelqu'un " -"d'autre." +"Vous n'avez pas le droit de voir l'historique des factures d'un autre " +"utilisateur." -#: models.py:195 +#: models.py:200 msgid "The invoice has been invalidated." msgstr "La facture a été invalidée." -#: models.py:205 -#, fuzzy -#| msgid "You don't have the right to edit the controlled state." +#: models.py:210 msgid "You don't have the right to edit the \"controlled\" state." -msgstr "Vous n'avez pas le droit de modifier l'état \"controllé\"." +msgstr "Vous n'avez pas le droit de modifier le statut \"contrôlé\"." -#: models.py:372 -msgid "A cotisation should always have a duration." -msgstr "Une cotisation devrait toujours avoir une durée." +#: models.py:224 +msgid "There are no payment method which you can use." +msgstr "Il n'y a pas de moyen de paiement que vous puissiez utiliser." -#: models.py:379 +#: models.py:226 +msgid "There are no article that you can buy." +msgstr "Il n'y a pas d'article que vous puissiez acheter." + +#: models.py:261 +msgid "Can view a custom invoice object" +msgstr "Peut voir un objet facture personnalisée" + +#: models.py:265 templates/cotisations/aff_custom_invoice.html:36 +msgid "Recipient" +msgstr "Destinataire" + +#: models.py:269 templates/cotisations/aff_paiement.html:33 +msgid "Payment type" +msgstr "Type de paiement" + +#: models.py:273 +msgid "Address" +msgstr "Adresse" + +#: models.py:276 templates/cotisations/aff_custom_invoice.html:54 +msgid "Paid" +msgstr "Payé" + +#: models.py:296 models.py:516 models.py:764 +msgid "Connection" +msgstr "Connexion" + +#: models.py:297 models.py:517 models.py:765 +msgid "Membership" +msgstr "Adhésion" + +#: models.py:298 models.py:512 models.py:518 models.py:766 +msgid "Both of them" +msgstr "Les deux" + +#: models.py:310 +msgid "amount" +msgstr "montant" + +#: models.py:315 +msgid "article" +msgstr "article" + +#: models.py:322 +msgid "price" +msgstr "prix" + +#: models.py:327 models.py:535 +msgid "duration (in months)" +msgstr "durée (en mois)" + +#: models.py:335 models.py:549 models.py:780 +msgid "subscription type" +msgstr "type de cotisation" + +#: models.py:340 +msgid "Can view a purchase object" +msgstr "Peut voir un objet achat" + +#: models.py:341 +msgid "Can edit all the previous purchases" +msgstr "Peut modifier tous les achats précédents" + +#: models.py:343 models.py:774 +msgid "purchase" +msgstr "achat" + +#: models.py:344 +msgid "purchases" +msgstr "achats" + +#: models.py:411 models.py:573 +msgid "Duration must be specified for a subscription." +msgstr "La durée de la cotisation doit être indiquée." + +#: models.py:418 msgid "You don't have the right to edit the purchases." msgstr "Vous n'avez pas le droit de modifier les achats." -#: models.py:384 +#: models.py:423 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." -#: models.py:388 +#: models.py:427 msgid "" "You don't have the right to edit a purchase already controlled or " "invalidated." msgstr "" -"Vous n'avez pas le droit de modifier un achat précédement controllé ou " +"Vous n'avez pas le droit de modifier un achat précédemment contrôlé ou " "invalidé." -#: models.py:395 +#: models.py:434 msgid "You don't have the right to delete a purchase." msgstr "Vous n'avez pas le droit de supprimer un achat." -#: models.py:397 +#: models.py:436 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." -#: models.py:400 +#: models.py:439 msgid "" "You don't have the right to delete a purchase already controlled or " "invalidated." msgstr "" -"Vous n'avez pas le droit de supprimer un achat précédement controllé ou " +"Vous n'avez pas le droit de supprimer un achat précédement contrôlé ou " "invalidé." -#: models.py:408 -msgid "You don't have the right to see someone else's purchase history." +#: models.py:447 +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 d'un achat de quelqu'un " -"d'autre." +"Vous n'avez pas le droit de voir l'historique des achats d'un autre " +"utilisateur." -#: models.py:517 -msgid "Solde is a reserved article name" -msgstr "Solde est un nom d'article réservé" +#: models.py:511 +msgid "Club" +msgstr "Club" -#: models.py:521 -msgid "Duration must be specified for a cotisation" -msgstr "La durée doit être spécifiée pour une cotisation" +#: models.py:523 +msgid "designation" +msgstr "désignation" -#: models.py:603 -msgid "You cannot have multiple payment method of type cheque" -msgstr "Vous ne pouvez avoir plusieurs moyens de paiement de type chèque" +#: models.py:529 +msgid "unit price" +msgstr "prix unitaire" + +#: models.py:541 +msgid "type of users concerned" +msgstr "type d'utilisateurs concernés" + +#: models.py:553 models.py:649 +msgid "is available for every user" +msgstr "est disponible pour chaque utilisateur" + +#: models.py:560 +msgid "Can view an article object" +msgstr "Peut voir un objet article" + +#: models.py:561 +msgid "Can buy every article" +msgstr "Peut acheter chaque article" + +#: models.py:569 +msgid "Balance is a reserved article name." +msgstr "Solde est un nom d'article réservé." + +#: models.py:594 +msgid "You can't buy this article." +msgstr "Vous ne pouvez pas acheter cet article." + +#: models.py:624 +msgid "Can view a bank object" +msgstr "Peut voir un objet banque" + +#: models.py:626 +msgid "bank" +msgstr "banque" + +#: models.py:627 +msgid "banks" +msgstr "banques" + +#: models.py:645 +msgid "method" +msgstr "moyen" #: models.py:654 -msgid "You don't have the right to edit a cotisation." -msgstr "Vous n'avez pas le droit de modifier une cotisation." +msgid "is user balance" +msgstr "est solde utilisateur" -#: models.py:658 -msgid "" -"You don't have the right to edit a cotisation already controlled or " -"invalidated." -msgstr "" -"Vous n'avez pas le droit de modifier une cotisaiton précédement controllée " -"ou invalidée." +#: models.py:655 +msgid "There should be only one balance payment method." +msgstr "Il ne devrait y avoir qu'un moyen de paiement solde." + +#: models.py:661 +msgid "Can view a payment method object" +msgstr "Peut voir un objet moyen de paiement" + +#: models.py:662 +msgid "Can use every payment method" +msgstr "Peut utiliser chaque moyen de paiement" + +#: models.py:664 +msgid "payment method" +msgstr "moyen de paiement" #: models.py:665 -msgid "You don't have the right to delete a cotisation." -msgstr "Vous n'avez pas le droit de supprimer une cotisation." +msgid "payment methods" +msgstr "moyens de paiement" -#: models.py:668 +#: models.py:699 payment_methods/comnpay/views.py:63 +#, 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." + +#: models.py:709 +msgid "The invoice was created." +msgstr "La facture a été créée." + +#: models.py:730 +msgid "You can't use this payment method." +msgstr "Vous ne pouvez pas utiliser ce moyen de paiement." + +#: models.py:748 +msgid "No custom payment method." +msgstr "Pas de moyen de paiement personnalisé." + +#: models.py:783 +msgid "start date" +msgstr "date de début" + +#: models.py:786 +msgid "end date" +msgstr "date de fin" + +#: models.py:791 +msgid "Can view a subscription object" +msgstr "Peut voir un objet cotisation" + +#: models.py:792 +msgid "Can edit the previous subscriptions" +msgstr "Peut modifier les cotisations précédentes" + +#: models.py:794 +msgid "subscription" +msgstr "cotisation" + +#: models.py:795 +msgid "subscriptions" +msgstr "cotisations" + +#: models.py:799 +msgid "You don't have the right to edit a subscription." +msgstr "Vous n'avez pas le droit de modifier une cotisation." + +#: models.py:803 msgid "" -"You don't have the right to delete a cotisation already controlled or " +"You don't have the right to edit a subscription already controlled or " "invalidated." msgstr "" -"Vous n'avez pas le droit de supprimer une cotisation précédement controllée " +"Vous n'avez pas le droit de modifier une cotisation précédemment contrôlée " "ou invalidée." -#: models.py:676 -msgid "You don't have the right to see someone else's cotisation history." -msgstr "" -"Vous n'avez pas le droit de voir l'historique d'une cotisation de quelqu'un " -"d'autre." +#: models.py:810 +msgid "You don't have the right to delete a subscription." +msgstr "Vous n'avez pas le droit de supprimer une cotisation." -#: payment.py:31 +#: models.py:813 +msgid "" +"You don't have the right to delete a subscription already controlled or " +"invalidated." +msgstr "" +"Vous n'avez pas le droit de supprimer une cotisation précédemment contrôlée " +"ou invalidée." + +#: models.py:821 +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 " +"utilisateur." + +#: payment_methods/balance/models.py:38 +msgid "user balance" +msgstr "solde utilisateur" + +#: payment_methods/balance/models.py:47 +msgid "Minimum balance" +msgstr "Solde minimum" + +#: payment_methods/balance/models.py:48 +msgid "" +"The minimal amount of money allowed for the balance at the end of a payment. " +"You can specify negative amount." +msgstr "" +"Le montant minimal d'argent autorisé pour le solde à la fin d'un paiement. " +"Vous pouvez renseigner un montant négatif." + +#: payment_methods/balance/models.py:57 +msgid "Maximum balance" +msgstr "Solde maximum" + +#: payment_methods/balance/models.py:58 +msgid "The maximal amount of money allowed for the balance." +msgstr "Le montant maximal d'argent autorisé pour le solde." + +#: payment_methods/balance/models.py:66 +msgid "Allow user to credit their balance" +msgstr "Autorise l'utilisateur à créditer son solde" + +#: payment_methods/balance/models.py:81 payment_methods/balance/models.py:112 +msgid "Your balance is too low for this operation." +msgstr "Votre solde est trop bas pour cette opération." + +#: payment_methods/balance/models.py:99 validators.py:20 +msgid "There is already a payment method for user balance." +msgstr "Il y a déjà un moyen de paiement pour le solde utilisateur." + +#: payment_methods/cheque/models.py:36 +msgid "Cheque" +msgstr "Chèque" + +#: payment_methods/cheque/views.py:47 +msgid "You can't pay this invoice with a cheque." +msgstr "Vous ne pouvez pas payer cette facture avec un chèque." + +#: payment_methods/comnpay/models.py:39 +msgid "ComNpay" +msgstr "ComNpay" + +#: payment_methods/comnpay/models.py:51 +msgid "ComNpay VAT Number" +msgstr "Numéro de TVA de ComNpay" + +#: payment_methods/comnpay/models.py:57 +msgid "ComNpay secret key" +msgstr "Clé secrète de ComNpay" + +#: payment_methods/comnpay/models.py:60 +msgid "Minimum payment" +msgstr "Paiement minimum" + +#: payment_methods/comnpay/models.py:61 +msgid "The minimal amount of money you have to use when paying with ComNpay" +msgstr "" +"Le montant minimal d'agent que vous devez utiliser en payant avec ComNpay" + +#: payment_methods/comnpay/models.py:69 +msgid "Production mode enabled (production URL, instead of homologation)" +msgstr "Mode production activé (URL de production, au lieu d'homologation)" + +#: payment_methods/comnpay/models.py:104 +msgid "Pay invoice number " +msgstr "Payer la facture numéro " + +#: payment_methods/comnpay/models.py:116 +msgid "" +"In order to pay your invoice with ComNpay, the price must be greater than {} " +"€." +msgstr "" +"Pour payer votre facture avec ComNpay, le prix doit être plus grand que {} €." + +#: payment_methods/comnpay/views.py:53 #, python-format -msgid "The payment of %(amount)s € has been accepted." +msgid "The payment of %(amount)s € was accepted." msgstr "Le paiement de %(amount)s € a été accepté." -#: payment.py:49 -msgid "The payment has been refused." +#: payment_methods/comnpay/views.py:84 +msgid "The payment was refused." msgstr "Le paiment a été refusé." -#: templates/cotisations/aff_article.html:31 -#: templates/cotisations/facture.html:43 -#: templates/cotisations/new_facture.html:50 -#: templates/cotisations/new_facture_solde.html:44 -msgid "Article" -msgstr "Article" +#: payment_methods/forms.py:60 +msgid "Special payment method" +msgstr "Moyen de paiement spécial" -#: templates/cotisations/aff_article.html:32 +#: payment_methods/forms.py:61 +msgid "" +"Warning: you will not be able to change the payment method later. But you " +"will be allowed to edit the other options." +msgstr "" +"Attention : vous ne pourrez pas changer le moyen de paiement plus tard. Mais " +"vous pourrez modifier les autres options." + +#: payment_methods/forms.py:72 +msgid "no" +msgstr "non" + +#: templates/cotisations/aff_article.html:34 msgid "Price" msgstr "Prix" -#: templates/cotisations/aff_article.html:33 -msgid "Cotisation type" +#: templates/cotisations/aff_article.html:35 +msgid "Subscription type" msgstr "Type de cotisation" -#: templates/cotisations/aff_article.html:34 -msgid "Duration (month)" -msgstr "Durée (mois)" +#: templates/cotisations/aff_article.html:36 +msgid "Duration (in months)" +msgstr "Durée (en mois)" -#: templates/cotisations/aff_article.html:35 +#: templates/cotisations/aff_article.html:37 msgid "Concerned users" msgstr "Utilisateurs concernés" -#: templates/cotisations/aff_article.html:48 -#: templates/cotisations/aff_banque.html:40 -#: templates/cotisations/aff_cotisations.html:69 -#: templates/cotisations/aff_cotisations.html:75 -#: templates/cotisations/aff_paiement.html:40 -#: templates/cotisations/control.html:104 views.py:396 views.py:443 -#: views.py:507 views.py:585 +#: templates/cotisations/aff_article.html:38 +msgid "Available for everyone" +msgstr "Disponible pour tous" + +#: templates/cotisations/aff_article.html:52 +#: templates/cotisations/aff_paiement.html:48 +#: templates/cotisations/control.html:107 views.py:483 views.py:570 +#: views.py:650 msgid "Edit" msgstr "Modifier" -#: templates/cotisations/aff_article.html:52 -#: templates/cotisations/aff_banque.html:44 -#: templates/cotisations/aff_cotisations.html:90 -#: templates/cotisations/aff_paiement.html:44 -msgid "Historique" -msgstr "Historique" - -#: templates/cotisations/aff_banque.html:31 +#: templates/cotisations/aff_banque.html:32 msgid "Bank" msgstr "Banque" -#: templates/cotisations/aff_cotisations.html:37 +#: templates/cotisations/aff_cotisations.html:38 msgid "User" msgstr "Utilisateur" -#: templates/cotisations/aff_cotisations.html:40 -#: templates/cotisations/control.html:60 +#: templates/cotisations/aff_cotisations.html:41 +#: templates/cotisations/aff_custom_invoice.html:39 +#: templates/cotisations/control.html:63 #: templates/cotisations/edit_facture.html:45 msgid "Designation" msgstr "Désignation" -#: templates/cotisations/aff_cotisations.html:41 -#: templates/cotisations/control.html:61 +#: templates/cotisations/aff_cotisations.html:42 +#: templates/cotisations/aff_custom_invoice.html:40 +#: templates/cotisations/control.html:64 msgid "Total price" msgstr "Prix total" -#: templates/cotisations/aff_cotisations.html:43 -#: templates/cotisations/aff_paiement.html:31 -#: templates/cotisations/control.html:63 -msgid "Payment method" -msgstr "Moyen de paiement" +#: templates/cotisations/aff_cotisations.html:52 +#: templates/cotisations/aff_custom_invoice.html:50 +#: templates/cotisations/control.html:56 +msgid "Invoice ID" +msgstr "ID facture" -#: templates/cotisations/aff_cotisations.html:47 -#: templates/cotisations/control.html:67 -msgid "Date" -msgstr "Date" - -#: templates/cotisations/aff_cotisations.html:51 -#: templates/cotisations/control.html:53 -msgid "Invoice id" -msgstr "Id facture" - -#: templates/cotisations/aff_cotisations.html:79 +#: templates/cotisations/aff_cotisations.html:71 msgid "Controlled invoice" -msgstr "Facture controllé" +msgstr "Facture contrôlée" -#: templates/cotisations/aff_cotisations.html:84 views.py:464 views.py:542 -#: views.py:620 -msgid "Delete" -msgstr "Supprimer" - -#: templates/cotisations/aff_cotisations.html:99 +#: templates/cotisations/aff_cotisations.html:81 +#: templates/cotisations/aff_custom_invoice.html:79 msgid "PDF" msgstr "PDF" -#: templates/cotisations/aff_cotisations.html:102 +#: templates/cotisations/aff_cotisations.html:84 msgid "Invalidated invoice" msgstr "Facture invalidée" +#: templates/cotisations/aff_paiement.html:34 +msgid "Is available for everyone" +msgstr "Est disponible pour tous" + +#: templates/cotisations/aff_paiement.html:35 +msgid "Custom payment method" +msgstr "Moyen de paiement personnalisé" + #: templates/cotisations/control.html:30 msgid "Invoice control" msgstr "Contrôle des factures" -#: templates/cotisations/control.html:33 +#: templates/cotisations/control.html:34 msgid "Invoice control and validation" msgstr "Contrôle et validation des factures" -#: templates/cotisations/control.html:43 -msgid "Profil" +#: templates/cotisations/control.html:46 +msgid "Profile" msgstr "Profil" -#: templates/cotisations/control.html:45 +#: templates/cotisations/control.html:48 msgid "Last name" msgstr "Nom" -#: templates/cotisations/control.html:49 +#: templates/cotisations/control.html:52 msgid "First name" msgstr "Prénom" -#: templates/cotisations/control.html:57 -msgid "User id" -msgstr "Id utilisateur" +#: templates/cotisations/control.html:60 +msgid "User ID" +msgstr "ID utilisateur" -#: templates/cotisations/control.html:71 +#: templates/cotisations/control.html:74 msgid "Validated" msgstr "Validé" -#: templates/cotisations/control.html:75 +#: templates/cotisations/control.html:78 msgid "Controlled" -msgstr "Controllé" +msgstr "Contrôlé" #: templates/cotisations/delete.html:29 -msgid "Deletion of cotisations" -msgstr "Supprimer des cotisations" +msgid "Deletion of subscriptions" +msgstr "Suppression de cotisations" #: templates/cotisations/delete.html:36 #, python-format msgid "" -"\n" -" Warning. Are you sure you really want te delete this %(object_name)s " -"object ( %(objet)s ) ?\n" -" " +"Warning: are you sure you really want to delete this %(object_name)s object " +"( %(objet)s )?" msgstr "" -"\n" -" Attention. Êtes-vous vraiment sûr de vouloir supprimer cet objet " -"%(object_name)s ( %(objet)s ) ?\n" -" " +"\tAttention: voulez-vous vraiment supprimer cet objet %(object_name)s " +"( %(objet)s ) ?" -#: templates/cotisations/delete.html:40 +#: templates/cotisations/delete.html:38 #: templates/cotisations/edit_facture.html:60 -#: templates/cotisations/new_facture_solde.html:59 -#: templates/cotisations/recharge.html:42 msgid "Confirm" msgstr "Confirmer" #: templates/cotisations/edit_facture.html:31 #: templates/cotisations/facture.html:30 -#: templates/cotisations/new_facture.html:30 -#: templates/cotisations/new_facture_solde.html:30 -msgid "Invoices creation and edition" +msgid "Creation and editing of invoices" msgstr "Création et modification de factures" #: templates/cotisations/edit_facture.html:38 msgid "Edit the invoice" -msgstr "Edition de factures" +msgstr "Modifier la facture" #: templates/cotisations/edit_facture.html:41 -#: templates/cotisations/facture.html:38 -#: templates/cotisations/new_facture.html:46 -#: templates/cotisations/new_facture_solde.html:40 +#: templates/cotisations/facture.html:56 msgid "Invoice's articles" msgstr "Articles de la facture" -#: templates/cotisations/edit_facture.html:46 -msgid "Quantity" -msgstr "Quantité" +#: templates/cotisations/facture.html:37 +msgid "New invoice" +msgstr "Nouvelle facture" -#: templates/cotisations/facture.html:52 -#: templates/cotisations/new_facture.html:59 -#: templates/cotisations/new_facture_solde.html:53 +#: templates/cotisations/facture.html:40 +#, python-format +msgid "Maximum allowed balance: %(max_balance)s €" +msgstr "Solde maximum autorisé : %(max_balance)s €" + +#: templates/cotisations/facture.html:44 +#, python-format +msgid "Current balance: %(balance)s €" +msgstr "Solde actuel : %(balance)s €" + +#: templates/cotisations/facture.html:70 msgid "Add an article" msgstr "Ajouter un article" -#: templates/cotisations/facture.html:54 -#: templates/cotisations/new_facture.html:61 -#: templates/cotisations/new_facture_solde.html:55 -msgid "" -"\n" -" Total price : 0,00 €\n" -" " -msgstr "" -"\n" -" Prix total : 0,00 €\n" -" " +#: templates/cotisations/facture.html:72 +msgid "Total price: 0,00 €" +msgstr "Prix total : 0,00 €" #: templates/cotisations/index.html:29 templates/cotisations/sidebar.html:40 msgid "Invoices" msgstr "Factures" #: templates/cotisations/index.html:32 -msgid "Cotisations" +msgid "Subscriptions" msgstr "Cotisations" #: templates/cotisations/index_article.html:30 @@ -453,18 +734,18 @@ msgstr "Articles" #: templates/cotisations/index_article.html:33 msgid "Article types list" -msgstr "Liste des types d'articles" +msgstr "Liste des types d'article" #: templates/cotisations/index_article.html:36 msgid "Add an article type" msgstr "Ajouter un type d'article" #: templates/cotisations/index_article.html:40 -msgid "Delete article types" -msgstr "Supprimer des types d'articles" +msgid "Delete one or several article types" +msgstr "Supprimer un ou plusieurs types d'article" #: templates/cotisations/index_banque.html:30 -#: templates/cotisations/sidebar.html:50 +#: templates/cotisations/sidebar.html:55 msgid "Banks" msgstr "Banques" @@ -477,78 +758,48 @@ msgid "Add a bank" msgstr "Ajouter une banque" #: templates/cotisations/index_banque.html:40 -msgid "Delete banks" -msgstr "Supprimer des banques" +msgid "Delete one or several banks" +msgstr "Supprimer une ou plusieurs banques" + +#: templates/cotisations/index_custom_invoice.html:28 +#: templates/cotisations/sidebar.html:45 +msgid "Custom invoices" +msgstr "Factures personnalisées" + +#: templates/cotisations/index_custom_invoice.html:31 +msgid "Custom invoices list" +msgstr "Liste des factures personalisées" #: templates/cotisations/index_paiement.html:30 -msgid "Payments" -msgstr "Paiement" +#: templates/cotisations/sidebar.html:60 +msgid "Payment methods" +msgstr "Moyens de paiement" #: templates/cotisations/index_paiement.html:33 -msgid "Payment types list" -msgstr "Liste des types de paiement" +msgid "List of payment methods" +msgstr "Liste des moyens de paiement" #: templates/cotisations/index_paiement.html:36 -msgid "Add a payment type" -msgstr "Ajouter un type de paiement" +msgid "Add a payment method" +msgstr "Ajouter un moyen de paiement" #: templates/cotisations/index_paiement.html:40 -msgid "Delete payment types" -msgstr "Supprimer un type de paiement" +msgid "Delete one or several payment methods" +msgstr "Supprimer un ou plusieurs moyens de paiement" -#: templates/cotisations/new_facture.html:37 -#: templates/cotisations/new_facture_solde.html:37 -msgid "New invoice" -msgstr "Nouvelle facture" - -#: templates/cotisations/new_facture.html:39 -#, python-format -msgid "" -"\n" -" User's balance : %(user.solde)s €\n" -" " -msgstr "" -"\n" -" Solde de l'utilisateur : %(user.solde)s €\n" -" " - -#: templates/cotisations/new_facture.html:65 views.py:257 -msgid "Create" -msgstr "Créer" - -#: templates/cotisations/payment.html:30 templates/cotisations/recharge.html:30 -#: templates/cotisations/recharge.html:33 +#: templates/cotisations/payment.html:30 msgid "Balance refill" msgstr "Rechargement de solde" #: templates/cotisations/payment.html:34 #, python-format -msgid "" -"\n" -" Refill of %(amount)s €\n" -" " -msgstr "" -"\n" -" Recharger de %(amount)s €\n" -" " +msgid "Pay %(amount)s €" +msgstr "Recharger de %(amount)s €" -#: templates/cotisations/payment.html:40 +#: templates/cotisations/payment.html:42 views.py:870 msgid "Pay" msgstr "Payer" -#: templates/cotisations/recharge.html:35 -#, python-format -msgid "" -"\n" -" Balance : %(request.user.solde)s " -"€\n" -" " -msgstr "" -"\n" -" Solde : %(request.user.solde)s " -"€\n" -" " - #: templates/cotisations/sidebar.html:32 msgid "Create an invoice" msgstr "Créer une facture" @@ -557,106 +808,118 @@ msgstr "Créer une facture" msgid "Control the invoices" msgstr "Contrôler les factures" -#: templates/cotisations/sidebar.html:45 -msgid "Available articles" -msgstr "Articles disponibles" - -#: templates/cotisations/sidebar.html:55 -msgid "Payment methods" -msgstr "Moyens de paiement" - -#: views.py:138 -msgid "Your balance is too low for this operation." -msgstr "Votre solde est trop faible pour cette opération." - -#: views.py:168 -#, python-format -msgid "" -"The cotisation of %(member_name)s has been extended to " -"%(end_date)s." -msgstr "La cotisation de %(member_name)s a été étendu jusqu'à %(end_date)s." - -#: views.py:178 -msgid "The invoice has been created." -msgstr "La facture a été créée." - -#: views.py:186 views.py:824 +#: views.py:167 msgid "You need to choose at least one article." msgstr "Vous devez choisir au moins un article." -#: views.py:338 -msgid "The invoice has been successfully edited." -msgstr "La facture a été crée avec succès." +#: views.py:181 views.py:235 +msgid "Create" +msgstr "Créer" -#: views.py:358 -msgid "The invoice has been successfully deleted." -msgstr "La facture a été supprimée avec succès." +#: views.py:228 +msgid "The custom invoice was created." +msgstr "La facture personnalisée a été créée." -#: views.py:363 +#: views.py:316 views.py:370 +msgid "The invoice was edited." +msgstr "La facture a été modifiée." + +#: views.py:336 views.py:430 +msgid "The invoice was deleted." +msgstr "La facture a été supprimée." + +#: views.py:341 views.py:435 msgid "Invoice" msgstr "Facture" -#: views.py:391 -msgid "Balance successfully updated." -msgstr "Solde mis à jour avec succès." +#: views.py:456 +msgid "The article was created." +msgstr "L'article a été créé." -#: views.py:417 -msgid "The article has been successfully created." -msgstr "L'article a été créé avec succès." - -#: views.py:422 views.py:485 views.py:563 -#, fuzzy -#| msgid "Address" +#: views.py:461 views.py:534 views.py:627 msgid "Add" -msgstr "Adresse" +msgstr "Ajouter" -#: views.py:438 -msgid "The article has been successfully edited." -msgstr "L'article a été modifié avec succès." +#: views.py:462 +msgid "New article" +msgstr "Nouvel article" -#: views.py:459 -msgid "The article(s) have been successfully deleted." -msgstr "L'(es) article(s) a(ont) été supprimé(s) avec succès. " +#: views.py:478 +msgid "The article was edited." +msgstr "L'article a été modifié." -#: views.py:480 -msgid "The payment method has been successfully created." -msgstr "Le moyen de paiement a été créé avec succès." +#: views.py:484 +msgid "Edit article" +msgstr "Modifier l'article" -#: views.py:502 -msgid "The payement method has been successfully edited." -msgstr "Le moyen de paiement a été modifié avec succès." +#: views.py:500 +msgid "The articles were deleted." +msgstr "Les articles ont été supprimés." -#: views.py:526 +#: views.py:505 views.py:605 views.py:685 +msgid "Delete" +msgstr "Supprimer" + +#: views.py:506 +msgid "Delete article" +msgstr "Supprimer l'article" + +#: views.py:528 +msgid "The payment method was created." +msgstr "Le moyen de paiment a été créé." + +#: views.py:535 +msgid "New payment method" +msgstr "Nouveau moyen de paiement" + +#: views.py:564 +msgid "The payment method was edited." +msgstr "Le moyen de paiment a été modifié." + +#: views.py:571 +msgid "Edit payment method" +msgstr "Modifier le moyen de paiement" + +#: views.py:590 #, python-format -msgid "" -"The payment method %(method_name)s has been successfully " -"deleted." -msgstr "Le moyen de paiement %(method_name)s a été supprimé avec succès." +msgid "The payment method %(method_name)s was deleted." +msgstr "Le moyen de paiement %(method_name)s a été supprimé." -#: views.py:534 +#: views.py:597 #, python-format msgid "" "The payment method %(method_name)s can't be deleted " "because there are invoices using it." msgstr "" -"Le moyen de paiement %(method_name)s ne peut pas être mis à jour car il y a " -"des factures l'utilisant." +"Le moyen de paiement %(method_name)s ne peut pas être supprimé car il y a " +"des factures qui l'utilisent." -#: views.py:558 -msgid "The bank has been successfully created." -msgstr "La banque a été crée avec succès." +#: views.py:606 +msgid "Delete payment method" +msgstr "Supprimer le moyen de paiement" -#: views.py:580 -msgid "The bank has been successfully edited" -msgstr "La banque a été modifée avec succès." +#: views.py:622 +msgid "The bank was created." +msgstr "La banque a été créée." -#: views.py:604 +#: views.py:628 +msgid "New bank" +msgstr "Nouvelle banque" + +#: views.py:645 +msgid "The bank was edited." +msgstr "La banque a été modifiée." + +#: views.py:651 +msgid "Edit bank" +msgstr "Modifier la banque" + +#: views.py:670 #, python-format -msgid "" -"The bank %(bank_name)s has been successfully deleted." -msgstr "La banque %(bank_name)s a été supprimée avec succès." +msgid "The bank %(bank_name)s was deleted." +msgstr "La banque %(bank_name)s a été supprimée." -#: views.py:612 +#: views.py:677 #, python-format msgid "" "The bank %(bank_name)s can't be deleted because there " @@ -665,127 +928,18 @@ msgstr "" "La banque %(bank_name)s ne peut pas être supprimée car il y a des factures " "qui l'utilisent." -#: views.py:656 +#: views.py:686 +msgid "Delete bank" +msgstr "Supprimer la banque" + +#: views.py:722 msgid "Your changes have been properly taken into account." msgstr "Vos modifications ont correctement été prises en compte." -#: views.py:776 -msgid "The balance is too low for this operation." -msgstr "Le solde est trop faible pour cette opération." +#: views.py:834 +msgid "You are not allowed to credit your balance." +msgstr "Vous n'êtes pas autorisés à créditer votre solde." -#: views.py:806 -#, python-format -msgid "" -"The cotisation of %(member_name)s has been successfully " -"extended to %(end_date)s." -msgstr "La cotisation de %(member_name)s a été prolongée jusqu'à %(end_date)s." - -#: views.py:816 -msgid "The invoice has been successuflly created." -msgstr "La facture a été créée avec succès." - -#: views.py:846 -msgid "Online payment is disabled." -msgstr "Le paiement en ligne est désactivé." - -#~ msgid "Paid" -#~ msgstr "Payé" - -#~ msgid "Recipient" -#~ msgstr "Destinataire" - -#~ msgid "Invoice number" -#~ msgstr "Numéro de facture" - -#~ msgid "Existing articles" -#~ msgstr "Articles disponibles" - -#~ msgid "Existing payment method" -#~ msgstr "Moyen de paiements disponibles" - -#~ msgid "Existing banks" -#~ msgstr "Banques disponibles" - -#~ msgid "Amount" -#~ msgstr "Montant" - -#~ msgid "Can change the \"controlled\" state" -#~ msgstr "Peut modifier l'état \"controllé\"" - -#~ msgid "Can create a custom PDF invoice" -#~ msgstr "Peut crée une facture PDF personnalisée" - -#~ msgid "Can see an invoice's details" -#~ msgstr "Peut voir les détails d'une facture" - -#~ msgid "Can edit all the previous invoices" -#~ msgstr "Peut modifier toutes les factures existantes" - -#~ msgid "Connexion" -#~ msgstr "Connexion" - -#~ msgid "Membership" -#~ msgstr "Adhésion" - -#~ msgid "Both of them" -#~ msgstr "Les deux" - -#~ msgid "Duration (in whole month)" -#~ msgstr "Durée (en mois entiers)" - -#~ msgid "Type of cotisation" -#~ msgstr "Type de cotisation" - -#~ msgid "Can see a purchase's details" -#~ msgstr "Peut voir les détails d'un achat" - -#~ msgid "Can edit all the previous purchases" -#~ msgstr "Peut voir les achats existants" - -#~ msgid "Purchase" -#~ msgstr "Achat" - -#~ msgid "Purchases" -#~ msgstr "Achat" - -#~ msgid "Club" -#~ msgstr "Club" - -#~ msgid "Unitary price" -#~ msgstr "Prix unitaire" - -#~ msgid "Type of users concerned" -#~ msgstr "Type d'utilisateurs concernés" - -#~ msgid "Can see an article's details" -#~ msgstr "Peut voir les détails d'un article" - -#~ msgid "Name" -#~ msgstr "Nom" - -#~ msgid "Can see a bank's details" -#~ msgstr "Peut voir les détails d'une banque" - -#~ msgid "Standard" -#~ msgstr "Standard" - -#~ msgid "Cheque" -#~ msgstr "Chèque" - -#~ msgid "Method" -#~ msgstr "Moyen" - -#~ msgid "Can see a payement's details" -#~ msgstr "Peut voir les détails d'un paiement" - -#~ msgid "Starting date" -#~ msgstr "Date de début" - -#~ msgid "Ending date" -#~ msgstr "Date de fin" - -#~ msgid "Can see a cotisation's details" -#~ msgstr "Peut voir les détails d'une cotisation" - -#~ msgid "Can edit the previous cotisations" -#~ msgstr "Peut voir les cotisations existantes" +#: views.py:869 +msgid "Refill your balance" +msgstr "Recharger votre solde" diff --git a/cotisations/migrations/0031_comnpaypayment_production.py b/cotisations/migrations/0031_comnpaypayment_production.py new file mode 100644 index 00000000..25ec7fb8 --- /dev/null +++ b/cotisations/migrations/0031_comnpaypayment_production.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-11 23:03 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0030_custom_payment'), + ] + + operations = [ + migrations.AddField( + model_name='comnpaypayment', + name='production', + field=models.BooleanField(default=True, verbose_name='Production mode enabled (production url, instead of homologation)'), + ), + ] diff --git a/cotisations/migrations/0032_custom_invoice.py b/cotisations/migrations/0032_custom_invoice.py new file mode 100644 index 00000000..68963bf5 --- /dev/null +++ b/cotisations/migrations/0032_custom_invoice.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-21 20:01 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +from django.contrib.auth.management import create_permissions +import re2o.field_permissions +import re2o.mixins + + +def reattribute_ids(apps, schema_editor): + Facture = apps.get_model('cotisations', 'Facture') + BaseInvoice = apps.get_model('cotisations', 'BaseInvoice') + + for f in Facture.objects.all(): + base = BaseInvoice.objects.create(id=f.pk) + base.date = f.date + base.save() + f.baseinvoice_ptr = base + f.save() + + +def update_rights(apps, schema_editor): + Permission = apps.get_model('auth', 'Permission') + + # creates needed permissions + app = apps.get_app_config('cotisations') + app.models_module = True + create_permissions(app) + app.models_module = False + + former = Permission.objects.get(codename='change_facture_pdf') + new_1 = Permission.objects.get(codename='add_custominvoice') + new_2 = Permission.objects.get(codename='change_custominvoice') + new_3 = Permission.objects.get(codename='view_custominvoice') + new_4 = Permission.objects.get(codename='delete_custominvoice') + for group in former.group_set.all(): + group.permissions.remove(former) + group.permissions.add(new_1) + group.permissions.add(new_2) + group.permissions.add(new_3) + group.permissions.add(new_4) + group.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0031_comnpaypayment_production'), + ] + + operations = [ + migrations.CreateModel( + name='BaseInvoice', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateTimeField(auto_now_add=True, verbose_name='Date')), + ], + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, re2o.field_permissions.FieldPermissionModelMixin, models.Model), + ), + migrations.CreateModel( + name='CustomInvoice', + fields=[ + ('baseinvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice')), + ('recipient', models.CharField(max_length=255, verbose_name='Recipient')), + ('payment', models.CharField(max_length=255, verbose_name='Payment type')), + ('address', models.CharField(max_length=255, verbose_name='Address')), + ('paid', models.BooleanField(verbose_name='Paid')), + ], + bases=('cotisations.baseinvoice',), + options={'permissions': (('view_custominvoice', 'Can view a custom invoice'),)}, + ), + migrations.AddField( + model_name='facture', + name='baseinvoice_ptr', + field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='cotisations.BaseInvoice', null=True), + preserve_default=False, + ), + migrations.RunPython(reattribute_ids), + migrations.AlterField( + model_name='vente', + name='facture', + field=models.ForeignKey(on_delete=models.CASCADE, verbose_name='Invoice', to='cotisations.BaseInvoice') + ), + migrations.RemoveField( + model_name='facture', + name='id', + ), + migrations.RemoveField( + model_name='facture', + name='date', + ), + migrations.AlterField( + model_name='facture', + name='baseinvoice_ptr', + field=models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.BaseInvoice'), + ), + migrations.RunPython(update_rights), + migrations.AlterModelOptions( + name='facture', + options={'permissions': (('change_facture_control', 'Can change the "controlled" state'), ('view_facture', "Can see an invoice's details"), ('change_all_facture', 'Can edit all the previous invoices')), 'verbose_name': 'Invoice', 'verbose_name_plural': 'Invoices'}, + ), + ] diff --git a/cotisations/migrations/0033_auto_20180818_1319.py b/cotisations/migrations/0033_auto_20180818_1319.py new file mode 100644 index 00000000..2e61fbb6 --- /dev/null +++ b/cotisations/migrations/0033_auto_20180818_1319.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-18 11:19 +from __future__ import unicode_literals + +import cotisations.validators +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion +import re2o.aes_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0032_custom_invoice'), + ] + + operations = [ + migrations.AlterModelOptions( + name='article', + options={'permissions': (('view_article', 'Can view an article object'), ('buy_every_article', 'Can buy every article')), 'verbose_name': 'article', 'verbose_name_plural': 'articles'}, + ), + migrations.AlterModelOptions( + name='balancepayment', + options={'verbose_name': 'user balance'}, + ), + migrations.AlterModelOptions( + name='banque', + options={'permissions': (('view_banque', 'Can view a bank object'),), 'verbose_name': 'bank', 'verbose_name_plural': 'banks'}, + ), + migrations.AlterModelOptions( + name='cotisation', + options={'permissions': (('view_cotisation', 'Can view a subscription object'), ('change_all_cotisation', 'Can edit the previous subscriptions')), 'verbose_name': 'subscription', 'verbose_name_plural': 'subscriptions'}, + ), + migrations.AlterModelOptions( + name='custominvoice', + options={'permissions': (('view_custominvoice', 'Can view a custom invoice object'),)}, + ), + migrations.AlterModelOptions( + name='facture', + options={'permissions': (('change_facture_control', 'Can edit the "controlled" state'), ('view_facture', 'Can view an invoice object'), ('change_all_facture', 'Can edit all the previous invoices')), 'verbose_name': 'invoice', 'verbose_name_plural': 'invoices'}, + ), + migrations.AlterModelOptions( + name='paiement', + options={'permissions': (('view_paiement', 'Can view a payment method object'), ('use_every_payment', 'Can use every payment method')), 'verbose_name': 'payment method', 'verbose_name_plural': 'payment methods'}, + ), + migrations.AlterModelOptions( + name='vente', + options={'permissions': (('view_vente', 'Can view a purchase object'), ('change_all_vente', 'Can edit all the previous purchases')), 'verbose_name': 'purchase', 'verbose_name_plural': 'purchases'}, + ), + migrations.AlterField( + model_name='article', + name='available_for_everyone', + field=models.BooleanField(default=False, verbose_name='is available for every user'), + ), + migrations.AlterField( + model_name='article', + name='duration', + field=models.PositiveIntegerField(blank=True, null=True, validators=[django.core.validators.MinValueValidator(0)], verbose_name='duration (in months)'), + ), + migrations.AlterField( + model_name='article', + name='name', + field=models.CharField(max_length=255, verbose_name='designation'), + ), + migrations.AlterField( + model_name='article', + name='prix', + field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name='unit price'), + ), + migrations.AlterField( + model_name='article', + name='type_cotisation', + field=models.CharField(blank=True, choices=[('Connexion', 'Connection'), ('Adhesion', 'Membership'), ('All', 'Both of them')], default=None, max_length=255, null=True, verbose_name='subscription type'), + ), + migrations.AlterField( + model_name='article', + name='type_user', + field=models.CharField(choices=[('Adherent', 'Member'), ('Club', 'Club'), ('All', 'Both of them')], default='All', max_length=255, verbose_name='type of users concerned'), + ), + migrations.AlterField( + model_name='banque', + name='name', + field=models.CharField(max_length=255), + ), + migrations.AlterField( + model_name='comnpaypayment', + name='payment_credential', + field=models.CharField(blank=True, default='', max_length=255, verbose_name='ComNpay VAT Number'), + ), + migrations.AlterField( + model_name='comnpaypayment', + name='payment_pass', + field=re2o.aes_field.AESEncryptedField(blank=True, max_length=255, null=True, verbose_name='ComNpay secret key'), + ), + migrations.AlterField( + model_name='comnpaypayment', + name='production', + field=models.BooleanField(default=True, verbose_name='Production mode enabled (production URL, instead of homologation)'), + ), + migrations.AlterField( + model_name='cotisation', + name='date_end', + field=models.DateTimeField(verbose_name='end date'), + ), + migrations.AlterField( + model_name='cotisation', + name='date_start', + field=models.DateTimeField(verbose_name='start date'), + ), + migrations.AlterField( + model_name='cotisation', + name='type_cotisation', + field=models.CharField(choices=[('Connexion', 'Connection'), ('Adhesion', 'Membership'), ('All', 'Both of them')], default='All', max_length=255, verbose_name='subscription type'), + ), + migrations.AlterField( + model_name='cotisation', + name='vente', + field=models.OneToOneField(null=True, on_delete=django.db.models.deletion.CASCADE, to='cotisations.Vente', verbose_name='purchase'), + ), + migrations.AlterField( + model_name='facture', + name='cheque', + field=models.CharField(blank=True, max_length=255, verbose_name='cheque number'), + ), + migrations.AlterField( + model_name='facture', + name='control', + field=models.BooleanField(default=False, verbose_name='controlled'), + ), + migrations.AlterField( + model_name='facture', + name='valid', + field=models.BooleanField(default=True, verbose_name='validated'), + ), + migrations.AlterField( + model_name='paiement', + name='available_for_everyone', + field=models.BooleanField(default=False, verbose_name='is available for every user'), + ), + migrations.AlterField( + model_name='paiement', + name='is_balance', + field=models.BooleanField(default=False, editable=False, help_text='There should be only one balance payment method.', validators=[cotisations.validators.check_no_balance], verbose_name='is user balance'), + ), + migrations.AlterField( + model_name='paiement', + name='moyen', + field=models.CharField(max_length=255, verbose_name='method'), + ), + migrations.AlterField( + model_name='vente', + name='duration', + field=models.PositiveIntegerField(blank=True, null=True, verbose_name='duration (in months)'), + ), + migrations.AlterField( + model_name='vente', + name='facture', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cotisations.BaseInvoice', verbose_name='invoice'), + ), + migrations.AlterField( + model_name='vente', + name='name', + field=models.CharField(max_length=255, verbose_name='article'), + ), + migrations.AlterField( + model_name='vente', + name='number', + field=models.IntegerField(validators=[django.core.validators.MinValueValidator(1)], verbose_name='amount'), + ), + migrations.AlterField( + model_name='vente', + name='prix', + field=models.DecimalField(decimal_places=2, max_digits=5, verbose_name='price'), + ), + migrations.AlterField( + model_name='vente', + name='type_cotisation', + field=models.CharField(blank=True, choices=[('Connexion', 'Connection'), ('Adhesion', 'Membership'), ('All', 'Both of them')], max_length=255, null=True, verbose_name='subscription type'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index 52d71b58..fe89aa5d 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -41,8 +41,7 @@ from django.dispatch import receiver from django.forms import ValidationError from django.core.validators import MinValueValidator from django.utils import timezone -from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_lazy as _l +from django.utils.translation import ugettext_lazy as _ from django.urls import reverse from django.shortcuts import redirect from django.contrib import messages @@ -55,80 +54,11 @@ from cotisations.utils import find_payment_method from cotisations.validators import check_no_balance -# TODO : change facture to invoice -class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): - """ - The model for an invoice. It reprensents the fact that a user paid for - something (it can be multiple article paid at once). - - An invoice is linked to : - * one or more purchases (one for each article sold that time) - * a user (the one who bought those articles) - * a payment method (the one used by the user) - * (if applicable) a bank - * (if applicable) a cheque number. - Every invoice is dated throught the 'date' value. - An invoice has a 'controlled' value (default : False) which means that - someone with high enough rights has controlled that invoice and taken it - into account. It also has a 'valid' value (default : True) which means - that someone with high enough rights has decided that this invoice was not - valid (thus it's like the user never paid for his articles). It may be - necessary in case of non-payment. - """ - - user = models.ForeignKey('users.User', on_delete=models.PROTECT) - # TODO : change paiement to payment - paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT) - # TODO : change banque to bank - banque = models.ForeignKey( - 'Banque', - on_delete=models.PROTECT, - blank=True, - null=True - ) - # TODO : maybe change to cheque nummber because not evident - cheque = models.CharField( - max_length=255, - blank=True, - verbose_name=_l("Cheque number") - ) +class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): date = models.DateTimeField( auto_now_add=True, - verbose_name=_l("Date") + verbose_name=_("Date") ) - # TODO : change name to validity for clarity - valid = models.BooleanField( - default=True, - verbose_name=_l("Validated") - ) - # TODO : changed name to controlled for clarity - control = models.BooleanField( - default=False, - verbose_name=_l("Controlled") - ) - - class Meta: - abstract = False - permissions = ( - # TODO : change facture to invoice - ('change_facture_control', - _l("Can change the \"controlled\" state")), - # TODO : seems more likely to be call create_facture_pdf - # or create_invoice_pdf - ('change_facture_pdf', - _l("Can create a custom PDF invoice")), - ('view_facture', - _l("Can see an invoice's details")), - ('change_all_facture', - _l("Can edit all the previous invoices")), - ) - verbose_name = _l("Invoice") - verbose_name_plural = _l("Invoices") - - def linked_objects(self): - """Return linked objects : machine and domain. - Usefull in history display""" - return self.vente_set.all() # TODO : change prix to price def prix(self): @@ -167,6 +97,74 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): ).values_list('name', flat=True)) return name + +# TODO : change facture to invoice +class Facture(BaseInvoice): + """ + The model for an invoice. It reprensents the fact that a user paid for + something (it can be multiple article paid at once). + + An invoice is linked to : + * one or more purchases (one for each article sold that time) + * a user (the one who bought those articles) + * a payment method (the one used by the user) + * (if applicable) a bank + * (if applicable) a cheque number. + Every invoice is dated throught the 'date' value. + An invoice has a 'controlled' value (default : False) which means that + someone with high enough rights has controlled that invoice and taken it + into account. It also has a 'valid' value (default : True) which means + that someone with high enough rights has decided that this invoice was not + valid (thus it's like the user never paid for his articles). It may be + necessary in case of non-payment. + """ + + user = models.ForeignKey('users.User', on_delete=models.PROTECT) + # TODO : change paiement to payment + paiement = models.ForeignKey('Paiement', on_delete=models.PROTECT) + # TODO : change banque to bank + banque = models.ForeignKey( + 'Banque', + on_delete=models.PROTECT, + blank=True, + null=True + ) + # TODO : maybe change to cheque nummber because not evident + cheque = models.CharField( + max_length=255, + blank=True, + verbose_name=_("cheque number") + ) + # TODO : change name to validity for clarity + valid = models.BooleanField( + default=True, + verbose_name=_("validated") + ) + # TODO : changed name to controlled for clarity + control = models.BooleanField( + default=False, + verbose_name=_("controlled") + ) + + class Meta: + abstract = False + permissions = ( + # TODO : change facture to invoice + ('change_facture_control', + _("Can edit the \"controlled\" state")), + ('view_facture', + _("Can view an invoice object")), + ('change_all_facture', + _("Can edit all the previous invoices")), + ) + verbose_name = _("invoice") + verbose_name_plural = _("invoices") + + def linked_objects(self): + """Return linked objects : machine and domain. + Usefull in history display""" + return self.vente_set.all() + def can_edit(self, user_request, *args, **kwargs): if not user_request.has_perm('cotisations.change_facture'): return False, _("You don't have the right to edit an invoice.") @@ -196,7 +194,7 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): def can_view(self, user_request, *_args, **_kwargs): if not user_request.has_perm('cotisations.view_facture') and \ self.user != user_request: - return False, _("You don't have the right to see someone else's " + return False, _("You don't have the right to view someone else's " "invoices history.") elif not self.valid: return False, _("The invoice has been invalidated.") @@ -212,14 +210,6 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): _("You don't have the right to edit the \"controlled\" state.") ) - @staticmethod - def can_change_pdf(user_request, *_args, **_kwargs): - """ Returns True if the user can change this invoice """ - return ( - user_request.has_perm('cotisations.change_facture_pdf'), - _("You don't have the right to edit an invoice.") - ) - @staticmethod def can_create(user_request, *_args, **_kwargs): """Check if a user can create an invoice. @@ -231,8 +221,8 @@ class Facture(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): if user_request.has_perm('cotisations.add_facture'): return True, None if len(Paiement.find_allowed_payments(user_request)) <= 0: - return False, _("There are no payment types which you can use.") - if len(Article.find_allowed_articles(user_request)) <= 0: + return False, _("There are no payment method which you can use.") + if len(Article.find_allowed_articles(user_request, user_request)) <= 0: return False, _("There are no article that you can buy.") return True, None @@ -265,6 +255,28 @@ def facture_post_delete(**kwargs): user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) +class CustomInvoice(BaseInvoice): + class Meta: + permissions = ( + ('view_custominvoice', _("Can view a custom invoice object")), + ) + recipient = models.CharField( + max_length=255, + verbose_name=_("Recipient") + ) + payment = models.CharField( + max_length=255, + verbose_name=_("Payment type") + ) + address = models.CharField( + max_length=255, + verbose_name=_("Address") + ) + paid = models.BooleanField( + verbose_name=_("Paid") + ) + + # TODO : change Vente to Purchase class Vente(RevMixin, AclMixin, models.Model): """ @@ -281,38 +293,38 @@ class Vente(RevMixin, AclMixin, models.Model): # TODO : change this to English COTISATION_TYPE = ( - ('Connexion', _l("Connexion")), - ('Adhesion', _l("Membership")), - ('All', _l("Both of them")), + ('Connexion', _("Connection")), + ('Adhesion', _("Membership")), + ('All', _("Both of them")), ) # TODO : change facture to invoice facture = models.ForeignKey( - 'Facture', + 'BaseInvoice', on_delete=models.CASCADE, - verbose_name=_l("Invoice") + verbose_name=_("invoice") ) # TODO : change number to amount for clarity number = models.IntegerField( validators=[MinValueValidator(1)], - verbose_name=_l("Amount") + verbose_name=_("amount") ) # TODO : change this field for a ForeinKey to Article name = models.CharField( max_length=255, - verbose_name=_l("Article") + verbose_name=_("article") ) # TODO : change prix to price # TODO : this field is not needed if you use Article ForeignKey prix = models.DecimalField( max_digits=5, decimal_places=2, - verbose_name=_l("Price")) + verbose_name=_("price")) # TODO : this field is not needed if you use Article ForeignKey duration = models.PositiveIntegerField( blank=True, null=True, - verbose_name=_l("Duration (in whole month)") + verbose_name=_("duration (in months)") ) # TODO : this field is not needed if you use Article ForeignKey type_cotisation = models.CharField( @@ -320,16 +332,16 @@ class Vente(RevMixin, AclMixin, models.Model): blank=True, null=True, max_length=255, - verbose_name=_l("Type of cotisation") + verbose_name=_("subscription type") ) class Meta: permissions = ( - ('view_vente', _l("Can see a purchase's details")), - ('change_all_vente', _l("Can edit all the previous purchases")), + ('view_vente', _("Can view a purchase object")), + ('change_all_vente', _("Can edit all the previous purchases")), ) - verbose_name = _l("Purchase") - verbose_name_plural = _l("Purchases") + verbose_name = _("purchase") + verbose_name_plural = _("purchases") # TODO : change prix_total to total_price def prix_total(self): @@ -355,6 +367,10 @@ class Vente(RevMixin, AclMixin, models.Model): cotisation_type defined (which means the article sold represents a cotisation) """ + try: + invoice = self.facture.facture + except Facture.DoesNotExist: + return if not hasattr(self, 'cotisation') and self.type_cotisation: cotisation = Cotisation(vente=self) cotisation.type_cotisation = self.type_cotisation @@ -362,7 +378,7 @@ class Vente(RevMixin, AclMixin, models.Model): end_cotisation = Cotisation.objects.filter( vente__in=Vente.objects.filter( facture__in=Facture.objects.filter( - user=self.facture.user + user=invoice.user ).exclude(valid=False)) ).filter( Q(type_cotisation='All') | @@ -371,9 +387,9 @@ class Vente(RevMixin, AclMixin, models.Model): date_start__lt=date_start ).aggregate(Max('date_end'))['date_end__max'] elif self.type_cotisation == "Adhesion": - end_cotisation = self.facture.user.end_adhesion() + end_cotisation = invoice.user.end_adhesion() else: - end_cotisation = self.facture.user.end_connexion() + end_cotisation = invoice.user.end_connexion() date_start = date_start or timezone.now() end_cotisation = end_cotisation or date_start date_max = max(end_cotisation, date_start) @@ -392,7 +408,7 @@ class Vente(RevMixin, AclMixin, models.Model): # Checking that if a cotisation is specified, there is also a duration if self.type_cotisation and not self.duration: raise ValidationError( - _("A cotisation should always have a duration.") + _("Duration must be specified for a subscription.") ) self.update_cotisation() super(Vente, self).save(*args, **kwargs) @@ -428,7 +444,7 @@ class Vente(RevMixin, AclMixin, models.Model): def can_view(self, user_request, *_args, **_kwargs): if (not user_request.has_perm('cotisations.view_vente') and self.facture.user != user_request): - return False, _("You don't have the right to see someone " + return False, _("You don't have the right to view someone " "else's purchase history.") else: return True, None @@ -445,6 +461,10 @@ def vente_post_save(**kwargs): LDAP user when a purchase has been saved. """ purchase = kwargs['instance'] + try: + purchase.facture.facture + except Facture.DoesNotExist: + return if hasattr(purchase, 'cotisation'): purchase.cotisation.vente = purchase purchase.cotisation.save() @@ -462,8 +482,12 @@ def vente_post_delete(**kwargs): Synchronise the LDAP user after a purchase has been deleted. """ purchase = kwargs['instance'] + try: + invoice = purchase.facture.facture + except Facture.DoesNotExist: + return if purchase.type_cotisation: - user = purchase.facture.user + user = invoice.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -483,38 +507,38 @@ class Article(RevMixin, AclMixin, models.Model): # TODO : Either use TYPE or TYPES in both choices but not both USER_TYPES = ( - ('Adherent', _l("Member")), - ('Club', _l("Club")), - ('All', _l("Both of them")), + ('Adherent', _("Member")), + ('Club', _("Club")), + ('All', _("Both of them")), ) COTISATION_TYPE = ( - ('Connexion', _l("Connexion")), - ('Adhesion', _l("Membership")), - ('All', _l("Both of them")), + ('Connexion', _("Connection")), + ('Adhesion', _("Membership")), + ('All', _("Both of them")), ) name = models.CharField( max_length=255, - verbose_name=_l("Designation") + verbose_name=_("designation") ) # TODO : change prix to price prix = models.DecimalField( max_digits=5, decimal_places=2, - verbose_name=_l("Unitary price") + verbose_name=_("unit price") ) duration = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(0)], - verbose_name=_l("Duration (in whole month)") + verbose_name=_("duration (in months)") ) type_user = models.CharField( choices=USER_TYPES, default='All', max_length=255, - verbose_name=_l("Type of users concerned") + verbose_name=_("type of users concerned") ) type_cotisation = models.CharField( choices=COTISATION_TYPE, @@ -522,31 +546,31 @@ class Article(RevMixin, AclMixin, models.Model): blank=True, null=True, max_length=255, - verbose_name=_l("Type of cotisation") + verbose_name=_("subscription type") ) available_for_everyone = models.BooleanField( default=False, - verbose_name=_l("Is available for every user") + verbose_name=_("is available for every user") ) unique_together = ('name', 'type_user') class Meta: permissions = ( - ('view_article', _l("Can see an article's details")), - ('buy_every_article', _l("Can buy every_article")) + ('view_article', _("Can view an article object")), + ('buy_every_article', _("Can buy every article")) ) - verbose_name = "Article" - verbose_name_plural = "Articles" + verbose_name = "article" + verbose_name_plural = "articles" def clean(self): if self.name.lower() == 'solde': raise ValidationError( - _("Solde is a reserved article name") + _("Balance is a reserved article name.") ) if self.type_cotisation and not self.duration: raise ValidationError( - _("Duration must be specified for a cotisation") + _("Duration must be specified for a subscription.") ) def __str__(self): @@ -567,19 +591,28 @@ class Article(RevMixin, AclMixin, models.Model): self.available_for_everyone or user.has_perm('cotisations.buy_every_article') or user.has_perm('cotisations.add_facture'), - _("You cannot buy this Article.") + _("You can't buy this article.") ) @classmethod - def find_allowed_articles(cls, user): - """Finds every allowed articles for an user. + def find_allowed_articles(cls, user, target_user): + """Finds every allowed articles for an user, on a target user. Args: user: The user requesting articles. + target_user: The user to sell articles """ + if target_user.is_class_club: + objects_pool = cls.objects.filter( + Q(type_user='All') | Q(type_user='Club') + ) + else: + objects_pool = cls.objects.filter( + Q(type_user='All') | Q(type_user='Adherent') + ) if user.has_perm('cotisations.buy_every_article'): - return cls.objects.all() - return cls.objects.filter(available_for_everyone=True) + return objects_pool + return objects_pool.filter(available_for_everyone=True) class Banque(RevMixin, AclMixin, models.Model): @@ -593,15 +626,14 @@ class Banque(RevMixin, AclMixin, models.Model): name = models.CharField( max_length=255, - verbose_name=_l("Name") ) class Meta: permissions = ( - ('view_banque', _l("Can see a bank's details")), + ('view_banque', _("Can view a bank object")), ) - verbose_name = _l("Bank") - verbose_name_plural = _l("Banks") + verbose_name = _("bank") + verbose_name_plural = _("banks") def __str__(self): return self.name @@ -619,33 +651,33 @@ class Paiement(RevMixin, AclMixin, models.Model): # TODO : change moyen to method moyen = models.CharField( max_length=255, - verbose_name=_l("Method") + verbose_name=_("method") ) available_for_everyone = models.BooleanField( default=False, - verbose_name=_l("Is available for every user") + verbose_name=_("is available for every user") ) is_balance = models.BooleanField( default=False, editable=False, - verbose_name=_l("Is user balance"), - help_text=_l("There should be only one balance payment method."), + verbose_name=_("is user balance"), + help_text=_("There should be only one balance payment method."), validators=[check_no_balance] ) class Meta: permissions = ( - ('view_paiement', _l("Can see a payement's details")), - ('use_every_payment', _l("Can use every payement")), + ('view_paiement', _("Can view a payment method object")), + ('use_every_payment', _("Can use every payment method")), ) - verbose_name = _l("Payment method") - verbose_name_plural = _l("Payment methods") + verbose_name = _("payment method") + verbose_name_plural = _("payment methods") def __str__(self): return self.moyen def clean(self): - """ + """l Override of the herited clean function to get a correct name """ self.moyen = self.moyen.title() @@ -673,8 +705,8 @@ class Paiement(RevMixin, AclMixin, models.Model): if any(sell.type_cotisation for sell in invoice.vente_set.all()): messages.success( request, - _("The cotisation of %(member_name)s has been \ - extended to %(end_date)s.") % { + _("The subscription of %(member_name)s was extended to" + " %(end_date)s.") % { 'member_name': invoice.user.pseudo, 'end_date': invoice.user.end_adhesion() } @@ -683,7 +715,7 @@ class Paiement(RevMixin, AclMixin, models.Model): else: messages.success( request, - _("The invoice has been created.") + _("The invoice was created.") ) return redirect(reverse( 'users:profil', @@ -704,7 +736,7 @@ class Paiement(RevMixin, AclMixin, models.Model): self.available_for_everyone or user.has_perm('cotisations.use_every_payment') or user.has_perm('cotisations.add_facture'), - _("You cannot use this Payment.") + _("You can't use this payment method.") ) @classmethod @@ -722,7 +754,7 @@ class Paiement(RevMixin, AclMixin, models.Model): p = find_payment_method(self) if p is not None: return p._meta.verbose_name - return _("No custom payment method") + return _("No custom payment method.") class Cotisation(RevMixin, AclMixin, models.Model): @@ -738,9 +770,9 @@ class Cotisation(RevMixin, AclMixin, models.Model): """ COTISATION_TYPE = ( - ('Connexion', _l("Connexion")), - ('Adhesion', _l("Membership")), - ('All', _l("Both of them")), + ('Connexion', _("Connection")), + ('Adhesion', _("Membership")), + ('All', _("Both of them")), ) # TODO : change vente to purchase @@ -748,34 +780,36 @@ class Cotisation(RevMixin, AclMixin, models.Model): 'Vente', on_delete=models.CASCADE, null=True, - verbose_name=_l("Purchase") + verbose_name=_("purchase") ) type_cotisation = models.CharField( choices=COTISATION_TYPE, max_length=255, default='All', - verbose_name=_l("Type of cotisation") + verbose_name=_("subscription type") ) date_start = models.DateTimeField( - verbose_name=_l("Starting date") + verbose_name=_("start date") ) date_end = models.DateTimeField( - verbose_name=_l("Ending date") + verbose_name=_("end date") ) class Meta: permissions = ( - ('view_cotisation', _l("Can see a cotisation's details")), - ('change_all_cotisation', _l("Can edit the previous cotisations")), + ('view_cotisation', _("Can view a subscription object")), + ('change_all_cotisation', _("Can edit the previous subscriptions")), ) + verbose_name = _("subscription") + verbose_name_plural = _("subscriptions") def can_edit(self, user_request, *_args, **_kwargs): if not user_request.has_perm('cotisations.change_cotisation'): - return False, _("You don't have the right to edit a cotisation.") + return False, _("You don't have the right to edit a subscription.") elif not user_request.has_perm('cotisations.change_all_cotisation') \ and (self.vente.facture.control or not self.vente.facture.valid): - return False, _("You don't have the right to edit a cotisation " + return False, _("You don't have the right to edit a subscription " "already controlled or invalidated.") else: return True, None @@ -783,9 +817,9 @@ class Cotisation(RevMixin, AclMixin, models.Model): def can_delete(self, user_request, *_args, **_kwargs): if not user_request.has_perm('cotisations.delete_cotisation'): return False, _("You don't have the right to delete a " - "cotisation.") + "subscription.") if self.vente.facture.control or not self.vente.facture.valid: - return False, _("You don't have the right to delete a cotisation " + return False, _("You don't have the right to delete a subscription " "already controlled or invalidated.") else: return True, None @@ -793,8 +827,8 @@ class Cotisation(RevMixin, AclMixin, models.Model): def can_view(self, user_request, *_args, **_kwargs): if not user_request.has_perm('cotisations.view_cotisation') and\ self.vente.facture.user != user_request: - return False, _("You don't have the right to see someone else's " - "cotisation history.") + return False, _("You don't have the right to view someone else's " + "subscription history.") else: return True, None @@ -822,3 +856,4 @@ def cotisation_post_delete(**_kwargs): """ regen('mac_ip_list') regen('mailing') + diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py index 4e488405..250e6949 100644 --- a/cotisations/payment_methods/balance/models.py +++ b/cotisations/payment_methods/balance/models.py @@ -21,8 +21,7 @@ from django.db import models from django.shortcuts import redirect from django.urls import reverse -from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_lazy as _l +from django.utils.translation import ugettext_lazy as _ from django.contrib import messages @@ -36,7 +35,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): """ class Meta: - verbose_name = _l("User Balance") + verbose_name = _("user balance") payment = models.OneToOneField( Paiement, @@ -45,8 +44,8 @@ class BalancePayment(PaymentMethodMixin, models.Model): editable=False ) minimum_balance = models.DecimalField( - verbose_name=_l("Minimum balance"), - help_text=_l("The minimal amount of money allowed for the balance" + verbose_name=_("Minimum balance"), + help_text=_("The minimal amount of money allowed for the balance" " at the end of a payment. You can specify negative " "amount." ), @@ -55,8 +54,8 @@ class BalancePayment(PaymentMethodMixin, models.Model): default=0, ) maximum_balance = models.DecimalField( - verbose_name=_l("Maximum balance"), - help_text=_l("The maximal amount of money allowed for the balance."), + verbose_name=_("Maximum balance"), + help_text=_("The maximal amount of money allowed for the balance."), max_digits=5, decimal_places=2, default=50, @@ -64,7 +63,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): null=True, ) credit_balance_allowed = models.BooleanField( - verbose_name=_l("Allow user to credit their balance"), + verbose_name=_("Allow user to credit their balance"), default=False, ) @@ -97,7 +96,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): if len(p) > 0: form.add_error( 'payment_method', - _("There is already a payment type for user balance") + _("There is already a payment method for user balance.") ) def alter_payment(self, payment): @@ -118,3 +117,4 @@ class BalancePayment(PaymentMethodMixin, models.Model): len(Paiement.find_allowed_payments(user_request) .exclude(is_balance=True)) > 0 ) and self.credit_balance_allowed + diff --git a/cotisations/payment_methods/cheque/models.py b/cotisations/payment_methods/cheque/models.py index c2680e7a..cd6d2920 100644 --- a/cotisations/payment_methods/cheque/models.py +++ b/cotisations/payment_methods/cheque/models.py @@ -21,7 +21,7 @@ from django.db import models from django.shortcuts import redirect from django.urls import reverse -from django.utils.translation import ugettext_lazy as _l +from django.utils.translation import ugettext_lazy as _ from cotisations.models import Paiement from cotisations.payment_methods.mixins import PaymentMethodMixin @@ -33,7 +33,7 @@ class ChequePayment(PaymentMethodMixin, models.Model): """ class Meta: - verbose_name = _l("Cheque") + verbose_name = _("Cheque") payment = models.OneToOneField( Paiement, @@ -52,3 +52,4 @@ class ChequePayment(PaymentMethodMixin, models.Model): 'cotisations:cheque:validate', kwargs={'invoice_pk': invoice.pk} )) + diff --git a/cotisations/payment_methods/cheque/views.py b/cotisations/payment_methods/cheque/views.py index 4d164a79..3cce3e5c 100644 --- a/cotisations/payment_methods/cheque/views.py +++ b/cotisations/payment_methods/cheque/views.py @@ -44,7 +44,7 @@ def cheque(request, invoice_pk): if invoice.valid or not isinstance(payment_method, ChequePayment): messages.error( request, - _("You cannot pay this invoice with a cheque.") + _("You can't pay this invoice with a cheque.") ) return redirect(reverse( 'users:profil', @@ -67,3 +67,4 @@ def cheque(request, invoice_pk): 'amount': invoice.prix_total() } ) + diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index ff6fed0d..af389cf8 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -21,8 +21,7 @@ from django.db import models from django.shortcuts import render from django.urls import reverse -from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_lazy as _l +from django.utils.translation import ugettext_lazy as _ from cotisations.models import Paiement from cotisations.payment_methods.mixins import PaymentMethodMixin @@ -37,7 +36,7 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): """ class Meta: - verbose_name = "ComNpay" + verbose_name = _("ComNpay") payment = models.OneToOneField( Paiement, @@ -49,22 +48,32 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): max_length=255, default='', blank=True, - verbose_name=_l("ComNpay VAD Number"), + verbose_name=_("ComNpay VAT Number"), ) payment_pass = AESEncryptedField( max_length=255, null=True, blank=True, - verbose_name=_l("ComNpay Secret Key"), + verbose_name=_("ComNpay secret key"), ) minimum_payment = models.DecimalField( - verbose_name=_l("Minimum payment"), - help_text=_l("The minimal amount of money you have to use when paying" + verbose_name=_("Minimum payment"), + help_text=_("The minimal amount of money you have to use when paying" " with ComNpay"), max_digits=5, decimal_places=2, default=1, ) + production = models.BooleanField( + default=True, + verbose_name=_("Production mode enabled (production URL, instead of homologation)"), + ) + + def return_url_comnpay(self): + if self.production: + return 'https://secure.comnpay.com' + else: + return 'https://secure.homologation.comnpay.com' def end_payment(self, invoice, request): """ @@ -87,11 +96,12 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): "", "D" ) + r = { - 'action': 'https://secure.homologation.comnpay.com', + 'action': self.return_url_comnpay(), 'method': 'POST', 'content': p.buildSecretHTML( - _("Pay invoice no : ")+str(invoice.id), + _("Pay invoice number ")+str(invoice.id), invoice.prix_total(), idTransaction=str(invoice.id) ), @@ -103,6 +113,6 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): """Checks that the price meets the requirement to be paid with ComNpay. """ return ((price >= self.minimum_payment), - _('In order to pay your invoice with ComNpay' - ', the price must be grater than {} €') - .format(self.minimum_payment)) + _("In order to pay your invoice with ComNpay, the price must" + " be greater than {} €.").format(self.minimum_payment)) + diff --git a/cotisations/payment_methods/comnpay/views.py b/cotisations/payment_methods/comnpay/views.py index 89966b48..2383f1e9 100644 --- a/cotisations/payment_methods/comnpay/views.py +++ b/cotisations/payment_methods/comnpay/views.py @@ -50,7 +50,7 @@ def accept_payment(request, factureid): if invoice.valid: messages.success( request, - _("The payment of %(amount)s € has been accepted.") % { + _("The payment of %(amount)s € was accepted.") % { 'amount': invoice.prix_total() } ) @@ -60,8 +60,8 @@ def accept_payment(request, factureid): for purchase in invoice.vente_set.all()): messages.success( request, - _("The cotisation of %(member_name)s has been \ - extended to %(end_date)s.") % { + _("The subscription of %(member_name)s was extended to" + " %(end_date)s.") % { 'member_name': request.user.pseudo, 'end_date': request.user.end_adhesion() } @@ -81,7 +81,7 @@ def refuse_payment(request): """ messages.error( request, - _("The payment has been refused.") + _("The payment was refused.") ) return redirect(reverse( 'users:profil', @@ -136,3 +136,4 @@ def ipn(request): # Everything worked we send a reponse to Comnpay indicating that # it's ok for them to proceed return HttpResponse("HTTP/1.0 200 OK") + diff --git a/cotisations/payment_methods/forms.py b/cotisations/payment_methods/forms.py index d4d55a74..daa65118 100644 --- a/cotisations/payment_methods/forms.py +++ b/cotisations/payment_methods/forms.py @@ -19,8 +19,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from django import forms -from django.utils.translation import ugettext as _ -from django.utils.translation import ugettext_lazy as _l +from django.utils.translation import ugettext_lazy as _ from . import PAYMENT_METHODS from cotisations.utils import find_payment_method @@ -58,8 +57,8 @@ class PaymentMethodForm(forms.Form): """ payment_method = forms.ChoiceField( - label=_l("Special payment method"), - help_text=_l("Warning : You will not be able to change the payment " + label=_("Special payment method"), + help_text=_("Warning: you will not be able to change the payment " "method later. But you will be allowed to edit the other " "options." ), @@ -70,7 +69,7 @@ class PaymentMethodForm(forms.Form): super(PaymentMethodForm, self).__init__(*args, **kwargs) prefix = kwargs.get('prefix', None) self.fields['payment_method'].choices = [(i,p.NAME) for (i,p) in enumerate(PAYMENT_METHODS)] - self.fields['payment_method'].choices.insert(0, ('', _l('no'))) + self.fields['payment_method'].choices.insert(0, ('', _('no'))) self.fields['payment_method'].widget.attrs = { 'id': 'paymentMethodSelect' } diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html index 2afe726d..b07035da 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -32,10 +32,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Article" %} {% trans "Price" %} - {% trans "Cotisation type" %} - {% trans "Duration (month)" %} + {% trans "Subscription type" %} + {% trans "Duration (in months)" %} {% trans "Concerned users" %} - {% trans "Available for everyone" | tick %} + {% trans "Available for everyone" %} @@ -46,7 +46,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ article.type_cotisation }} {{ article.duration }} {{ article.type_user }} - {{ article.available_for_everyone }} + {{ article.available_for_everyone | tick }} {% can_edit article %} diff --git a/cotisations/templates/cotisations/aff_banque.html b/cotisations/templates/cotisations/aff_banque.html index b5074c6c..057c6995 100644 --- a/cotisations/templates/cotisations/aff_banque.html +++ b/cotisations/templates/cotisations/aff_banque.html @@ -26,25 +26,23 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load i18n %} {% load logs_extra %} - - - - - - - - {% for banque in banque_list %} +
{% trans "Bank" %}
+ - - + + - {% endfor %} -
{{ banque.name }} - {% can_edit banque %} - - - - {% acl_end %} - {% history_button banque %} - {% trans "Bank" %}
+ + {% for banque in banque_list %} + + {{ banque.name }} + + {% can_edit banque %} + {% include 'buttons/edit.html' with href='cotisations:edit-banque' id=banque.id %} + {% acl_end %} + {% history_button banque %} + + + {% endfor %} + diff --git a/cotisations/templates/cotisations/aff_cotisations.html b/cotisations/templates/cotisations/aff_cotisations.html index 6b5fa8fa..30de85dc 100644 --- a/cotisations/templates/cotisations/aff_cotisations.html +++ b/cotisations/templates/cotisations/aff_cotisations.html @@ -49,7 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %} - {% trans "Invoice id" as tr_invoice_id %} + {% trans "Invoice ID" as tr_invoice_id %} {% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %} @@ -65,32 +65,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ facture.date }} {{ facture.id }} -
+ {% can_edit facture %} + {% include 'buttons/edit.html' with href='cotisations:edit-facture' id=facture.id %} + {% acl_else %} + {% trans "Controlled invoice" %} + {% acl_end %} + {% can_delete facture %} + {% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %} + {% acl_end %} + {% history_button facture %} {% if facture.valid %} @@ -109,3 +92,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include 'pagination.html' with list=facture_list %} {% endif %} + diff --git a/cotisations/templates/cotisations/aff_custom_invoice.html b/cotisations/templates/cotisations/aff_custom_invoice.html new file mode 100644 index 00000000..41984c2c --- /dev/null +++ b/cotisations/templates/cotisations/aff_custom_invoice.html @@ -0,0 +1,89 @@ +{% 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 © 2018 Hugo Levy-Falk + +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 i18n %} +{% load acl %} +{% load logs_extra %} +{% load design %} + +
+ {% if custom_invoice_list.paginator %} + {% include 'pagination.html' with list=custom_invoice_list %} + {% endif %} + + + + + + + + + + + + + + + + {% for invoice in custom_invoice_list %} + + + + + + + + + + + {% endfor %} +
+ {% trans "Recipient" as tr_recip %} + {% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %} + {% trans "Designation" %}{% trans "Total price" %} + {% trans "Payment method" as tr_payment_method %} + {% include 'buttons/sort.html' with prefix='invoice' col='payement' text=tr_payment_method %} + + {% trans "Date" as tr_date %} + {% include 'buttons/sort.html' with prefix='invoice' col='date' text=tr_date %} + + {% trans "Invoice ID" as tr_invoice_id %} + {% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_invoice_id %} + + {% trans "Paid" as tr_invoice_paid%} + {% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_paid %} +
{{ invoice.recipient }}{{ invoice.name }}{{ invoice.prix_total }}{{ invoice.payment }}{{ invoice.date }}{{ invoice.id }}{{ invoice.paid|tick }} + {% can_edit invoice %} + {% include 'buttons/edit.html' with href='cotisations:edit-custom-invoice' id=invoice.id %} + {% acl_end %} + {% can_delete invoice %} + {% include 'buttons/suppr.html' with href='cotisations:del-custom-invoice' id=invoice.id %} + {% acl_end %} + {% history_button invoice %} + + {% trans "PDF" %} + +
+ + {% if custom_invoice_list.paginator %} + {% include 'pagination.html' with list=custom_invoice_list %} + {% endif %} +
diff --git a/cotisations/templates/cotisations/aff_paiement.html b/cotisations/templates/cotisations/aff_paiement.html index 46523928..633eb456 100644 --- a/cotisations/templates/cotisations/aff_paiement.html +++ b/cotisations/templates/cotisations/aff_paiement.html @@ -41,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ paiement.moyen }} {{ paiement.available_for_everyone|tick }} - {{paiement.get_payment_method_name}} + {{ paiement.get_payment_method_name }} {% can_edit paiement %} diff --git a/cotisations/templates/cotisations/control.html b/cotisations/templates/cotisations/control.html index bb3a06b6..6a4a5cca 100644 --- a/cotisations/templates/cotisations/control.html +++ b/cotisations/templates/cotisations/control.html @@ -30,17 +30,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Invoice control" %}{% endblock %} {% block content %} +

{% trans "Invoice control and validation" %}

+ {% if facture_list.paginator %} {% include 'pagination.html' with list=facture_list %} {% endif %} +
{% csrf_token %} {{ controlform.management_form }} - + @@ -65,7 +68,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Profil" %}{% trans "Profile" %} {% trans "Last name" as tr_last_name %} {% include 'buttons/sort.html' with prefix='control' col='name' text=tr_last_name %} @@ -50,11 +53,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include 'buttons/sort.html' with prefix='control' col='surname' text=tr_first_name %} - {% trans "Invoice id" as tr_invoice_id %} + {% trans "Invoice ID" as tr_invoice_id %} {% include 'buttons/sort.html' with prefix='control' col='id' text=tr_invoice_id %} - {% trans "User id" as tr_user_id %} + {% trans "User ID" as tr_user_id %} {% include 'buttons/sort.html' with prefix='control' col='user-id' text=tr_user_id %} {% trans "Designation" %} {% trans "Date" as tr_date %} - {% include 'buttons/sort.html' with prefix='control' col='date' text=tr_date %}i + {% include 'buttons/sort.html' with prefix='control' col='date' text=tr_date %} {% trans "Validated" as tr_validated %} @@ -109,3 +112,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if facture_list.paginator %} {% include 'pagination.html' with list=facture_list %} {% endif %} + diff --git a/cotisations/templates/cotisations/delete.html b/cotisations/templates/cotisations/delete.html index a1c95d7a..dc06e5a5 100644 --- a/cotisations/templates/cotisations/delete.html +++ b/cotisations/templates/cotisations/delete.html @@ -26,18 +26,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load i18n %} -{% block title %}{% trans "Deletion of cotisations" %}{% endblock %} +{% block title %}{% trans "Deletion of subscriptions" %}{% endblock %} {% block content %} {% csrf_token %}

- {% blocktrans %} - Warning. Are you sure you really want te delete this {{ object_name }} object ( {{ objet }} ) ? - {% endblocktrans %} + {% blocktrans %}Warning: are you sure you really want to delete this {{ object_name }} object ( {{ objet }} )?{% endblocktrans %}

{% trans "Confirm" as tr_confirm %} {% bootstrap_button tr_confirm button_type='submit' icon='trash' %} {% endblock %} + diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index d28f8511..9ddcac8c 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load massive_bootstrap_form %} {% load i18n %} -{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} +{% block title %}{% trans "Creation and editing of invoices" %}{% endblock %} {% block content %} {% bootstrap_form_errors factureform %} @@ -62,3 +62,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endblock %} + diff --git a/cotisations/templates/cotisations/email_invoice b/cotisations/templates/cotisations/email_invoice new file mode 100644 index 00000000..8d6b2cc2 --- /dev/null +++ b/cotisations/templates/cotisations/email_invoice @@ -0,0 +1,22 @@ +=== English version below === + +Bonjour {{name}}, + +Nous vous remercions pour votre achat auprès de {{asso_name}} et nous vous en joignons la facture. + +En cas de question, n’hésitez pas à nous contacter par mail à {{contact_mail}}. + +Cordialement, +L’équipe de {{asso_name}} + + +=== English version === + +Dear {{name}}, + +Thank you for your purchase. Here is your invoice. + +Should you need extra information, you can email us at {{contact_mail}}. + +Best regards, + {{ asso_name }}'s team diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 4ec05ec1..4f905160 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -27,20 +27,21 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load staticfiles%} {% load i18n %} -{% block title %}{% trans "Invoices creation and edition" %}{% endblock %} +{% block title %}{% trans "Creation and editing of invoices" %}{% endblock %} {% block content %} + {% if title %} -

{{title}}

+

{{ title }}

{% else %}

{% trans "New invoice" %}

{% endif %} {% if max_balance %} -

{% trans "Maximum allowed balance : "%}{{max_balance}} €

+

{% blocktrans %}Maximum allowed balance: {{ max_balance }} €{% endblocktrans %}

{% endif %} {% if balance is not None %}

- {% trans "Current balance :" %} {{ balance }} € +{% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}

{% endif %} @@ -68,9 +69,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

- {% blocktrans %} - Total price : 0,00 € - {% endblocktrans %} + {% blocktrans %}Total price: 0,00 €{% endblocktrans %}

{% endif %} {% bootstrap_button action_name button_type='submit' icon='star' %} @@ -183,3 +182,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% endblock %} + diff --git a/cotisations/templates/cotisations/index.html b/cotisations/templates/cotisations/index.html index 9482cb5a..ca9cde5b 100644 --- a/cotisations/templates/cotisations/index.html +++ b/cotisations/templates/cotisations/index.html @@ -29,7 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Invoices" %}{% endblock %} {% block content %} -

{% trans "Cotisations" %}

+

{% trans "Subscriptions" %}

{% include 'cotisations/aff_cotisations.html' with facture_list=facture_list %} {% endblock %} diff --git a/cotisations/templates/cotisations/index_article.html b/cotisations/templates/cotisations/index_article.html index 5e6c3967..41ffb62e 100644 --- a/cotisations/templates/cotisations/index_article.html +++ b/cotisations/templates/cotisations/index_article.html @@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} - {% trans "Delete article types" %} + {% trans "Delete one or several article types" %} {% include 'cotisations/aff_article.html' with article_list=article_list %} {% endblock %} diff --git a/cotisations/templates/cotisations/index_banque.html b/cotisations/templates/cotisations/index_banque.html index e9118d75..f4dea1b1 100644 --- a/cotisations/templates/cotisations/index_banque.html +++ b/cotisations/templates/cotisations/index_banque.html @@ -37,7 +37,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} - {% trans "Delete banks" %} + {% trans "Delete one or several banks" %} {% include 'cotisations/aff_banque.html' with banque_list=banque_list %} {% endblock %} diff --git a/cotisations/templates/cotisations/index_custom_invoice.html b/cotisations/templates/cotisations/index_custom_invoice.html new file mode 100644 index 00000000..67d00126 --- /dev/null +++ b/cotisations/templates/cotisations/index_custom_invoice.html @@ -0,0 +1,36 @@ +{% extends "cotisations/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 Gabriel Détraz +Copyright © 2017 Goulven Kermarec +Copyright © 2017 Augustin Lemesle + +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 acl %} +{% load i18n %} + +{% block title %}{% trans "Custom invoices" %}{% endblock %} + +{% block content %} +

{% trans "Custom invoices list" %}

+{% can_create CustomInvoice %} +{% include "buttons/add.html" with href='cotisations:new-custom-invoice'%} +{% acl_end %} +{% include 'cotisations/aff_custom_invoice.html' with custom_invoice_list=custom_invoice_list %} +{% endblock %} diff --git a/cotisations/templates/cotisations/index_paiement.html b/cotisations/templates/cotisations/index_paiement.html index d84c72eb..f4908d02 100644 --- a/cotisations/templates/cotisations/index_paiement.html +++ b/cotisations/templates/cotisations/index_paiement.html @@ -27,17 +27,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load i18n %} -{% block title %}{% trans "Payments" %}{% endblock %} +{% block title %}{% trans "Payment methods" %}{% endblock %} {% block content %} -

{% trans "Payment types list" %}

+

{% trans "List of payment methods" %}

{% can_create Paiement %} - {% trans "Add a payment type" %} + {% trans "Add a payment method" %} {% acl_end %} - {% trans "Delete payment types" %} + {% trans "Delete one or several payment methods" %} {% include 'cotisations/aff_paiement.html' with paiement_list=paiement_list %} {% endblock %} diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html index e1c8b0d0..997168fd 100644 --- a/cotisations/templates/cotisations/payment.html +++ b/cotisations/templates/cotisations/payment.html @@ -31,11 +31,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

- {% blocktrans %} - Pay {{ amount }} € - {% endblocktrans %} + {% blocktrans %}Pay {{ amount }} €{% endblocktrans %}

-
+ {{ content | safe }} {% if form %} {% csrf_token %} @@ -45,3 +43,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %}
{% endblock %} + diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 296730f2..4f077fad 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -27,8 +27,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load i18n %} {% block sidebar %} - {% can_change Facture pdf %} - + {% can_create CustomInvoice %} + {% trans "Create an invoice" %} @@ -40,6 +40,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Invoices" %} {% acl_end %} + {% can_view_all CustomInvoice %} + + {% trans "Custom invoices" %} + + {% acl_end %} {% can_view_all Article %} {% trans "Available articles" %} @@ -56,3 +61,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} {% endblock %} + diff --git a/cotisations/tex.py b/cotisations/tex.py index f456fe8a..0ee45bdb 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -1,3 +1,4 @@ +# coding: utf-8 # 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. @@ -24,6 +25,7 @@ Module in charge of rendering some LaTex templates. Used to generated PDF invoice. """ + import tempfile from subprocess import Popen, PIPE import os @@ -61,18 +63,24 @@ def render_invoice(_request, ctx={}): return r -def render_tex(_request, template, ctx={}): - """ - Creates a PDF from a LaTex templates using pdflatex. - Writes it in a temporary directory and send back an HTTP response for - accessing this file. +def create_pdf(template, ctx={}): + """Creates and returns a PDF from a LaTeX template using pdflatex. + + It create a temporary file for the PDF then read it to return its content. + + Args: + template: Path to the LaTeX template. + ctx: Dict with the context for rendering the template. + + Returns: + The content of the temporary PDF file generated. """ context = Context(ctx) template = get_template(template) rendered_tpl = template.render(context).encode('utf-8') with tempfile.TemporaryDirectory() as tempdir: - for i in range(2): + for _ in range(2): process = Popen( ['pdflatex', '-output-directory', tempdir], stdin=PIPE, @@ -81,6 +89,25 @@ def render_tex(_request, template, ctx={}): process.communicate(rendered_tpl) with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f: pdf = f.read() + + return pdf + + +def render_tex(_request, template, ctx={}): + """Creates a PDF from a LaTex templates using pdflatex. + + Calls `create_pdf` and send back an HTTP response for + accessing this file. + + Args: + _request: Unused, but allow using this function as a Django view. + template: Path to the LaTeX template. + ctx: Dict with the context for rendering the template. + + Returns: + An HttpResponse with type `application/pdf` containing the PDF file. + """ + pdf = create_pdf(template, ctx) r = HttpResponse(content_type='application/pdf') r.write(pdf) return r diff --git a/cotisations/urls.py b/cotisations/urls.py index 470ccbfa..edc448fe 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -52,9 +52,29 @@ urlpatterns = [ name='facture-pdf' ), url( - r'^new_facture_pdf/$', - views.new_facture_pdf, - name='new-facture-pdf' + r'^index_custom_invoice/$', + views.index_custom_invoice, + name='index-custom-invoice' + ), + url( + r'^new_custom_invoice/$', + views.new_custom_invoice, + name='new-custom-invoice' + ), + url( + r'^edit_custom_invoice/(?P[0-9]+)$', + views.edit_custom_invoice, + name='edit-custom-invoice' + ), + url( + r'^custom_invoice_pdf/(?P[0-9]+)$', + views.custom_invoice_pdf, + name='custom-invoice-pdf', + ), + url( + r'^del_custom_invoice/(?P[0-9]+)$', + views.del_custom_invoice, + name='del-custom-invoice' ), url( r'^credit_solde/(?P[0-9]+)$', diff --git a/cotisations/utils.py b/cotisations/utils.py index f36b376f..0211dd40 100644 --- a/cotisations/utils.py +++ b/cotisations/utils.py @@ -19,6 +19,16 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +import os + +from django.template.loader import get_template +from django.core.mail import EmailMessage + +from .tex import create_pdf +from preferences.models import AssoOption, GeneralOption +from re2o.settings import LOGO_PATH +from re2o import settings + def find_payment_method(payment): """Finds the payment method associated to the payment if it exists.""" @@ -30,3 +40,56 @@ def find_payment_method(payment): except method.PaymentMethod.DoesNotExist: pass return None + + +def send_mail_invoice(invoice): + """Creates the pdf of the invoice and sends it by email to the client""" + purchases_info = [] + for purchase in invoice.vente_set.all(): + purchases_info.append({ + 'name': purchase.name, + 'price': purchase.prix, + 'quantity': purchase.number, + 'total_price': purchase.prix_total + }) + + ctx = { + 'paid': True, + 'fid': invoice.id, + 'DATE': invoice.date, + 'recipient_name': "{} {}".format( + invoice.user.name, + invoice.user.surname + ), + 'address': invoice.user.room, + 'article': purchases_info, + 'total': invoice.prix_total(), + 'asso_name': AssoOption.get_cached_value('name'), + 'line1': AssoOption.get_cached_value('adresse1'), + 'line2': AssoOption.get_cached_value('adresse2'), + 'siret': AssoOption.get_cached_value('siret'), + 'email': AssoOption.get_cached_value('contact'), + 'phone': AssoOption.get_cached_value('telephone'), + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + } + + pdf = create_pdf('cotisations/factures.tex', ctx) + template = get_template('cotisations/email_invoice') + + ctx = { + 'name': "{} {}".format( + invoice.user.name, + invoice.user.surname + ), + 'contact_mail': AssoOption.get_cached_value('contact'), + 'asso_name': AssoOption.get_cached_value('name') + } + + mail = EmailMessage( + 'Votre facture / Your invoice', + template.render(ctx), + GeneralOption.get_cached_value('email_from'), + [invoice.user.email], + attachments=[('invoice.pdf', pdf, 'application/pdf')] + ) + mail.send() diff --git a/cotisations/validators.py b/cotisations/validators.py index fa8ea2cf..b1683e82 100644 --- a/cotisations/validators.py +++ b/cotisations/validators.py @@ -17,5 +17,6 @@ def check_no_balance(is_balance): p = Paiement.objects.filter(is_balance=True) if len(p) > 0: raise ValidationError( - _("There are already payment method(s) for user balance") + _("There is already a payment method for user balance.") ) + diff --git a/cotisations/views.py b/cotisations/views.py index 8b9fe79e..a4a35825 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -58,7 +58,15 @@ from re2o.acl import ( can_change, ) from preferences.models import AssoOption, GeneralOption -from .models import Facture, Article, Vente, Paiement, Banque +from .models import ( + Facture, + Article, + Vente, + Paiement, + Banque, + CustomInvoice, + BaseInvoice +) from .forms import ( FactureForm, ArticleForm, @@ -67,14 +75,13 @@ from .forms import ( DelPaiementForm, BanqueForm, DelBanqueForm, - NewFactureFormPdf, - SelectUserArticleForm, - SelectClubArticleForm, - RechargeForm + SelectArticleForm, + RechargeForm, + CustomInvoiceForm ) from .tex import render_invoice from .payment_methods.forms import payment_method_factory -from .utils import find_payment_method +from .utils import find_payment_method, send_mail_invoice @login_required @@ -102,16 +109,10 @@ def new_facture(request, user, userid): creation=True ) - if request.user.is_class_club: - article_formset = formset_factory(SelectClubArticleForm)( - request.POST or None, - form_kwargs={'user': request.user} - ) - else: - article_formset = formset_factory(SelectUserArticleForm)( - request.POST or None, - form_kwargs={'user': request.user} - ) + article_formset = formset_factory(SelectArticleForm)( + request.POST or None, + form_kwargs={'user': request.user, 'target_user': user} + ) if invoice_form.is_valid() and article_formset.is_valid(): new_invoice_instance = invoice_form.save(commit=False) @@ -147,6 +148,8 @@ def new_facture(request, user, userid): p.facture = new_invoice_instance p.save() + send_mail_invoice(new_invoice_instance) + return new_invoice_instance.paiement.end_payment( new_invoice_instance, request @@ -161,6 +164,7 @@ def new_facture(request, user, userid): balance = user.solde else: balance = None + return form( { 'factureform': invoice_form, @@ -175,10 +179,10 @@ def new_facture(request, user, userid): # TODO : change facture to invoice @login_required -@can_change(Facture, 'pdf') -def new_facture_pdf(request): +@can_create(CustomInvoice) +def new_custom_invoice(request): """ - View used to generate a custom PDF invoice. It's mainly used to + View used to generate a custom invoice. It's mainly used to get invoices that are not taken into account, for the administrative point of view. """ @@ -187,56 +191,39 @@ def new_facture_pdf(request): Q(type_user='All') | Q(type_user=request.user.class_name) ) # Building the invocie form and the article formset - invoice_form = NewFactureFormPdf(request.POST or None) - if request.user.is_class_club: - articles_formset = formset_factory(SelectClubArticleForm)( - request.POST or None, - form_kwargs={'user': request.user} - ) - else: - articles_formset = formset_factory(SelectUserArticleForm)( - request.POST or None, - form_kwargs={'user': request.user} - ) - if invoice_form.is_valid() and articles_formset.is_valid(): - # Get the article list and build an list out of it - # contiaining (article_name, article_price, quantity, total_price) - articles_info = [] - for articles_form in articles_formset: - if articles_form.cleaned_data: - article = articles_form.cleaned_data['article'] - quantity = articles_form.cleaned_data['quantity'] - articles_info.append({ - 'name': article.name, - 'price': article.prix, - 'quantity': quantity, - 'total_price': article.prix * quantity - }) - paid = invoice_form.cleaned_data['paid'] - recipient = invoice_form.cleaned_data['dest'] - address = invoice_form.cleaned_data['chambre'] - total_price = sum(a['total_price'] for a in articles_info) + invoice_form = CustomInvoiceForm(request.POST or None) + + article_formset = formset_factory(SelectArticleForm)( + request.POST or None, + form_kwargs={'user': request.user, 'target_user': user} + ) + + if invoice_form.is_valid() and articles_formset.is_valid(): + new_invoice_instance = invoice_form.save() + for art_item in articles_formset: + if art_item.cleaned_data: + article = art_item.cleaned_data['article'] + quantity = art_item.cleaned_data['quantity'] + Vente.objects.create( + facture=new_invoice_instance, + name=article.name, + prix=article.prix, + type_cotisation=article.type_cotisation, + duration=article.duration, + number=quantity + ) + messages.success( + request, + _("The custom invoice was created.") + ) + return redirect(reverse('cotisations:index-custom-invoice')) + - return render_invoice(request, { - 'DATE': timezone.now(), - 'recipient_name': recipient, - 'address': address, - 'article': articles_info, - 'total': total_price, - 'paid': paid, - 'asso_name': AssoOption.get_cached_value('name'), - 'line1': AssoOption.get_cached_value('adresse1'), - 'line2': AssoOption.get_cached_value('adresse2'), - 'siret': AssoOption.get_cached_value('siret'), - 'email': AssoOption.get_cached_value('contact'), - 'phone': AssoOption.get_cached_value('telephone'), - 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) - }) return form({ 'factureform': invoice_form, 'action_name': _("Create"), 'articlesformset': articles_formset, - 'articles': articles + 'articlelist': articles }, 'cotisations/facture.html', request) @@ -289,7 +276,7 @@ def facture_pdf(request, facture, **_kwargs): def edit_facture(request, facture, **_kwargs): """ View used to edit an existing invoice. - Articles can be added or remove to the invoice and quantity + Articles can be added or removed to the invoice and quantity can be set as desired. This is also the view used to invalidate an invoice. """ @@ -315,7 +302,7 @@ def edit_facture(request, facture, **_kwargs): purchase_form.save() messages.success( request, - _("The invoice has been successfully edited.") + _("The invoice was edited.") ) return redirect(reverse('cotisations:index')) return form({ @@ -335,7 +322,7 @@ def del_facture(request, facture, **_kwargs): facture.delete() messages.success( request, - _("The invoice has been successfully deleted.") + _("The invoice was deleted.") ) return redirect(reverse('cotisations:index')) return form({ @@ -344,6 +331,100 @@ def del_facture(request, facture, **_kwargs): }, 'cotisations/delete.html', request) +@login_required +@can_edit(CustomInvoice) +def edit_custom_invoice(request, invoice, **kwargs): + # Building the invocie form and the article formset + invoice_form = CustomInvoiceForm( + request.POST or None, + instance=invoice + ) + purchases_objects = Vente.objects.filter(facture=invoice) + purchase_form_set = modelformset_factory( + Vente, + fields=('name', 'number'), + extra=0, + max_num=len(purchases_objects) + ) + purchase_form = purchase_form_set( + request.POST or None, + queryset=purchases_objects + ) + if invoice_form.is_valid() and purchase_form.is_valid(): + if invoice_form.changed_data: + invoice_form.save() + purchase_form.save() + messages.success( + request, + _("The invoice was edited.") + ) + return redirect(reverse('cotisations:index-custom-invoice')) + + return form({ + 'factureform': invoice_form, + 'venteform': purchase_form + }, 'cotisations/edit_facture.html', request) + + +@login_required +@can_view(CustomInvoice) +def custom_invoice_pdf(request, invoice, **_kwargs): + """ + View used to generate a PDF file from an existing invoice in database + Creates a line for each Purchase (thus article sold) and generate the + invoice with the total price, the payment method, the address and the + legal information for the user. + """ + # TODO : change vente to purchase + purchases_objects = Vente.objects.all().filter(facture=invoice) + # Get the article list and build an list out of it + # contiaining (article_name, article_price, quantity, total_price) + purchases_info = [] + for purchase in purchases_objects: + purchases_info.append({ + 'name': purchase.name, + 'price': purchase.prix, + 'quantity': purchase.number, + 'total_price': purchase.prix_total + }) + return render_invoice(request, { + 'paid': invoice.paid, + 'fid': invoice.id, + 'DATE': invoice.date, + 'recipient_name': invoice.recipient, + 'address': invoice.address, + 'article': purchases_info, + 'total': invoice.prix_total(), + 'asso_name': AssoOption.get_cached_value('name'), + 'line1': AssoOption.get_cached_value('adresse1'), + 'line2': AssoOption.get_cached_value('adresse2'), + 'siret': AssoOption.get_cached_value('siret'), + 'email': AssoOption.get_cached_value('contact'), + 'phone': AssoOption.get_cached_value('telephone'), + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH) + }) + + +# TODO : change facture to invoice +@login_required +@can_delete(CustomInvoice) +def del_custom_invoice(request, invoice, **_kwargs): + """ + View used to delete an existing invocie. + """ + if request.method == "POST": + invoice.delete() + messages.success( + request, + _("The invoice was deleted.") + ) + return redirect(reverse('cotisations:index-custom-invoice')) + return form({ + 'objet': invoice, + 'objet_name': _("Invoice") + }, 'cotisations/delete.html', request) + + @login_required @can_create(Article) def add_article(request): @@ -361,7 +442,7 @@ def add_article(request): article.save() messages.success( request, - _("The article has been successfully created.") + _("The article was created.") ) return redirect(reverse('cotisations:index-article')) return form({ @@ -383,7 +464,7 @@ def edit_article(request, article_instance, **_kwargs): article.save() messages.success( request, - _("The article has been successfully edited.") + _("The article was edited.") ) return redirect(reverse('cotisations:index-article')) return form({ @@ -405,7 +486,7 @@ def del_article(request, instances): article_del.delete() messages.success( request, - _("The article(s) have been successfully deleted.") + _("The articles were deleted.") ) return redirect(reverse('cotisations:index-article')) return form({ @@ -433,7 +514,7 @@ def add_paiement(request): payment_method.save(payment) messages.success( request, - _("The payment method has been successfully created.") + _("The payment method was created.") ) return redirect(reverse('cotisations:index-paiement')) return form({ @@ -469,8 +550,7 @@ def edit_paiement(request, paiement_instance, **_kwargs): if payment_method is not None: payment_method.save() messages.success( - request, - _("The payement method has been successfully edited.") + request,_("The payment method was edited.") ) return redirect(reverse('cotisations:index-paiement')) return form({ @@ -496,8 +576,7 @@ def del_paiement(request, instances): payment_del.delete() messages.success( request, - _("The payment method %(method_name)s has been \ - successfully deleted.") % { + _("The payment method %(method_name)s was deleted.") % { 'method_name': payment_del } ) @@ -529,7 +608,7 @@ def add_banque(request): bank.save() messages.success( request, - _("The bank has been successfully created.") + _("The bank was created.") ) return redirect(reverse('cotisations:index-banque')) return form({ @@ -552,7 +631,7 @@ def edit_banque(request, banque_instance, **_kwargs): bank.save() messages.success( request, - _("The bank has been successfully edited") + _("The bank was edited.") ) return redirect(reverse('cotisations:index-banque')) return form({ @@ -577,8 +656,7 @@ def del_banque(request, instances): bank_del.delete() messages.success( request, - _("The bank %(bank_name)s has been successfully \ - deleted.") % { + _("The bank %(bank_name)s was deleted.") % { 'bank_name': bank_del } ) @@ -678,8 +756,31 @@ def index_banque(request): }) +@login_required +@can_view_all(CustomInvoice) +def index_custom_invoice(request): + """View used to display every custom invoice.""" + pagination_number = GeneralOption.get_cached_value('pagination_number') + custom_invoice_list = CustomInvoice.objects.prefetch_related('vente_set') + custom_invoice_list = SortTable.sort( + custom_invoice_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_CUSTOM + ) + custom_invoice_list = re2o_paginator( + request, + custom_invoice_list, + pagination_number, + ) + return render(request, 'cotisations/index_custom_invoice.html', { + 'custom_invoice_list': custom_invoice_list + }) + + @login_required @can_view_all(Facture) +@can_view_all(CustomInvoice) def index(request): """ View used to display the list of all exisitng invoices. @@ -695,7 +796,7 @@ def index(request): ) invoice_list = re2o_paginator(request, invoice_list, pagination_number) return render(request, 'cotisations/index.html', { - 'facture_list': invoice_list + 'facture_list': invoice_list, }) @@ -726,7 +827,7 @@ def credit_solde(request, user, **_kwargs): kwargs={'userid': user.id} )) - refill_form = RechargeForm(request.POST or None, user=request.user) + refill_form = RechargeForm(request.POST or None, user=user, user_source=request.user) if refill_form.is_valid(): price = refill_form.cleaned_data['value'] invoice = Facture(user=user) @@ -746,12 +847,16 @@ def credit_solde(request, user, **_kwargs): prix=refill_form.cleaned_data['value'], number=1 ) + + send_mail_invoice(invoice) + return invoice.paiement.end_payment(invoice, request) p = get_object_or_404(Paiement, is_balance=True) return form({ 'factureform': refill_form, - 'balance': request.user.solde, + 'balance': user.solde, 'title': _("Refill your balance"), 'action_name': _("Pay"), 'max_balance': p.payment_method.maximum_balance, }, 'cotisations/facture.html', request) + diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index fcf55cfa..afa834b0 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -63,6 +63,7 @@ from preferences.models import OptionalTopologie options, created = OptionalTopologie.objects.get_or_create() VLAN_NOK = options.vlan_decision_nok.vlan_id VLAN_OK = options.vlan_decision_ok.vlan_id +RADIUS_POLICY = options.radius_general_policy #: Serveur radius de test (pas la prod) TEST_SERVER = bool(os.getenv('DBG_FREERADIUS', False)) @@ -347,7 +348,7 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, if not nas_machine: return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK) - sw_name = str(nas_machine) + sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine))) port = (Port.objects .filter( @@ -355,27 +356,47 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, port=port_number ) .first()) + # Si le port est inconnu, on place sur le vlan defaut + # Aucune information particulière ne permet de déterminer quelle + # politique à appliquer sur ce port if not port: return (sw_name, "Chambre inconnue", u'Port inconnu', VLAN_OK) - # Si un vlan a été précisé, on l'utilise pour VLAN_OK - if port.vlan_force: - DECISION_VLAN = int(port.vlan_force.vlan_id) + # On récupère le profil du port + port_profile = port.get_port_profile + + # Si un vlan a été précisé dans la config du port, + # on l'utilise pour VLAN_OK + if port_profile.vlan_untagged: + DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) extra_log = u"Force sur vlan " + str(DECISION_VLAN) else: DECISION_VLAN = VLAN_OK - if port.radius == 'NO': + # Si le port est désactivé, on rejette sur le vlan de déconnexion + if not port.state: + return (sw_name, port.room, u'Port desactivé', VLAN_NOK) + + # Si radius est désactivé, on laisse passer + if port_profile.radius_type == 'NO': return (sw_name, "", u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN) - if port.radius == 'BLOQ': - return (sw_name, port.room, u'Port desactive', VLAN_NOK) + # Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment + # Par conséquent, on laisse passer sur le bon vlan + if nas_type.port_access_mode == '802.1X' and port_profile.radius_type == '802.1X': + room = port.room or "Chambre/local inconnu" + return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN) - if port.radius == 'STRICT': + # Sinon, cela veut dire qu'on fait de l'auth radius par mac + # Si le port est en mode strict, on vérifie que tous les users + # rattachés à ce port sont bien à jour de cotisation. Sinon on rejette (anti squattage) + # Il n'est pas possible de se connecter sur une prise strict sans adhérent à jour de cotis + # dedans + if port_profile.radius_mode == 'STRICT': room = port.room if not room: return (sw_name, "Inconnue", u'Chambre inconnue', VLAN_NOK) @@ -390,7 +411,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, return (sw_name, room, u'Chambre resident desactive', VLAN_NOK) # else: user OK, on passe à la verif MAC - if port.radius == 'COMMON' or port.radius == 'STRICT': + # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd + if port_profile.radius_mode == 'COMMON' or port_profile.radius_mode == 'STRICT': # Authentification par mac interface = (Interface.objects .filter(mac_address=mac_address) @@ -399,15 +421,19 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, .first()) if not interface: room = port.room - # On essaye de register la mac + # On essaye de register la mac, si l'autocapture a été activée + # Sinon on rejette sur vlan_nok if not nas_type.autocapture_mac: return (sw_name, "", u'Machine inconnue', VLAN_NOK) + # On ne peut autocapturer que si on connait la chambre et donc l'user correspondant elif not room: return (sw_name, "Inconnue", u'Chambre et machine inconnues', VLAN_NOK) else: + # Si la chambre est vide (local club, prises en libre services) + # Impossible d'autocapturer if not room_user: room_user = User.objects.filter( Q(club__room=port.room) | Q(adherent__room=port.room) @@ -418,6 +444,8 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, u'Machine et propriétaire de la chambre ' 'inconnus', VLAN_NOK) + # Si il y a plus d'un user dans la chambre, impossible de savoir à qui + # Ajouter la machine elif room_user.count() > 1: return (sw_name, room, @@ -425,19 +453,24 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, 'dans la chambre/local -> ajout de mac ' 'automatique impossible', VLAN_NOK) + # Si l'adhérent de la chambre n'est pas à jour de cotis, pas d'autocapture elif not room_user.first().has_access(): return (sw_name, room, u'Machine inconnue et adhérent non cotisant', VLAN_NOK) + # Sinon on capture et on laisse passer sur le bon vlan else: - result, reason = (room_user + interface, reason = (room_user .first() .autoregister_machine( mac_address, nas_type )) - if result: + if interface: + ## Si on choisi de placer les machines sur le vlan correspondant à leur type : + if RADIUS_POLICY == 'MACHINE': + DECISION_VLAN = interface.type.ip_type.vlan.vlan_id return (sw_name, room, u'Access Ok, Capture de la mac: ' + extra_log, @@ -449,6 +482,9 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, reason + str(mac_address) ), VLAN_NOK) + # L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject + # Si elle n'a pas d'ipv4, on lui en met une + # Enfin on laisse passer sur le vlan pertinent else: room = port.room if not interface.is_active: @@ -456,7 +492,10 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, room, u'Machine non active / adherent non cotisant', VLAN_NOK) - elif not interface.ipv4: + ## Si on choisi de placer les machines sur le vlan correspondant à leur type : + if RADIUS_POLICY == 'MACHINE': + DECISION_VLAN = interface.type.ip_type.vlan.vlan_id + if not interface.ipv4: interface.assign_ipv4() return (sw_name, room, diff --git a/freeradius_utils/freeradius3/mods-enabled/python b/freeradius_utils/freeradius3/mods-enabled/python index 414860a3..d4e99f35 100644 --- a/freeradius_utils/freeradius3/mods-enabled/python +++ b/freeradius_utils/freeradius3/mods-enabled/python @@ -9,7 +9,7 @@ python re2o { module = auth - python_path = /etc/freeradius/3.0:/usr/lib/python2.7/:/usr/lib/python2.7/dist-packages/:/usr/local/lib/python2.7/site-packages/:/usr/local/lib/python2.7/dist-packages/ + python_path = /etc/freeradius/3.0:/usr/lib/python2.7:/usr/lib/python2.7/dist-packages:/usr/local/lib/python2.7/site-packages:/usr/lib/python2.7/lib-dynload:/usr/local/lib/python2.7/dist-packages mod_instantiate = ${.module} func_instantiate = instantiate diff --git a/install_utils/schema.ldiff b/install_utils/schema.ldiff index 4c217191..194f886a 100644 --- a/install_utils/schema.ldiff +++ b/install_utils/schema.ldiff @@ -1157,6 +1157,7 @@ olcDbIndex: dc eq olcDbIndex: entryCSN eq olcDbIndex: entryUUID eq olcDbIndex: radiusCallingStationId eq +olcSizeLimit: 50000 structuralObjectClass: olcHdbConfig entryUUID: fc8fa138-514b-1034-9c36-0faf5bc7ead5 creatorsName: cn=admin,cn=config diff --git a/logs/acl.py b/logs/acl.py index 1ec227d3..ee9a7b1b 100644 --- a/logs/acl.py +++ b/logs/acl.py @@ -25,6 +25,7 @@ Here are defined some functions to check acl on the application. """ +from django.utils.translation import ugettext as _ def can_view(user): @@ -38,4 +39,6 @@ def can_view(user): viewing is granted and msg is a message (can be None). """ can = user.has_module_perms('admin') - return can, None if can else "Vous ne pouvez pas voir cette application." + return can, None if can else _("You don't have the right to view this" + " application.") + diff --git a/logs/locale/fr/LC_MESSAGES/django.mo b/logs/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 00000000..030b0cac Binary files /dev/null and b/logs/locale/fr/LC_MESSAGES/django.mo differ diff --git a/logs/locale/fr/LC_MESSAGES/django.po b/logs/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..70c58073 --- /dev/null +++ b/logs/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,338 @@ +# 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 © 2018 Maël Kervella +# +# 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. +msgid "" +msgstr "" +"Project-Id-Version: 2.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-08-15 20:12+0200\n" +"PO-Revision-Date: 2018-06-23 16:01+0200\n" +"Last-Translator: Laouen Fernet \n" +"Language-Team: \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: acl.py:42 +msgid "You don't have the right to view this application." +msgstr "Vous n'avez pas le droit de voir cette application." + +#: templates/logs/aff_stats_logs.html:36 +msgid "Edited object" +msgstr "Objet modifié" + +#: templates/logs/aff_stats_logs.html:37 +#: templates/logs/aff_stats_models.html:32 +msgid "Object type" +msgstr "Type d'objet" + +#: templates/logs/aff_stats_logs.html:38 +msgid "Edited by" +msgstr "Modifié par" + +#: templates/logs/aff_stats_logs.html:40 +msgid "Date of editing" +msgstr "Date de modification" + +#: templates/logs/aff_stats_logs.html:42 +msgid "Comment" +msgstr "Commentaire" + +#: templates/logs/aff_stats_logs.html:58 templates/logs/aff_summary.html:62 +#: templates/logs/aff_summary.html:85 templates/logs/aff_summary.html:104 +#: templates/logs/aff_summary.html:123 templates/logs/aff_summary.html:142 +msgid "Cancel" +msgstr "Annuler" + +#: templates/logs/aff_stats_models.html:29 +#, python-format +msgid "Statistics of the set %(key)s" +msgstr "Statistiques de l'ensemble %(key)s" + +#: templates/logs/aff_stats_models.html:33 +msgid "Number of stored entries" +msgstr "Nombre d'entrées enregistrées" + +#: templates/logs/aff_stats_users.html:31 +#, python-format +msgid "Statistics per %(key_dict)s of %(key)s" +msgstr "Statistiques par %(key_dict)s de %(key)s" + +#: templates/logs/aff_stats_users.html:34 +#, python-format +msgid "Number of %(key)s per %(key_dict)s" +msgstr "Nombre de %(key)s par %(key_dict)s" + +#: templates/logs/aff_stats_users.html:35 +msgid "Rank" +msgstr "Rang" + +#: templates/logs/aff_summary.html:37 +msgid "Date" +msgstr "Date" + +#: templates/logs/aff_summary.html:39 +msgid "Editing" +msgstr "Modification" + +#: templates/logs/aff_summary.html:48 +#, python-format +msgid "%(username)s has banned" +msgstr "%(username)s a banni" + +#: templates/logs/aff_summary.html:52 templates/logs/aff_summary.html:75 +msgid "No reason" +msgstr "Aucun motif" + +#: templates/logs/aff_summary.html:71 +#, python-format +msgid "%(username)s has graciously authorised" +msgstr "%(username)s a autorisé gracieusement" + +#: templates/logs/aff_summary.html:94 +#, python-format +msgid "%(username)s has updated" +msgstr "%(username)s a mis à jour" + +#: templates/logs/aff_summary.html:113 +#, python-format +msgid "%(username)s has sold %(number)sx %(name)s to" +msgstr "%(username)s a vendu %(number)sx %(name)s à" + +#: templates/logs/aff_summary.html:116 +#, python-format +msgid "+%(duration)s months" +msgstr "+%(duration)s mois" + +#: templates/logs/aff_summary.html:132 +#, python-format +msgid "%(username)s has edited an interface of" +msgstr "%(username)s a modifié une interface de" + +#: templates/logs/delete.html:29 +msgid "Deletion of actions" +msgstr "Suppression d'actions" + +#: templates/logs/delete.html:35 +#, python-format +msgid "" +"Warning: are you sure you want to delete this action %(objet_name)s " +"( %(objet)s )?" +msgstr "" +"Attention: voulez-vous vraiment supprimer cette action %(objet_name)s " +"( %(objet)s ) ?" + +#: templates/logs/delete.html:36 +msgid "Confirm" +msgstr "Confirmer" + +#: templates/logs/index.html:29 templates/logs/stats_general.html:29 +#: templates/logs/stats_logs.html:29 templates/logs/stats_models.html:29 +#: templates/logs/stats_users.html:29 +msgid "Statistics" +msgstr "Statistiques" + +#: templates/logs/index.html:32 templates/logs/stats_logs.html:32 views.py:403 +msgid "Actions performed" +msgstr "Actions effectuées" + +#: templates/logs/sidebar.html:33 +msgid "Summary" +msgstr "Résumé" + +#: templates/logs/sidebar.html:37 +msgid "Events" +msgstr "Évènements" + +#: templates/logs/sidebar.html:41 +msgid "General" +msgstr "Général" + +#: templates/logs/sidebar.html:45 +msgid "Database" +msgstr "Base de données" + +#: templates/logs/sidebar.html:49 +msgid "Wiring actions" +msgstr "Actions de câblage" + +#: templates/logs/sidebar.html:53 views.py:325 +msgid "Users" +msgstr "Utilisateurs" + +#: templates/logs/stats_general.html:32 +msgid "General statistics" +msgstr "Statistiques générales" + +#: templates/logs/stats_models.html:32 +msgid "Database statistics" +msgstr "Statistiques sur la base de données" + +#: templates/logs/stats_users.html:32 +msgid "Statistics about users" +msgstr "Statistiques sur les utilisateurs" + +#: views.py:191 +msgid "Nonexistent revision." +msgstr "Révision inexistante." + +#: views.py:194 +msgid "The action was deleted." +msgstr "L'action a été supprimée." + +#: views.py:227 +msgid "Category" +msgstr "Catégorie" + +#: views.py:228 +msgid "Number of users (members and clubs)" +msgstr "Nombre d'utilisateurs (adhérents et clubs)" + +#: views.py:229 +msgid "Number of members" +msgstr "Nombre d'adhérents" + +#: views.py:230 +msgid "Number of clubs" +msgstr "Nombre de clubs" + +#: views.py:234 +msgid "Activated users" +msgstr "Utilisateurs activés" + +#: views.py:242 +msgid "Disabled users" +msgstr "Utilisateurs désactivés" + +#: views.py:250 +msgid "Archived users" +msgstr "Utilisateurs archivés" + +#: views.py:258 +msgid "Contributing members" +msgstr "Adhérents cotisants" + +#: views.py:264 +msgid "Users benefiting from a connection" +msgstr "Utilisateurs bénéficiant d'une connexion" + +#: views.py:270 +msgid "Banned users" +msgstr "Utilisateurs bannis" + +#: views.py:276 +msgid "Users benefiting from a free connection" +msgstr "Utilisateurs bénéficiant d'une connexion gratuite" + +#: views.py:282 +msgid "Active interfaces (with access to the network)" +msgstr "Interfaces actives (ayant accès au réseau)" + +#: views.py:292 +msgid "Active interfaces assigned IPv4" +msgstr "Interfaces actives assignées IPv4" + +#: views.py:305 +msgid "IP range" +msgstr "Plage d'IP" + +#: views.py:306 +msgid "VLAN" +msgstr "VLAN" + +#: views.py:307 +msgid "Total number of IP addresses" +msgstr "Nombre total d'adresses IP" + +#: views.py:308 +msgid "Number of assigned IP addresses" +msgstr "Nombre d'adresses IP non assignées" + +#: views.py:309 +msgid "Number of IP address assigned to an activated machine" +msgstr "Nombre d'adresses IP assignées à une machine activée" + +#: views.py:310 +msgid "Number of nonassigned IP addresses" +msgstr "Nombre d'adresses IP non assignées" + +#: views.py:337 +msgid "Subscriptions" +msgstr "Cotisations" + +#: views.py:359 views.py:420 +msgid "Machines" +msgstr "Machines" + +#: views.py:386 +msgid "Topology" +msgstr "Topologie" + +#: views.py:405 +msgid "Number of actions" +msgstr "Nombre d'actions" + +#: views.py:419 views.py:437 views.py:442 views.py:447 views.py:462 +msgid "User" +msgstr "Utilisateur" + +#: views.py:423 +msgid "Invoice" +msgstr "Facture" + +#: views.py:426 +msgid "Ban" +msgstr "Bannissement" + +#: views.py:429 +msgid "Whitelist" +msgstr "Accès gracieux" + +#: views.py:432 +msgid "Rights" +msgstr "Droits" + +#: views.py:436 +msgid "School" +msgstr "Établissement" + +#: views.py:441 +msgid "Payment method" +msgstr "Moyen de paiement" + +#: views.py:446 +msgid "Bank" +msgstr "Banque" + +#: views.py:463 +msgid "Action" +msgstr "Action" + +#: views.py:494 +msgid "No model found." +msgstr "Aucun modèle trouvé." + +#: views.py:500 +msgid "Nonexistent entry." +msgstr "Entrée inexistante." + +#: views.py:507 +msgid "You don't have the right to access this menu." +msgstr "Vous n'avez pas le droit d'accéder à ce menu." diff --git a/logs/templates/logs/aff_stats_general.html b/logs/templates/logs/aff_stats_general.html index 49d067d0..662efa54 100644 --- a/logs/templates/logs/aff_stats_general.html +++ b/logs/templates/logs/aff_stats_general.html @@ -22,7 +22,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - {% for stats in stats_list %} +{% for stats in stats_list %} @@ -32,11 +32,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% for key, stat in stats.1.items %} - - {% for item in stat %} - - {% endfor %} - - {% endfor %} + + {% for item in stat %} + + {% endfor %} + + {% endfor %}
{{ item }}
{{ item }}
- {% endfor %} +{% endfor %} + diff --git a/logs/templates/logs/aff_stats_logs.html b/logs/templates/logs/aff_stats_logs.html index 77e9e9b4..1ca79df9 100644 --- a/logs/templates/logs/aff_stats_logs.html +++ b/logs/templates/logs/aff_stats_logs.html @@ -28,39 +28,43 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load logs_extra %} {% load acl %} +{% load i18n %} - - +
+ + + + + {% trans "Edited by" as tr_edited_by %} + + {% trans "Date of editing" as tr_date_of_editing %} + + + + + + {% for revision in revisions_list %} + {% for reversion in revision.version_set.all %} - - - - - - + + + + + + {% can_edit_history %} + + {% acl_end %} - - {% for revision in revisions_list %} - {% for reversion in revision.version_set.all %} - - - - - - - {% can_edit_history %} - - {% acl_end %} - - {% endfor %} {% endfor %} -
{% trans "Edited object" %}{% trans "Object type" %}{% include "buttons/sort.html" with prefix='logs' col='author' text=tr_edited_by %}{% include "buttons/sort.html" with prefix='logs' col='date' text=tr_date_of_editing %}{% trans "Comment" %}
Objet modifiéType de l'objet{% include "buttons/sort.html" with prefix='logs' col='author' text='Modification par' %}{% include "buttons/sort.html" with prefix='logs' col='date' text='Date de modification' %}Commentaire{{ reversion.object|truncatechars:20 }}{{ reversion.object|classname }}{{ revision.user }}{{ revision.date_created }}{{ revision.comment }} + + + {% trans "Cancel" %} + +
{{ reversion.object|truncatechars:20 }}{{ reversion.object|classname }}{{ revision.user }}{{ revision.date_created }}{{ revision.comment }} - - - Annuler - -
+ {% endfor %} +
{% if revisions_list.paginator %} {% include "pagination.html" with list=revisions_list %} {% endif %} + diff --git a/logs/templates/logs/aff_stats_models.html b/logs/templates/logs/aff_stats_models.html index bd035f82..7809dfdf 100644 --- a/logs/templates/logs/aff_stats_models.html +++ b/logs/templates/logs/aff_stats_models.html @@ -22,20 +22,23 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - {% for key, stats in stats_list.items %} +{% load i18n %} + +{% for key, stats in stats_list.items %} -

Statistiques de l'ensemble {{ key }}

- - - - - - - {% for key, stat in stats.items %} - - - - - {% endfor %} +

{% blocktrans %}Statistics of the set {{ key }}{% endblocktrans %}

+ + + + + + + {% for key, stat in stats.items %} + + + + + {% endfor %}
Type d'objetNombre d'entrée stockées
{{ stat.0 }}{{ stat.1 }}
{% trans "Object type" %}{% trans "Number of stored entries" %}
{{ stat.0 }}{{ stat.1 }}
- {% endfor %} +{% endfor %} + diff --git a/logs/templates/logs/aff_stats_users.html b/logs/templates/logs/aff_stats_users.html index f5b21c7e..0ea6a426 100644 --- a/logs/templates/logs/aff_stats_users.html +++ b/logs/templates/logs/aff_stats_users.html @@ -22,24 +22,27 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} - {% for key_dict, stats_dict in stats_list.items %} +{% load i18n %} + +{% for key_dict, stats_dict in stats_list.items %} {% for key, stats in stats_dict.items %} - - -

Statistiques par {{ key_dict }} de {{ key }}

- - - - - - - {% for stat in stats %} - - - - - - {% endfor %} -
{{ key_dict }}Nombre de {{ key }} par {{ key_dict }}Rang
{{ stat|truncatechars:25 }}{{ stat.num }}{{ forloop.counter }}
- {% endfor %} - {% endfor %} + + +

{% blocktrans %}Statistics per {{ key_dict }} of {{ key }}{% endblocktrans %}

+ + + + + + + {% for stat in stats %} + + + + + + {% endfor %} +
{{ key_dict }}{% blocktrans %}Number of {{ key }} per {{ key_dict }}{% endblocktrans %}{% trans "Rank" %}
{{ stat|truncatechars:25 }}{{ stat.num }}{{ forloop.counter }}
+ {% endfor %} +{% endfor %} + diff --git a/logs/templates/logs/aff_summary.html b/logs/templates/logs/aff_summary.html index f743d637..366e07e7 100644 --- a/logs/templates/logs/aff_summary.html +++ b/logs/templates/logs/aff_summary.html @@ -28,122 +28,132 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load logs_extra %} {% load acl %} - - - - - - - - + +{% load i18n %} + +
{% include "buttons/sort.html" with prefix='sum' col='date' text='Date' %}Modification
+ + + {% trans "Date" as tr_date %} + + + + + {% for v in versions_list %} {% if v.version.content_type.model == 'ban' %} - {% can_edit_history %} + {% can_edit_history %} - {% acl_end %} + {% acl_end %} {% elif v.version.content_type.model == 'whitelist' %} - {% can_edit_history%} + {% can_edit_history%} - {% acl_end %} + {% acl_end %} {% elif v.version.content_type.model == 'user' %} - {% can_edit_history %} + {% can_edit_history %} - {% acl_end %} + {% acl_end %} {% elif v.version.content_type.model == 'vente' %} - {% can_edit_history %} + {% can_edit_history %} - {% acl_end %} + {% acl_end %} {% elif v.version.content_type.model == 'interface' %} - {% can_edit_history %} + {% can_edit_history %} - {% acl_end %} + {% acl_end %} {% endif %} {% endfor %} -
{% include "buttons/sort.html" with prefix='sum' col='date' text=tr_date %}{% trans "Editing" %}
{{ v.datetime }} - {{ v.username }} a banni + {% blocktrans with username=v.username %}{{ username }} has banned{% endblocktrans %} {{ v.version.object.user.get_username }} - ( + ( {% if v.version.object.raison == '' %} - Aucune raison + {% trans "No reason" %} {% else %} {{ v.version.object.raison }} {% endif %} ) - Annuler + {% trans "Cancel" %}
{{ v.datetime }} - {{ v.username }} a autorisé gracieusement + {% blocktrans with username=v.username %}{{ username }} has graciously authorised{% endblocktrans %} {{ v.version.object.user.get_username }} ( {% if v.version.object.raison == '' %} - Aucune raison + {% trans "No reason" %} {% else %} {{ v.version.object.raison }} {% endif %} ) - Annuler + {% trans "Cancel" %}
{{ v.datetime }} - {{ v.username }} a mis à jour + {% blocktrans with username=v.username %}{{ username }} has updated{% endblocktrans %} {{ v.version.object.get_username }} - {% if v.comment != '' %} - ({{ v.comment }}) - {% endif %} + {% if v.comment != '' %} + ({{ v.comment }}) + {% endif %} - Annuler + {% trans "Cancel" %}
{{ v.datetime }} - {{ v.username }} a vendu {{ v.version.object.number }}x {{ v.version.object.name }} à - {{ v.version.object.facture.user.get_username }} - {% if v.version.object.iscotisation %} - (+{{ v.version.object.duration }} mois) - {% endif %} + {% blocktrans with username=v.username number=v.version.object.number name=v.version.object.name %}{{ username }} has sold {{ number }}x {{ name }}{% endblocktrans %} + {% with invoice=v.version.object.facture %} + {% if invoice|is_facture %} + {% trans " to" %} + {{ v.version.object.facture.facture.user.get_username }} + {% if v.version.object.iscotisation %} + ({% blocktrans with duration=v.version.object.duration %}+{{ duration }} months{% endblocktrans %}) + {% endif %} + {% endif %} + {% endwith %} - Annuler + {% trans "Cancel" %}
{{ v.datetime }} - {{ v.username }} a modifié une interface de + {% blocktrans with username=v.username %}{{ username }} has edited an interface of{% endblocktrans %} {{ v.version.object.machine.user.get_username }} - {% if v.comment != '' %} - ({{ v.comment }}) - {% endif %} + {% if v.comment != '' %} + ({{ v.comment }}) + {% endif %} - Annuler + {% trans "Cancel" %}
+ {% if versions_list.paginator %} {% include "pagination.html" with list=versions_list %} {% endif %} + diff --git a/logs/templates/logs/delete.html b/logs/templates/logs/delete.html index 8bda7cb6..6ad11195 100644 --- a/logs/templates/logs/delete.html +++ b/logs/templates/logs/delete.html @@ -24,17 +24,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Supression d'action{% endblock %} +{% block title %}{% trans "Deletion of actions" %}{% endblock %} {% block content %}
{% csrf_token %} -

Attention, voulez-vous vraiment annuler cette action {{ objet_name }} ( {{ objet }} ) ?

- {% bootstrap_button "Confirmer" button_type="submit" icon="trash" %} +

{% blocktrans %}Warning: are you sure you want to delete this action {{ objet_name }} ( {{ objet }} )?{% endblocktrans %}

+ {% trans "Confirm" as tr_confirm %} + {% bootstrap_button tr_confirm button_type="submit" icon="trash" %}
-
-
-
+
+
+
{% endblock %} + diff --git a/logs/templates/logs/index.html b/logs/templates/logs/index.html index a120a531..dde47c7d 100644 --- a/logs/templates/logs/index.html +++ b/logs/templates/logs/index.html @@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Statistiques{% endblock %} +{% block title %}{%trans "Statistics" %}{% endblock %} {% block content %} -

Actions effectuées

- {% include "logs/aff_summary.html" with versions_list=versions_list %} -
-
-
- {% endblock %} +

{% trans "Actions performed" %}

+ {% include "logs/aff_summary.html" with versions_list=versions_list %} +
+
+
+{% endblock %} + diff --git a/logs/templates/logs/sidebar.html b/logs/templates/logs/sidebar.html index 0e3048e3..87011cfc 100644 --- a/logs/templates/logs/sidebar.html +++ b/logs/templates/logs/sidebar.html @@ -24,32 +24,34 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load i18n %} {% block sidebar %} {% can_view_app logs %} - Résumé + {% trans "Summary" %} - Évènements + {% trans "Events" %} - Général + {% trans "General" %} - Base de données + {% trans "Database" %} - Actions de cablage + {% trans "Wiring actions" %} - Utilisateurs + {% trans "Users" %} {% acl_end %} {% endblock %} + diff --git a/logs/templates/logs/stats_general.html b/logs/templates/logs/stats_general.html index b8590df1..07e3ec26 100644 --- a/logs/templates/logs/stats_general.html +++ b/logs/templates/logs/stats_general.html @@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Statistiques générales{% endblock %} +{% block title %}{% trans "Statistics" %}{% endblock %} {% block content %} -

Statistiques générales

- {% include "logs/aff_stats_general.html" with stats_list=stats_list %} -
-
-
- {% endblock %} +

{% trans "General statistics" %}

+ {% include "logs/aff_stats_general.html" with stats_list=stats_list %} +
+
+
+{% endblock %} + diff --git a/logs/templates/logs/stats_logs.html b/logs/templates/logs/stats_logs.html index 4db77c68..4f547cc3 100644 --- a/logs/templates/logs/stats_logs.html +++ b/logs/templates/logs/stats_logs.html @@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Statistiques{% endblock %} +{% block title %}{% trans "Statistics" %}{% endblock %} {% block content %} -

Actions effectuées

- {% include "logs/aff_stats_logs.html" with revisions_list=revisions_list %} -
-
-
- {% endblock %} +

{% trans "Actions performed" %}

+ {% include "logs/aff_stats_logs.html" with revisions_list=revisions_list %} +
+
+
+{% endblock %} + diff --git a/logs/templates/logs/stats_models.html b/logs/templates/logs/stats_models.html index 0ed28525..9b912da2 100644 --- a/logs/templates/logs/stats_models.html +++ b/logs/templates/logs/stats_models.html @@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Statistiques des objets base de données{% endblock %} +{% block title %}{% trans "Statistics" %}{% endblock %} {% block content %} -

Statistiques bdd

- {% include "logs/aff_stats_models.html" with stats_list=stats_list %} -
-
-
- {% endblock %} +

{% trans "Database statistics" %}

+ {% include "logs/aff_stats_models.html" with stats_list=stats_list %} +
+
+
+{% endblock %} + diff --git a/logs/templates/logs/stats_users.html b/logs/templates/logs/stats_users.html index fa0843ec..8cc645ab 100644 --- a/logs/templates/logs/stats_users.html +++ b/logs/templates/logs/stats_users.html @@ -24,13 +24,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Statistiques par utilisateur{% endblock %} +{% block title %}{% trans "Statistics" %}{% endblock %} {% block content %} -

Statistiques par utilisateur

- {% include "logs/aff_stats_users.html" with stats_list=stats_list %} -
-
-
- {% endblock %} +

{% trans "Statistics about users" %}

+ {% include "logs/aff_stats_users.html" with stats_list=stats_list %} +
+
+
+{% endblock %} + diff --git a/logs/templatetags/logs_extra.py b/logs/templatetags/logs_extra.py index 0620f8f4..813a577f 100644 --- a/logs/templatetags/logs_extra.py +++ b/logs/templatetags/logs_extra.py @@ -19,7 +19,7 @@ # # 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. +# 51 Franklin Street, Fifth Floor, Boston, MA 021}10-1301 USA. """logs.templatetags.logs_extra A templatetag to get the class name for a given object """ @@ -34,6 +34,10 @@ def classname(obj): """ Returns the object class name """ return obj.__class__.__name__ +@register.filter +def is_facture(baseinvoice): + """Returns True if a baseinvoice has a `Facture` child.""" + return hasattr(baseinvoice, 'facture') @register.inclusion_tag('buttons/history.html') def history_button(instance, text=False, html_class=True): diff --git a/logs/views.py b/logs/views.py index d7ba59f3..a9fe5418 100644 --- a/logs/views.py +++ b/logs/views.py @@ -188,10 +188,10 @@ def revert_action(request, revision_id): try: revision = Revision.objects.get(id=revision_id) except Revision.DoesNotExist: - messages.error(request, u"Revision inexistante") + messages.error(request, _("Nonexistent revision.")) if request.method == "POST": revision.revert() - messages.success(request, "L'action a été supprimée") + messages.success(request, _("The action was deleted.")) return redirect(reverse('logs:index')) return form({ 'objet': revision, @@ -224,14 +224,14 @@ def stats_general(request): stats = [ [ # First set of data (about users) [ # Headers - "Categorie", - "Nombre d'utilisateurs (total club et adhérents)", - "Nombre d'adhérents", - "Nombre de clubs" + _("Category"), + _("Number of users (members and clubs)"), + _("Number of members"), + _("Number of clubs") ], { # Data 'active_users': [ - "Users actifs", + _("Activated users"), User.objects.filter(state=User.STATE_ACTIVE).count(), (Adherent.objects .filter(state=Adherent.STATE_ACTIVE) @@ -239,7 +239,7 @@ def stats_general(request): Club.objects.filter(state=Club.STATE_ACTIVE).count() ], 'inactive_users': [ - "Users désactivés", + _("Disabled users"), User.objects.filter(state=User.STATE_DISABLED).count(), (Adherent.objects .filter(state=Adherent.STATE_DISABLED) @@ -247,7 +247,7 @@ def stats_general(request): Club.objects.filter(state=Club.STATE_DISABLED).count() ], 'archive_users': [ - "Users archivés", + _("Archived users"), User.objects.filter(state=User.STATE_ARCHIVE).count(), (Adherent.objects .filter(state=Adherent.STATE_ARCHIVE) @@ -255,31 +255,31 @@ def stats_general(request): Club.objects.filter(state=Club.STATE_ARCHIVE).count() ], 'adherent_users': [ - "Cotisant à l'association", + _("Contributing members"), _all_adherent.count(), _all_adherent.exclude(adherent__isnull=True).count(), _all_adherent.exclude(club__isnull=True).count() ], 'connexion_users': [ - "Utilisateurs bénéficiant d'une connexion", + _("Users benefiting from a connection"), _all_has_access.count(), _all_has_access.exclude(adherent__isnull=True).count(), _all_has_access.exclude(club__isnull=True).count() ], 'ban_users': [ - "Utilisateurs bannis", + _("Banned users"), _all_baned.count(), _all_baned.exclude(adherent__isnull=True).count(), _all_baned.exclude(club__isnull=True).count() ], 'whitelisted_user': [ - "Utilisateurs bénéficiant d'une connexion gracieuse", + _("Users benefiting from a free connection"), _all_whitelisted.count(), _all_whitelisted.exclude(adherent__isnull=True).count(), _all_whitelisted.exclude(club__isnull=True).count() ], 'actives_interfaces': [ - "Interfaces actives (ayant accès au reseau)", + _("Active interfaces (with access to the network)"), _all_active_interfaces_count.count(), (_all_active_interfaces_count .exclude(machine__user__adherent__isnull=True) @@ -289,7 +289,7 @@ def stats_general(request): .count()) ], 'actives_assigned_interfaces': [ - "Interfaces actives et assignées ipv4", + _("Active interfaces assigned IPv4"), _all_active_assigned_interfaces_count.count(), (_all_active_assigned_interfaces_count .exclude(machine__user__adherent__isnull=True) @@ -302,12 +302,12 @@ def stats_general(request): ], [ # Second set of data (about ip adresses) [ # Headers - "Range d'ip", - "Vlan", - "Nombre d'ip totales", - "Ip assignées", - "Ip assignées à une machine active", - "Ip non assignées" + _("IP range"), + _("VLAN"), + _("Total number of IP addresses"), + _("Number of assigned IP addresses"), + _("Number of IP address assigned to an activated machine"), + _("Number of nonassigned IP addresses") ], ip_dict # Data already prepared ] @@ -322,79 +322,87 @@ def stats_models(request): nombre d'users, d'écoles, de droits, de bannissements, de factures, de ventes, de banque, de machines, etc""" stats = { - 'Users': { - 'users': [User.PRETTY_NAME, User.objects.count()], - 'adherents': [Adherent.PRETTY_NAME, Adherent.objects.count()], - 'clubs': [Club.PRETTY_NAME, Club.objects.count()], - 'serviceuser': [ServiceUser.PRETTY_NAME, + _("Users"): { + 'users': [User._meta.verbose_name, User.objects.count()], + 'adherents': [Adherent._meta.verbose_name, Adherent.objects.count()], + 'clubs': [Club._meta.verbose_name, Club.objects.count()], + 'serviceuser': [ServiceUser._meta.verbose_name, ServiceUser.objects.count()], - 'school': [School.PRETTY_NAME, School.objects.count()], - 'listright': [ListRight.PRETTY_NAME, ListRight.objects.count()], - 'listshell': [ListShell.PRETTY_NAME, ListShell.objects.count()], - 'ban': [Ban.PRETTY_NAME, Ban.objects.count()], - 'whitelist': [Whitelist.PRETTY_NAME, Whitelist.objects.count()] + 'school': [School._meta.verbose_name, School.objects.count()], + 'listright': [ListRight._meta.verbose_name, ListRight.objects.count()], + 'listshell': [ListShell._meta.verbose_name, ListShell.objects.count()], + 'ban': [Ban._meta.verbose_name, Ban.objects.count()], + 'whitelist': [Whitelist._meta.verbose_name, Whitelist.objects.count()] }, - 'Cotisations': { + _("Subscriptions"): { 'factures': [ - Facture._meta.verbose_name.title(), + Facture._meta.verbose_name, Facture.objects.count() ], 'vente': [ - Vente._meta.verbose_name.title(), + Vente._meta.verbose_name, Vente.objects.count() ], 'cotisation': [ - Cotisation._meta.verbose_name.title(), + Cotisation._meta.verbose_name, Cotisation.objects.count() ], 'article': [ - Article._meta.verbose_name.title(), + Article._meta.verbose_name, Article.objects.count() ], 'banque': [ - Banque._meta.verbose_name.title(), + Banque._meta.verbose_name, Banque.objects.count() ], }, - 'Machines': { - 'machine': [Machine.PRETTY_NAME, Machine.objects.count()], - 'typemachine': [MachineType.PRETTY_NAME, + _("Machines"): { + 'machine': [Machine._meta.verbose_name, + Machine.objects.count()], + 'typemachine': [MachineType._meta.verbose_name, MachineType.objects.count()], - 'typeip': [IpType.PRETTY_NAME, IpType.objects.count()], - 'extension': [Extension.PRETTY_NAME, Extension.objects.count()], - 'interface': [Interface.PRETTY_NAME, Interface.objects.count()], - 'alias': [Domain.PRETTY_NAME, + 'typeip': [IpType._meta.verbose_name, + IpType.objects.count()], + 'extension': [Extension._meta.verbose_name, + Extension.objects.count()], + 'interface': [Interface._meta.verbose_name, + Interface.objects.count()], + 'alias': [Domain._meta.verbose_name, Domain.objects.exclude(cname=None).count()], - 'iplist': [IpList.PRETTY_NAME, IpList.objects.count()], - 'service': [Service.PRETTY_NAME, Service.objects.count()], + 'iplist': [IpList._meta.verbose_name, + IpList.objects.count()], + 'service': [Service._meta.verbose_name, + Service.objects.count()], 'ouvertureportlist': [ - OuverturePortList.PRETTY_NAME, + OuverturePortList._meta.verbose_name, OuverturePortList.objects.count() ], - 'vlan': [Vlan.PRETTY_NAME, Vlan.objects.count()], - 'SOA': [SOA.PRETTY_NAME, SOA.objects.count()], - 'Mx': [Mx.PRETTY_NAME, Mx.objects.count()], - 'Ns': [Ns.PRETTY_NAME, Ns.objects.count()], - 'nas': [Nas.PRETTY_NAME, Nas.objects.count()], + 'vlan': [Vlan._meta.verbose_name, Vlan.objects.count()], + 'SOA': [SOA._meta.verbose_name, SOA.objects.count()], + 'Mx': [Mx._meta.verbose_name, Mx.objects.count()], + 'Ns': [Ns._meta.verbose_name, Ns.objects.count()], + 'nas': [Nas._meta.verbose_name, Nas.objects.count()], }, - 'Topologie': { - 'switch': [Switch.PRETTY_NAME, Switch.objects.count()], - 'bornes': [AccessPoint.PRETTY_NAME, AccessPoint.objects.count()], - 'port': [Port.PRETTY_NAME, Port.objects.count()], - 'chambre': [Room.PRETTY_NAME, Room.objects.count()], - 'stack': [Stack.PRETTY_NAME, Stack.objects.count()], + _("Topology"): { + 'switch': [Switch._meta.verbose_name, + Switch.objects.count()], + 'bornes': [AccessPoint._meta.verbose_name, + AccessPoint.objects.count()], + 'port': [Port._meta.verbose_name, Port.objects.count()], + 'chambre': [Room._meta.verbose_name, Room.objects.count()], + 'stack': [Stack._meta.verbose_name, Stack.objects.count()], 'modelswitch': [ - ModelSwitch.PRETTY_NAME, + ModelSwitch._meta.verbose_name, ModelSwitch.objects.count() ], 'constructorswitch': [ - ConstructorSwitch.PRETTY_NAME, + ConstructorSwitch._meta.verbose_name, ConstructorSwitch.objects.count() ], }, - 'Actions effectuées sur la base': + _("Actions performed"): { - 'revision': ["Nombre d'actions", Revision.objects.count()], + 'revision': [_("Number of actions"), Revision.objects.count()], }, } return render(request, 'logs/stats_models.html', {'stats_list': stats}) @@ -408,35 +416,35 @@ def stats_users(request): de moyens de paiements par user, de banque par user, de bannissement par user, etc""" stats = { - 'Utilisateur': { - 'Machines': User.objects.annotate( + _("User"): { + _("Machines"): User.objects.annotate( num=Count('machine') ).order_by('-num')[:10], - 'Facture': User.objects.annotate( + _("Invoice"): User.objects.annotate( num=Count('facture') ).order_by('-num')[:10], - 'Bannissement': User.objects.annotate( + _("Ban"): User.objects.annotate( num=Count('ban') ).order_by('-num')[:10], - 'Accès gracieux': User.objects.annotate( + _("Whitelist"): User.objects.annotate( num=Count('whitelist') ).order_by('-num')[:10], - 'Droits': User.objects.annotate( + _("Rights"): User.objects.annotate( num=Count('groups') ).order_by('-num')[:10], }, - 'Etablissement': { - 'Utilisateur': School.objects.annotate( + _("School"): { + _("User"): School.objects.annotate( num=Count('user') ).order_by('-num')[:10], }, - 'Moyen de paiement': { - 'Utilisateur': Paiement.objects.annotate( + _("Payment method"): { + _("User"): Paiement.objects.annotate( num=Count('facture') ).order_by('-num')[:10], }, - 'Banque': { - 'Utilisateur': Banque.objects.annotate( + _("Bank"): { + _("User"): Banque.objects.annotate( num=Count('facture') ).order_by('-num')[:10], }, @@ -451,8 +459,8 @@ def stats_actions(request): utilisateurs. Affiche le nombre de modifications aggrégées par utilisateurs""" stats = { - 'Utilisateur': { - 'Action': User.objects.annotate( + _("User"): { + _("Action"): User.objects.annotate( num=Count('revision') ).order_by('-num')[:40], }, @@ -489,14 +497,14 @@ def history(request, application, object_name, object_id): try: instance = model.get_instance(**kwargs) except model.DoesNotExist: - messages.error(request, _("No entry found.")) + messages.error(request, _("Nonexistent entry.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} )) can, msg = instance.can_view(request.user) if not can: - messages.error(request, msg or _("You cannot acces to this menu")) + messages.error(request, msg or _("You don't have the right to access this menu.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} @@ -513,3 +521,4 @@ def history(request, application, object_name, object_id): 're2o/history.html', {'reversions': reversions, 'object': instance} ) + diff --git a/machines/acl.py b/machines/acl.py index 1b74760c..45cb6ec2 100644 --- a/machines/acl.py +++ b/machines/acl.py @@ -25,6 +25,7 @@ Here are defined some functions to check acl on the application. """ +from django.utils.translation import ugettext as _ def can_view(user): @@ -38,4 +39,6 @@ def can_view(user): viewing is granted and msg is a message (can be None). """ can = user.has_module_perms('machines') - return can, None if can else "Vous ne pouvez pas voir cette application." + return can, None if can else _("You don't have the right to view this" + " application.") + diff --git a/machines/admin.py b/machines/admin.py index 26d7a6a3..af721ff9 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -42,6 +42,7 @@ from .models import ( SshFp, Nas, Service, + Role, OuverturePort, Ipv6List, OuverturePortList, @@ -146,6 +147,11 @@ class ServiceAdmin(VersionAdmin): """ Admin view of a ServiceAdmin object """ list_display = ('service_type', 'min_time_regen', 'regular_time_regen') +class RoleAdmin(VersionAdmin): + """ Admin view of a RoleAdmin object """ + pass + + admin.site.register(Machine, MachineAdmin) admin.site.register(MachineType, MachineTypeAdmin) @@ -162,6 +168,7 @@ admin.site.register(IpList, IpListAdmin) admin.site.register(Interface, InterfaceAdmin) admin.site.register(Domain, DomainAdmin) admin.site.register(Service, ServiceAdmin) +admin.site.register(Role, RoleAdmin) admin.site.register(Vlan, VlanAdmin) admin.site.register(Ipv6List, Ipv6ListAdmin) admin.site.register(Nas, NasAdmin) diff --git a/machines/forms.py b/machines/forms.py index 23c2aa39..4af060d3 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -37,6 +37,7 @@ from __future__ import unicode_literals from django.forms import ModelForm, Form from django import forms +from django.utils.translation import ugettext_lazy as _ from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin @@ -53,6 +54,7 @@ from .models import ( Txt, DName, Ns, + Role, Service, Vlan, Srv, @@ -73,7 +75,7 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(EditMachineForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['name'].label = 'Nom de la machine' + self.fields['name'].label = _("Machine name") class NewMachineForm(EditMachineForm): @@ -92,12 +94,11 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) user = kwargs.get('user') super(EditInterfaceForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['mac_address'].label = 'Adresse mac' - self.fields['type'].label = 'Type de machine' - self.fields['type'].empty_label = "Séléctionner un type de machine" + self.fields['mac_address'].label = _("MAC address") + self.fields['type'].label = _("Machine type") + self.fields['type'].empty_label = _("Select a machine type") if "ipv4" in self.fields: - self.fields['ipv4'].empty_label = ("Assignation automatique de " - "l'ipv4") + self.fields['ipv4'].empty_label = _("Automatic IPv4 assignment") self.fields['ipv4'].queryset = IpList.objects.filter( interface__isnull=True ) @@ -168,7 +169,7 @@ class DelAliasForm(FormRevMixin, Form): """Suppression d'un ou plusieurs objets alias""" alias = forms.ModelMultipleChoiceField( queryset=Domain.objects.all(), - label="Alias actuels", + label=_("Current aliases"), widget=forms.CheckboxSelectMultiple ) @@ -189,15 +190,15 @@ class MachineTypeForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(MachineTypeForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['type'].label = 'Type de machine à ajouter' - self.fields['ip_type'].label = "Type d'ip relié" + self.fields['type'].label = _("Machine type to add") + self.fields['ip_type'].label = _("Related IP type") class DelMachineTypeForm(FormRevMixin, Form): """Suppression d'un ou plusieurs machinetype""" machinetypes = forms.ModelMultipleChoiceField( queryset=MachineType.objects.none(), - label="Types de machines actuelles", + label=_("Current machine types"), widget=forms.CheckboxSelectMultiple ) @@ -215,20 +216,21 @@ class IpTypeForm(FormRevMixin, ModelForm): stop après creation""" class Meta: model = IpType - fields = ['type', 'extension', 'need_infra', 'domaine_ip_start', - 'domaine_ip_stop', 'prefix_v6', 'vlan', 'ouverture_ports'] + fields = '__all__' def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(IpTypeForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['type'].label = 'Type ip à ajouter' + self.fields['type'].label = _("IP type to add") class EditIpTypeForm(IpTypeForm): """Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait synchroniser les objets iplist""" class Meta(IpTypeForm.Meta): - fields = ['extension', 'type', 'need_infra', 'prefix_v6', 'vlan', + fields = ['extension', 'type', 'need_infra', 'domaine_ip_network', 'domaine_ip_netmask', + 'prefix_v6', 'prefix_v6_length', + 'vlan', 'reverse_v4', 'reverse_v6', 'ouverture_ports'] @@ -236,7 +238,7 @@ class DelIpTypeForm(FormRevMixin, Form): """Suppression d'un ou plusieurs iptype""" iptypes = forms.ModelMultipleChoiceField( queryset=IpType.objects.none(), - label="Types d'ip actuelles", + label=_("Current IP types"), widget=forms.CheckboxSelectMultiple ) @@ -258,17 +260,17 @@ class ExtensionForm(FormRevMixin, ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ExtensionForm, self).__init__(*args, prefix=prefix, **kwargs) - self.fields['name'].label = 'Extension à ajouter' - self.fields['origin'].label = 'Enregistrement A origin' - self.fields['origin_v6'].label = 'Enregistrement AAAA origin' - self.fields['soa'].label = 'En-tête SOA à utiliser' + self.fields['name'].label = _("Extension to add") + self.fields['origin'].label = _("A record origin") + self.fields['origin_v6'].label = _("AAAA record origin") + self.fields['soa'].label = _("SOA record to use") class DelExtensionForm(FormRevMixin, Form): """Suppression d'une ou plusieurs extensions""" extensions = forms.ModelMultipleChoiceField( queryset=Extension.objects.none(), - label="Extensions actuelles", + label=_("Current extensions"), widget=forms.CheckboxSelectMultiple ) @@ -307,7 +309,7 @@ class DelSOAForm(FormRevMixin, Form): """Suppression d'un ou plusieurs SOA""" soa = forms.ModelMultipleChoiceField( queryset=SOA.objects.none(), - label="SOA actuels", + label=_("Current SOA records"), widget=forms.CheckboxSelectMultiple ) @@ -338,7 +340,7 @@ class DelMxForm(FormRevMixin, Form): """Suppression d'un ou plusieurs MX""" mx = forms.ModelMultipleChoiceField( queryset=Mx.objects.none(), - label="MX actuels", + label=_("Current MX records"), widget=forms.CheckboxSelectMultiple ) @@ -371,7 +373,7 @@ class DelNsForm(FormRevMixin, Form): """Suppresion d'un ou plusieurs NS""" ns = forms.ModelMultipleChoiceField( queryset=Ns.objects.none(), - label="Enregistrements NS actuels", + label=_("Current NS records"), widget=forms.CheckboxSelectMultiple ) @@ -399,7 +401,7 @@ class DelTxtForm(FormRevMixin, Form): """Suppression d'un ou plusieurs TXT""" txt = forms.ModelMultipleChoiceField( queryset=Txt.objects.none(), - label="Enregistrements Txt actuels", + label=_("Current TXT records"), widget=forms.CheckboxSelectMultiple ) @@ -427,7 +429,7 @@ class DelDNameForm(FormRevMixin, Form): """Delete a set of DNAME entries""" dnames = forms.ModelMultipleChoiceField( queryset=Txt.objects.none(), - label="Existing DNAME entries", + label=_("Current DNAME records"), widget=forms.CheckboxSelectMultiple ) @@ -455,7 +457,7 @@ class DelSrvForm(FormRevMixin, Form): """Suppression d'un ou plusieurs Srv""" srv = forms.ModelMultipleChoiceField( queryset=Srv.objects.none(), - label="Enregistrements Srv actuels", + label=_("Current SRV records"), widget=forms.CheckboxSelectMultiple ) @@ -484,7 +486,7 @@ class DelNasForm(FormRevMixin, Form): """Suppression d'un ou plusieurs nas""" nas = forms.ModelMultipleChoiceField( queryset=Nas.objects.none(), - label="Enregistrements Nas actuels", + label=_("Current NAS devices"), widget=forms.CheckboxSelectMultiple ) @@ -497,6 +499,38 @@ class DelNasForm(FormRevMixin, Form): self.fields['nas'].queryset = Nas.objects.all() +class RoleForm(FormRevMixin, ModelForm): + """Add and edit role.""" + class Meta: + model = Role + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(RoleForm, self).__init__(*args, prefix=prefix, **kwargs) + self.fields['servers'].queryset = (Interface.objects.all() + .select_related( + 'domain__extension' + )) + + +class DelRoleForm(FormRevMixin, Form): + """Deletion of one or several roles.""" + role = forms.ModelMultipleChoiceField( + queryset=Role.objects.none(), + label=_("Current roles"), + widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelRoleForm, self).__init__(*args, **kwargs) + if instances: + self.fields['role'].queryset = instances + else: + self.fields['role'].queryset = Role.objects.all() + + class ServiceForm(FormRevMixin, ModelForm): """Ajout et edition d'une classe de service : dns, dhcp, etc""" class Meta: @@ -525,7 +559,7 @@ class DelServiceForm(FormRevMixin, Form): """Suppression d'un ou plusieurs service""" service = forms.ModelMultipleChoiceField( queryset=Service.objects.none(), - label="Services actuels", + label=_("Current services"), widget=forms.CheckboxSelectMultiple ) @@ -553,7 +587,7 @@ class DelVlanForm(FormRevMixin, Form): """Suppression d'un ou plusieurs vlans""" vlan = forms.ModelMultipleChoiceField( queryset=Vlan.objects.none(), - label="Vlan actuels", + label=_("Current VLANs"), widget=forms.CheckboxSelectMultiple ) @@ -611,3 +645,4 @@ class SshFpForm(FormRevMixin, ModelForm): prefix=prefix, **kwargs ) + diff --git a/machines/locale/fr/LC_MESSAGES/django.mo b/machines/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 00000000..c9696d92 Binary files /dev/null and b/machines/locale/fr/LC_MESSAGES/django.mo differ diff --git a/machines/locale/fr/LC_MESSAGES/django.po b/machines/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..50ab03a8 --- /dev/null +++ b/machines/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,1748 @@ +# 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 © 2018 Maël Kervella +# +# 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. +msgid "" +msgstr "" +"Project-Id-Version: 2.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-08-15 18:10+0200\n" +"PO-Revision-Date: 2018-06-23 16:35+0200\n" +"Last-Translator: Laouen Fernet \n" +"Language-Team: \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: acl.py:42 +msgid "You don't have the right to view this application." +msgstr "Vous n'avez pas le droit de voir cette application." + +#: forms.py:78 +msgid "Machine name" +msgstr "Nom de la machine" + +#: forms.py:97 templates/machines/aff_machines.html:46 +msgid "MAC address" +msgstr "Adresse MAC" + +#: forms.py:98 templates/machines/aff_machinetype.html:32 +#: templates/machines/machine.html:112 +msgid "Machine type" +msgstr "Type de machine" + +#: forms.py:99 +msgid "Select a machine type" +msgstr "Sélectionnez un type de machine" + +#: forms.py:101 +msgid "Automatic IPv4 assignment" +msgstr "Assignation automatique IPv4" + +#: forms.py:172 +msgid "Current aliases" +msgstr "Alias actuels" + +#: forms.py:193 +msgid "Machine type to add" +msgstr "Type de machine à ajouter" + +#: forms.py:194 +msgid "Related IP type" +msgstr "Type d'IP relié" + +#: forms.py:201 +msgid "Current machine types" +msgstr "Types de machines actuels" + +#: forms.py:224 +msgid "IP type to add" +msgstr "Type d'IP à ajouter" + +#: forms.py:241 +msgid "Current IP types" +msgstr "Types d'IP actuels" + +#: forms.py:263 +msgid "Extension to add" +msgstr "Extension à ajouter" + +#: forms.py:264 templates/machines/aff_extension.html:37 +msgid "A record origin" +msgstr "Enregistrement A origin" + +#: forms.py:265 templates/machines/aff_extension.html:39 +msgid "AAAA record origin" +msgstr "Enregistrement AAAA origin" + +#: forms.py:266 +msgid "SOA record to use" +msgstr "Enregistrement SOA à utiliser" + +#: forms.py:273 +msgid "Current extensions" +msgstr "Extensions actuelles" + +#: forms.py:312 +msgid "Current SOA records" +msgstr "Enregistrements SOA actuels" + +#: forms.py:343 +msgid "Current MX records" +msgstr "Enregistrements MX actuels" + +#: forms.py:376 +msgid "Current NS records" +msgstr "Enregistrements NS actuels" + +#: forms.py:404 +msgid "Current TXT records" +msgstr "Enregistrements TXT actuels" + +#: forms.py:432 +msgid "Current DNAME records" +msgstr "Enregistrements DNAME actuels" + +#: forms.py:460 +msgid "Current SRV records" +msgstr "Enregistrements SRV actuels" + +#: forms.py:489 +msgid "Current NAS devices" +msgstr "Dispositifs NAS actuels" + +#: forms.py:521 +msgid "Current roles" +msgstr "Rôles actuels" + +#: forms.py:562 +msgid "Current services" +msgstr "Services actuels" + +#: forms.py:590 +msgid "Current VLANs" +msgstr "VLANs actuels" + +#: models.py:63 +msgid "Optional" +msgstr "Optionnel" + +#: models.py:71 +msgid "Can view a machine object" +msgstr "Peut voir un objet machine" + +#: models.py:73 +msgid "Can change the user of a machine" +msgstr "Peut changer l'utilisateur d'une machine" + +#: models.py:75 +msgid "machine" +msgstr "machine" + +#: models.py:76 +msgid "machines" +msgstr "machines" + +#: models.py:109 +msgid "You don't have the right to change the machine's user." +msgstr "Vous n'avez pas le droit de changer l'utilisateur de la machine." + +#: models.py:118 +msgid "You don't have the right to view all the machines." +msgstr "Vous n'avez pas le droit de voir toutes les machines." + +#: models.py:132 +msgid "Nonexistent user." +msgstr "Utilisateur inexistant." + +#: models.py:140 +msgid "You don't have the right to add a machine." +msgstr "Vous n'avez pas le droit d'ajouter une machine." + +#: models.py:142 +msgid "You don't have the right to add a machine to another user." +msgstr "Vous n'avez pas le droit d'ajouter une machine à un autre utilisateur." + +#: models.py:145 models.py:1152 +#, python-format +msgid "" +"You reached the maximum number of interfaces that you are allowed to create " +"yourself (%s)." +msgstr "" +"Vous avez atteint le nombre maximal d'interfaces que vous pouvez créer vous-" +"mêmes (%s)." + +#: models.py:164 models.py:1177 models.py:1194 models.py:1296 models.py:1313 +msgid "You don't have the right to edit a machine of another user." +msgstr "" +"Vous n'avez pas le droit de modifier une machine d'un autre utilisateur." + +#: models.py:182 +msgid "You don't have the right to delete a machine of another user." +msgstr "" +"Vous n'avez pas le droit de supprimer une machine d'une autre utilisateur." + +#: models.py:194 +msgid "You don't have the right to view other machines than yours." +msgstr "Vous n'avez pas le droit de voir d'autres machines que les vôtres." + +#: models.py:241 +msgid "Can view a machine type object" +msgstr "Peut voir un objet type de machine" + +#: models.py:242 +msgid "Can use all machine types" +msgstr "Peut utiliser tous les types de machine" + +#: models.py:244 +msgid "machine type" +msgstr "type de machine" + +#: models.py:245 +msgid "machine types" +msgstr "types de machine" + +#: models.py:263 +msgid "You don't have the right to use all machine types." +msgstr "Vous n'avez pas le droit d'utiliser tous les types de machine." + +#: models.py:282 +msgid "Network containing the domain's IPv4 range (optional)" +msgstr "Réseau contenant la plage IPv4 du domaine (optionnel)" + +#: models.py:290 +msgid "Netmask for the domain's IPv4 range" +msgstr "Masque de sous-réseau pour la plage IPv4 du domaine" + +#: models.py:294 +msgid "Enable reverse DNS for IPv4" +msgstr "Activer DNS inverse pour IPv4" + +#: models.py:310 +msgid "Enable reverse DNS for IPv6" +msgstr "Activer DNS inverser pour IPv6" + +#: models.py:326 +msgid "Can view an IP type object" +msgstr "Peut voir un objet type d'IP" + +#: models.py:327 +msgid "Can use all IP types" +msgstr "Peut utiliser tous les types d'IP" + +#: models.py:329 templates/machines/aff_iptype.html:35 +#: templates/machines/machine.html:108 +msgid "IP type" +msgstr "type d'IP" + +#: models.py:433 +msgid "" +"One or several IP addresses from the range are affected, impossible to " +"delete the range." +msgstr "" +"Une ou plusieurs adresses IP de la plage sont affectées, impossible de " +"supprimer la plage." + +#: models.py:475 +msgid "Range end must be after range start..." +msgstr "La fin de la plage doit être après le début..." + +#: models.py:478 +msgid "The range is too large, you can't create a larger one than a /16." +msgstr "" +"La plage est trop grande, vous ne pouvez pas en créer une plus grande " +"qu'un /16." + +#: models.py:483 +msgid "The specified range is not disjoint from existing ranges." +msgstr "La plage renseignée n'est pas disjointe des plages existantes." + +#: models.py:491 +msgid "" +"If you specify a domain network or netmask, it must contain the domain's IP " +"range." +msgstr "" +"Si vous renseignez un réseau ou masque de sous-réseau, il doit contenir" +" la plage IP du domaine." + +#: models.py:521 +msgid "Can view a VLAN object" +msgstr "Peut voir un objet VLAN" + +#: models.py:523 templates/machines/machine.html:160 +msgid "VLAN" +msgstr "VLAN" + +#: models.py:524 templates/machines/sidebar.html:57 +msgid "VLANs" +msgstr "VLANs" + +#: models.py:560 +msgid "Can view a NAS device object" +msgstr "Peut voir un objet dispositif NAS" + +#: models.py:562 templates/machines/machine.html:164 +msgid "NAS device" +msgstr "dispositif NAS" + +#: models.py:563 templates/machines/sidebar.html:63 +msgid "NAS devices" +msgstr "dispositifs NAS" + +#: models.py:577 +msgid "Contact email address for the zone" +msgstr "Adresse mail de contact pour la zone" + +#: models.py:581 +msgid "" +"Seconds before the secondary DNS have to ask the primary DNS serial to " +"detect a modification" +msgstr "" +"Secondes avant que le DNS secondaire demande au DNS primaire le serial pour " +"détecter une modification" + +#: models.py:586 +msgid "" +"Seconds before the secondary DNS ask the serial again in case of a primary " +"DNS timeout" +msgstr "" +"Secondes avant que le DNS secondaire demande le serial de nouveau dans le " +"cas d'un délai d'attente du DNS primaire" + +#: models.py:591 +msgid "" +"Seconds before the secondary DNS stop answering requests in case of primary " +"DNS timeout" +msgstr "" +"Secondes avant que le DNS secondaire arrête de répondre aux requêtes dans le " +"cas d'un délai d'attente du DNS primaire" + +#: models.py:596 models.py:846 +msgid "Time to Live" +msgstr "Temps de vie" + +#: models.py:601 +msgid "Can view an SOA record object" +msgstr "Peut voir un objet enregistrement SOA" + +#: models.py:603 templates/machines/aff_extension.html:36 +#: templates/machines/machine.html:120 +msgid "SOA record" +msgstr "enregistrement SOA" + +#: models.py:604 +msgid "SOA records" +msgstr "enregistrements SOA" + +#: models.py:643 +msgid "SOA to edit" +msgstr "SOA à modifier" + +#: models.py:654 +msgid "Zone name, must begin with a dot (.example.org)" +msgstr "Nom de zone, doit commencer par un point (.example.org)" + +#: models.py:662 +msgid "A record associated with the zone" +msgstr "Enregistrement A associé à la zone" + +#: models.py:668 +msgid "AAAA record associated with the zone" +msgstr "Enregristrement AAAA associé avec la zone" + +#: models.py:677 +msgid "Can view an extension object" +msgstr "Peut voir un objet extension" + +#: models.py:678 +msgid "Can use all extensions" +msgstr "Peut utiliser toutes les extensions" + +#: models.py:680 +msgid "DNS extension" +msgstr "extension DNS" + +#: models.py:681 +msgid "DNS extensions" +msgstr "extensions DNS" + +#: models.py:732 +msgid "An extension must begin with a dot." +msgstr "Une extension doit commencer par un point." + +#: models.py:746 +msgid "Can view an MX record object" +msgstr "Peut voir un objet enregistrement MX" + +#: models.py:748 templates/machines/machine.html:124 +msgid "MX record" +msgstr "enregistrement MX" + +#: models.py:749 +msgid "MX records" +msgstr "enregistrements MX" + +#: models.py:771 +msgid "Can view an NS record object" +msgstr "Peut voir un objet enregistrement NS" + +#: models.py:773 templates/machines/machine.html:128 +msgid "NS record" +msgstr "enregistrement NS" + +#: models.py:774 +msgid "NS records" +msgstr "enregistrements NS" + +#: models.py:793 +msgid "Can view a TXT record object" +msgstr "Peut voir un objet enregistrement TXT" + +#: models.py:795 templates/machines/machine.html:132 +msgid "TXT record" +msgstr "enregistrement TXT" + +#: models.py:796 +msgid "TXT records" +msgstr "enregistrements TXT" + +#: models.py:815 +msgid "Can view a DNAME record object" +msgstr "Peut voir un objet enregistrement DNAME" + +#: models.py:817 templates/machines/machine.html:136 +msgid "DNAME record" +msgstr "enregistrement DNAME" + +#: models.py:818 +msgid "DNAME records" +msgstr "enregistrements DNAME" + +#: models.py:851 +msgid "" +"Priority of the target server (positive integer value, the lower it is, the " +"more the server will be used if available)" +msgstr "" +"Priorité du serveur cible (entier positif, plus il est bas, plus le serveur " +"sera utilisé si disponible)" + +#: models.py:858 +msgid "" +"Relative weight for records with the same priority (integer value between 0 " +"and 65535)" +msgstr "" +"Poids relatif des enregistrements avec la même priorité (entier entre 0 et " +"65535)" + +#: models.py:863 +msgid "TCP/UDP port" +msgstr "Port TCP/UDP" + +#: models.py:868 +msgid "Target server" +msgstr "Serveur cible" + +#: models.py:873 +msgid "Can view an SRV record object" +msgstr "Peut voir un objet enregistrement SRV" + +#: models.py:875 templates/machines/machine.html:140 +msgid "SRV record" +msgstr "enregistrement SRV" + +#: models.py:876 +msgid "SRV records" +msgstr "enregistrements SRV" + +#: models.py:940 +msgid "Can view an SSHFP record object" +msgstr "Peut voir un objet enregistrement SSHFP" + +#: models.py:942 templates/machines/machine.html:144 +msgid "SSHFP record" +msgstr "enregistrement SSHFP" + +#: models.py:943 +msgid "SSHFP records" +msgstr "enregistrements SSHFP" + +#: models.py:981 +msgid "Can view an interface object" +msgstr "Peut voir un objet interface" + +#: models.py:983 +msgid "Can change the owner of an interface" +msgstr "Peut changer l'utilisateur d'une interface" + +#: models.py:985 +msgid "interface" +msgstr "interface" + +#: models.py:986 +msgid "interfaces" +msgstr "interfaces" + +#: models.py:1080 +msgid "The given MAC address is invalid." +msgstr "L'adresse MAC indiquée est invalide." + +#: models.py:1093 +msgid "The selected IP type is invalid." +msgstr "Le type d'IP sélectionné est invalide." + +#: models.py:1106 +msgid "There is no IP address available in the slash." +msgstr "Il n'y a pas d'adresse IP disponible dans le slash." + +#: models.py:1124 +msgid "The IPv4 address and the machine type don't match." +msgstr "L'adresse IPv4 et le type de machine ne correspondent pas." + +#: models.py:1138 +msgid "Nonexistent machine." +msgstr "Machine inexistante." + +#: models.py:1142 +msgid "You can't add a machine." +msgstr "Vous ne pouvez pas ajouter une machine." + +#: models.py:1148 +msgid "" +"You don't have the right to add an interface to a machine of another user." +msgstr "" +"Vous n'avez pas le droit d'ajouter une interface à une machine d'un autre " +"utilisateur." + +#: models.py:1162 +msgid "Permission required to edit the machine." +msgstr "Permission requise pour modifier la machine." + +#: models.py:1206 models.py:1325 models.py:1532 +msgid "You don't have the right to view machines other than yours." +msgstr "Vous n'avez pas le droit de voir d'autres machines que les vôtres." + +#: models.py:1252 +msgid "Can view an IPv6 addresses list object" +msgstr "Peut voir un objet list d'adresses IPv6" + +#: models.py:1253 +msgid "Can change the SLAAC value of an IPv6 addresses list" +msgstr "Peut modifier la valeur SLAAC d'une liste d'adresses IPv6" + +#: models.py:1256 +msgid "IPv6 addresses list" +msgstr "Liste d'adresses IPv6" + +#: models.py:1257 +msgid "IPv6 addresses lists" +msgstr "Listes d'adresses IPv6" + +#: models.py:1269 models.py:1480 +msgid "Nonexistent interface." +msgstr "Interface inexistante." + +#: models.py:1272 models.py:1487 +msgid "You don't have the right to add an alias to a machine of another user." +msgstr "" +"Vous n'avez pas le droit d'ajouter un alias à une machine d'un autre " +"utilisateur." + +#: models.py:1280 +msgid "Permission required to change the SLAAC value of an IPv6 address" +msgstr "Permission requise pour changer la valeur SLAAC d'une adresse IPv6." + +#: models.py:1352 +msgid "A SLAAC IP address is already registered." +msgstr "Une adresse IP SLAAC est déjà enregistrée." + +#: models.py:1357 +msgid "" +"The v6 prefix is incorrect and doesn't match the type associated with the " +"machine." +msgstr "" +"Le préfixe v6 est incorrect et ne correspond pas au type associé à la " +"machine." + +#: models.py:1383 +msgid "Mandatory and unique, must not contain dots." +msgstr "Obligatoire et unique, ne doit pas contenir de points." + +#: models.py:1397 +msgid "Can view a domain object" +msgstr "Peut voir un objet domaine" + +#: models.py:1399 +msgid "domain" +msgstr "domaine" + +#: models.py:1400 +msgid "domains" +msgstr "domaines" + +#: models.py:1422 +msgid "You can't create a both A and CNAME record." +msgstr "Vous ne pouvez pas créer un enregistrement à la fois A et CNAME." + +#: models.py:1425 +msgid "You can't create a CNAME record pointing to itself." +msgstr "Vous ne pouvez pas créer un enregistrement CNAME vers lui-même." + +#: models.py:1433 +#, python-format +msgid "The domain name %s is too long (over 63 characters)." +msgstr "Le nom de domaine %s est trop long (plus de 63 caractères)." + +#: models.py:1436 +#, python-format +msgid "The domain name %s contains forbidden characters." +msgstr "Le nom de domaine %s contient des caractères interdits." + +#: models.py:1454 +msgid "Invalid extension." +msgstr "Extension invalide." + +#: models.py:1495 +#, python-format +msgid "" +"You reached the maximum number of alias that you are allowed to create " +"yourself (%s). " +msgstr "" +"Vous avez atteint le nombre maximal d'alias que vous pouvez créer vous-mêmes " +"(%s)." + +#: models.py:1508 +msgid "You don't have the right to edit an alias of a machine of another user." +msgstr "" +"Vous n'avez pas le droit de modifier un alias d'une machine d'un autre " +"utilisateur." + +#: models.py:1520 +msgid "" +"You don't have the right to delete an alias of a machine of another user." +msgstr "" +"Vous n'avez pas le droit de supprimer un alias d'une machine d'un autre " +"utilisateur." + +#: models.py:1548 +msgid "Can view an IPv4 addresses list object" +msgstr "Peut voir un object liste d'adresses IPv4" + +#: models.py:1550 +msgid "IPv4 addresses list" +msgstr "Liste d'adresses IPv4" + +#: models.py:1551 +msgid "IPv4 addresses lists" +msgstr "Listes d'adresses IPv4" + +#: models.py:1562 +msgid "The IPv4 address and the range of the IP type don't match." +msgstr "L'adresse IPv4 et la plage du type d'IP ne correspondent pas." + +#: models.py:1580 +msgid "DHCP server" +msgstr "Serveur DHCP" + +#: models.py:1581 +msgid "Switches configuration server" +msgstr "Serveur de configuration des commutateurs réseau" + +#: models.py:1582 +msgid "Recursive DNS server" +msgstr "Serveur DNS récursif" + +#: models.py:1583 +msgid "NTP server" +msgstr "Serveur NTP" + +#: models.py:1584 +msgid "RADIUS server" +msgstr "Serveur RADIUS" + +#: models.py:1585 +msgid "Log server" +msgstr "Serveur log" + +#: models.py:1586 +msgid "LDAP master server" +msgstr "Serveur LDAP maître" + +#: models.py:1587 +msgid "LDAP backup server" +msgstr "Serveur LDAP de secours" + +#: models.py:1588 +msgid "SMTP server" +msgstr "Serveur SMTP" + +#: models.py:1589 +msgid "postgreSQL server" +msgstr "Serveur postgreSQL" + +#: models.py:1590 +msgid "mySQL server" +msgstr "Serveur mySQL" + +#: models.py:1591 +msgid "SQL client" +msgstr "Client SQL" + +#: models.py:1592 +msgid "Gateway" +msgstr "Passerelle" + +#: models.py:1606 +msgid "Can view a role object" +msgstr "Peut voir un objet rôle" + +#: models.py:1608 +msgid "server role" +msgstr "rôle de serveur" + +#: models.py:1609 +msgid "server roles" +msgstr "rôles de serveur" + +#: models.py:1650 +msgid "Minimal time before regeneration of the service." +msgstr "Temps minimal avant régénération du service." + +#: models.py:1654 +msgid "Maximal time before regeneration of the service." +msgstr "Temps maximal avant régénération du service." + +#: models.py:1660 +msgid "Can view a service object" +msgstr "Peut voir un objet service" + +#: models.py:1662 +msgid "service to generate (DHCP, DNS, ...)" +msgstr "service à générer (DHCP, DNS, ...)" + +#: models.py:1663 +msgid "services to generate (DHCP, DNS, ...)" +msgstr "services à générer (DHCP, DNS, ...)" + +#: models.py:1709 +msgid "Can view a service server link object" +msgstr "Peut voir un objet lien service serveur" + +#: models.py:1711 +msgid "link between service and server" +msgstr "lien entre service et serveur" + +#: models.py:1712 +msgid "links between service and server" +msgstr "liens entre service et serveur" + +#: models.py:1754 +msgid "Name of the ports configuration" +msgstr "Nom de la configuration de ports" + +#: models.py:1760 +msgid "Can view a ports opening list object" +msgstr "Peut voir un objet liste d'ouverture de ports" + +#: models.py:1763 +msgid "ports opening list" +msgstr "liste d'ouverture de ports" + +#: models.py:1764 +msgid "ports opening lists" +msgstr "listes d'ouverture de ports" + +#: models.py:1773 +msgid "You don't have the right to delete a ports opening list." +msgstr "Vous n'avez pas le droit de supprimer une liste d'ouverture de ports." + +#: models.py:1776 +msgid "This ports opening list is used." +msgstr "Cette liste d'ouverture de ports est utilisée." + +#: models.py:1849 +msgid "ports opening" +msgstr "ouverture de ports" + +#: models.py:1850 +msgid "ports openings" +msgstr "ouvertures de ports" + +#: templates/machines/aff_alias.html:32 +msgid "Aliases" +msgstr "Alias" + +#: templates/machines/aff_dname.html:30 +msgid "Target zone" +msgstr "Cible" + +#: templates/machines/aff_dname.html:31 templates/machines/aff_mx.html:34 +#: templates/machines/aff_txt.html:33 +msgid "Record" +msgstr "Enregistrement" + +#: templates/machines/aff_extension.html:34 +#: templates/machines/aff_iptype.html:36 templates/machines/aff_srv.html:34 +#: templates/machines/machine.html:116 +msgid "Extension" +msgstr "Extension" + +#: templates/machines/aff_extension.html:35 +#: templates/machines/aff_iptype.html:37 +msgid "'infra' right required" +msgstr "droit 'infra' requis" + +#: templates/machines/aff_iptype.html:38 +msgid "IPv4 range" +msgstr "Plage IPv4" + +#: templates/machines/aff_iptype.html:39 +msgid "v6 prefix" +msgstr "Préfixe v6" + +#: templates/machines/aff_iptype.html:40 +msgid "DNSSEC reverse v4/v6" +msgstr "DNSSEC inverse v4/v6" + +#: templates/machines/aff_iptype.html:41 +msgid "On VLAN(s)" +msgstr "Sur VLAN(s)" + +#: templates/machines/aff_iptype.html:42 +msgid "Default ports opening" +msgstr "Ouverture de ports par défaut" + +#: templates/machines/aff_ipv6.html:32 +msgid "IPv6 addresses" +msgstr "Adresses IPv6" + +#: templates/machines/aff_ipv6.html:33 +msgid "SLAAC" +msgstr "SLAAC" + +#: templates/machines/aff_machines.html:43 +msgid "DNS name" +msgstr "Nom DNS" + +#: templates/machines/aff_machines.html:45 +msgid "Type" +msgstr "Type" + +#: templates/machines/aff_machines.html:47 +msgid "IP address" +msgstr "Adresse IP" + +#: templates/machines/aff_machines.html:48 +msgid "Actions" +msgstr "Actions" + +#: templates/machines/aff_machines.html:53 +msgid "No name" +msgstr "Sans nom" + +#: templates/machines/aff_machines.html:54 +msgid "View the profile" +msgstr "Voir le profil" + +#: templates/machines/aff_machines.html:62 views.py:375 +msgid "Create an interface" +msgstr "Créer une interface" + +#: templates/machines/aff_machines.html:77 +msgid "Display the aliases" +msgstr "Afficher les alias" + +#: templates/machines/aff_machines.html:95 +msgid "Display the IPv6 address" +msgstr "Afficher les adresses IPv6" + +#: templates/machines/aff_machines.html:110 +msgid " Edit" +msgstr " Modifier" + +#: templates/machines/aff_machines.html:118 +msgid " Manage the aliases" +msgstr " Gérer les alias" + +#: templates/machines/aff_machines.html:126 +msgid " Manage the IPv6 addresses" +msgstr " Gérer les adresses IPv6" + +#: templates/machines/aff_machines.html:134 +msgid " Manage the SSH fingerprints" +msgstr " Gérer les empreintes SSH" + +#: templates/machines/aff_machines.html:142 +msgid " Manage the ports configuration" +msgstr " Gérer les configuration de ports" + +#: templates/machines/aff_machinetype.html:33 +msgid "Matching IP type" +msgstr "Type d'IP correspondant" + +#: templates/machines/aff_mx.html:32 templates/machines/aff_ns.html:32 +#: templates/machines/aff_txt.html:32 +msgid "Concerned zone" +msgstr "Zone concernée" + +#: templates/machines/aff_mx.html:33 templates/machines/aff_srv.html:36 +msgid "Priority" +msgstr "Priorité" + +#: templates/machines/aff_nas.html:33 templates/machines/aff_soa.html:32 +#: templates/machines/aff_vlan.html:34 +#: templates/machines/index_portlist.html:18 +msgid "Name" +msgstr "Nom" + +#: templates/machines/aff_nas.html:34 +msgid "NAS device type" +msgstr "Type de dispositif NAS" + +#: templates/machines/aff_nas.html:35 +msgid "Machine type linked to the NAS device" +msgstr "Type de machine lié au dispositif NAS" + +#: templates/machines/aff_nas.html:36 +msgid "Access mode" +msgstr "Mode d'accès" + +#: templates/machines/aff_nas.html:37 +msgid "MAC address auto capture" +msgstr "Capture automatique de l'adresse MAC" + +#: templates/machines/aff_ns.html:33 +msgid "Authoritarian interface for the concerned zone" +msgstr "Interface authoritaire pour la zone concernée" + +#: templates/machines/aff_role.html:33 +msgid "Role name" +msgstr "Nom du rôle" + +#: templates/machines/aff_role.html:34 +msgid "Specific role" +msgstr "Rôle spécifique" + +#: templates/machines/aff_role.html:35 +msgid "Servers" +msgstr "Serveurs" + +#: templates/machines/aff_servers.html:31 +#: templates/machines/aff_service.html:32 +msgid "Service name" +msgstr "Nom du service" + +#: templates/machines/aff_servers.html:32 +msgid "Server" +msgstr "Serveur" + +#: templates/machines/aff_servers.html:33 +msgid "Last regeneration" +msgstr "Dernière régénération" + +#: templates/machines/aff_servers.html:34 +msgid "Regeneration required" +msgstr "Régénération requise" + +#: templates/machines/aff_servers.html:35 +msgid "Regeneration activated" +msgstr "Régénération activée" + +#: templates/machines/aff_service.html:33 +msgid "Minimal time before regeneration" +msgstr "Temps minimal avant régénération" + +#: templates/machines/aff_service.html:34 +msgid "Maximal time before regeneration" +msgstr "Temps maximal avant régénération" + +#: templates/machines/aff_service.html:35 +msgid "Included servers" +msgstr "Serveurs inclus" + +#: templates/machines/aff_service.html:36 +msgid "Ask for regeneration" +msgstr "Demander la régénération" + +#: templates/machines/aff_soa.html:33 +msgid "Mail" +msgstr "Mail" + +#: templates/machines/aff_soa.html:34 +msgid "Refresh" +msgstr "Rafraichissement" + +#: templates/machines/aff_soa.html:35 +msgid "Retry" +msgstr "Relance" + +#: templates/machines/aff_soa.html:36 +msgid "Expire" +msgstr "Expiration" + +#: templates/machines/aff_soa.html:37 templates/machines/aff_srv.html:35 +msgid "TTL" +msgstr "Temps de vie" + +#: templates/machines/aff_srv.html:32 templates/machines/machine.html:152 +msgid "Service" +msgstr "Service" + +#: templates/machines/aff_srv.html:33 +msgid "Protocol" +msgstr "Protocole" + +#: templates/machines/aff_srv.html:37 +msgid "Weight" +msgstr "Poids" + +#: templates/machines/aff_srv.html:38 +msgid "Port" +msgstr "Port" + +#: templates/machines/aff_srv.html:39 +msgid "Target" +msgstr "Cible" + +#: templates/machines/aff_sshfp.html:31 +msgid "SSH public key" +msgstr "Clé publique SSH" + +#: templates/machines/aff_sshfp.html:32 +msgid "Algorithm used" +msgstr "Algorithme utilisé" + +#: templates/machines/aff_sshfp.html:33 templates/machines/aff_vlan.html:35 +msgid "Comment" +msgstr "Commentaire" + +#: templates/machines/aff_vlan.html:33 +msgid "ID" +msgstr "ID" + +#: templates/machines/aff_vlan.html:36 templates/machines/sidebar.html:51 +msgid "IP ranges" +msgstr "Plages d'IP" + +#: templates/machines/delete.html:29 +msgid "Creation and editing of machines" +msgstr "Création et modification de machines" + +#: templates/machines/delete.html:35 +#, python-format +msgid "" +"Warning: are you sure you want to delete this object %(objet_name)s " +"( %(objet)s )?" +msgstr "" +"Attention : voulez-vous vraiment supprimer cet objet %(objet_name)s " +"( %(objet)s ) ?" + +#: templates/machines/delete.html:36 +msgid "Confirm" +msgstr "Confirmer" + +#: templates/machines/edit_portlist.html:29 templates/machines/index.html:29 +#: templates/machines/index.html:32 templates/machines/index_alias.html:29 +#: templates/machines/index_extension.html:31 +#: templates/machines/index_iptype.html:31 +#: templates/machines/index_ipv6.html:30 +#: templates/machines/index_machinetype.html:31 +#: templates/machines/index_nas.html:31 +#: templates/machines/index_portlist.html:8 +#: templates/machines/index_portlist.html:23 +#: templates/machines/index_role.html:30 +#: templates/machines/index_service.html:30 +#: templates/machines/index_sshfp.html:28 templates/machines/index_vlan.html:31 +#: templates/machines/machine.html:31 templates/machines/sidebar.html:33 +msgid "Machines" +msgstr "Machines" + +#: templates/machines/edit_portlist.html:50 +msgid "Add a port" +msgstr "Ajouter un port" + +#: templates/machines/edit_portlist.html:53 +msgid "Create or edit" +msgstr "Créer ou modifier" + +#: templates/machines/index_alias.html:32 +msgid "List of the aliases of the interface" +msgstr "Liste des alias de l'interface" + +#: templates/machines/index_alias.html:33 +msgid " Add an alias" +msgstr " Ajouter un alias" + +#: templates/machines/index_alias.html:34 +msgid " Delete one or several aliases" +msgstr " Supprimer un ou plusieurs alias" + +#: templates/machines/index_extension.html:34 +msgid "List of extensions" +msgstr "Liste des extensions" + +#: templates/machines/index_extension.html:36 +msgid " Add an extension" +msgstr " Ajouter une extension" + +#: templates/machines/index_extension.html:38 +msgid " Delete one or several extensions" +msgstr " Supprimer une ou plusieurs extensions" + +#: templates/machines/index_extension.html:41 +msgid "List of SOA records" +msgstr "Liste des enregistrements SOA" + +#: templates/machines/index_extension.html:43 +msgid " Add an SOA record" +msgstr " Ajouter un enregistrement SOA" + +#: templates/machines/index_extension.html:45 +msgid " Delete one or several SOA records" +msgstr " Supprimer un ou plusieurs enregistrements SOA" + +#: templates/machines/index_extension.html:48 +msgid "List of MX records" +msgstr "Liste des enregistrements MX" + +#: templates/machines/index_extension.html:50 +msgid " Add an MX record" +msgstr " Ajouter un enregistrement MX" + +#: templates/machines/index_extension.html:52 +msgid " Delete one or several MX records" +msgstr " Supprimer un ou plusieurs enregistrements MX" + +#: templates/machines/index_extension.html:55 +msgid "List of NS records" +msgstr "Liste des enregistrements NS" + +#: templates/machines/index_extension.html:57 +msgid " Add an NS record" +msgstr " Ajouter un enregistrement NS" + +#: templates/machines/index_extension.html:59 +msgid " Delete one or several NS records" +msgstr " Supprimer un ou plusieurs enregistrements NS" + +#: templates/machines/index_extension.html:62 +msgid "List of TXT records" +msgstr "Liste des enregistrements TXT" + +#: templates/machines/index_extension.html:64 +msgid " Add a TXT record" +msgstr " Ajouter un enregistrement TXT" + +#: templates/machines/index_extension.html:66 +msgid " Delete one or several TXT records" +msgstr " Supprimer un ou plusieurs enregistrements TXT" + +#: templates/machines/index_extension.html:69 +msgid "List of DNAME records" +msgstr "Liste des enregistrements DNAME" + +#: templates/machines/index_extension.html:72 +msgid " Add a DNAME record" +msgstr " Ajouter un enregistrement DNAME" + +#: templates/machines/index_extension.html:76 +msgid " Delete one or several DNAME records" +msgstr " Supprimer un ou plusieurs enregistrements DNAME" + +#: templates/machines/index_extension.html:80 +msgid "List of SRV records" +msgstr "Liste des enregistrements SRV" + +#: templates/machines/index_extension.html:82 +msgid " Add an SRV record" +msgstr " Ajouter un enregistrement SRV" + +#: templates/machines/index_extension.html:84 +msgid " Delete one or several SRV records" +msgstr " Supprimer un ou plusieurs enregistrements SRV" + +#: templates/machines/index_iptype.html:34 +msgid "List of IP types" +msgstr "Liste des types d'IP" + +#: templates/machines/index_iptype.html:36 +msgid " Add an IP type" +msgstr " Ajouter un type d'IP" + +#: templates/machines/index_iptype.html:38 +msgid " Delete one or several IP types" +msgstr " Supprimer un ou plusieurs types d'IP" + +#: templates/machines/index_ipv6.html:33 +msgid "List of the IPv6 addresses of the interface" +msgstr "Liste des adresses IPv6 de l'interface" + +#: templates/machines/index_ipv6.html:35 +msgid " Add an IPv6 address" +msgstr " Ajouter une adresse IPv6" + +#: templates/machines/index_machinetype.html:34 +msgid "List of machine types" +msgstr "Liste des types de machine" + +#: templates/machines/index_machinetype.html:36 +msgid " Add a machine type" +msgstr " Ajouter un type de machine" + +#: templates/machines/index_machinetype.html:38 +msgid " Delete one or several machine types" +msgstr " Supprimer un ou plusieurs types de machine" + +#: templates/machines/index_nas.html:34 +msgid "List of NAS devices" +msgstr "Liste des dispositifs NAS" + +#: templates/machines/index_nas.html:35 +msgid "" +"The NAS device type and machine type are linked. It is useful for MAC " +"address auto capture by RADIUS, and allows to choose the machine type to " +"assign to the machines according to the NAS device type." +msgstr "" +"Le type de dispositif NAS et le type de machine sont liés. C'est utile pour " +"la capture automatique de l'adresse MAC par RADIUS, et permet de choisir le " +"type de machine à assigner aux machines en fonction du type de dispositif " +"NAS." + +#: templates/machines/index_nas.html:37 +msgid " Add a NAS device type" +msgstr " Ajouter un type de dispositif NAS" + +#: templates/machines/index_nas.html:39 +msgid " Delete one or several NAS device types" +msgstr " Supprimer un ou plusieurs types de dispositif NAS" + +#: templates/machines/index_portlist.html:11 +msgid "List of ports configurations" +msgstr "Liste des configurations de ports" + +#: templates/machines/index_portlist.html:13 +msgid " Add a configuration" +msgstr " Ajouter une configuration" + +#: templates/machines/index_portlist.html:19 +msgid "TCP (input)" +msgstr "TCP (entrée)" + +#: templates/machines/index_portlist.html:20 +msgid "TCP (output)" +msgstr "TCP (sortie)" + +#: templates/machines/index_portlist.html:21 +msgid "UDP (input)" +msgstr "UDP (entrée)" + +#: templates/machines/index_portlist.html:22 +msgid "UDP (output)" +msgstr "UDP (sortie)" + +#: templates/machines/index_role.html:33 +msgid "List of roles" +msgstr "Liste des rôles" + +#: templates/machines/index_role.html:35 +msgid " Add a role" +msgstr " Ajouter un rôle" + +#: templates/machines/index_role.html:37 +msgid " Delete one or several roles" +msgstr " Supprimer un ou plusieurs rôles" + +#: templates/machines/index_service.html:33 +msgid "List of services" +msgstr "Liste des services" + +#: templates/machines/index_service.html:35 +msgid " Add a service" +msgstr " Ajouter un service" + +#: templates/machines/index_service.html:37 +msgid " Delete one or several services" +msgstr " Supprimer un ou plusieurs services" + +#: templates/machines/index_service.html:39 +msgid "States of servers" +msgstr "États des serveurs" + +#: templates/machines/index_sshfp.html:31 +msgid "SSH fingerprints" +msgstr "Empreintes SSH" + +#: templates/machines/index_sshfp.html:34 +msgid " Add an SSH fingerprint" +msgstr " Ajouter une empreinte SSH" + +#: templates/machines/index_vlan.html:34 +msgid "List of VLANs" +msgstr "Liste des VLANs" + +#: templates/machines/index_vlan.html:36 +msgid " Add a VLAN" +msgstr " Ajouter un VLAN" + +#: templates/machines/index_vlan.html:38 +msgid " Delete one or several VLANs" +msgstr " Supprimer un ou plusieurs VLANs" + +#: templates/machines/machine.html:92 +msgid "Machine" +msgstr "Machine" + +#: templates/machines/machine.html:96 +msgid "Interface" +msgstr "Interface" + +#: templates/machines/machine.html:104 +msgid "Domain" +msgstr "Domaine" + +#: templates/machines/machine.html:148 +msgid "Alias" +msgstr "Alias" + +#: templates/machines/machine.html:168 +msgid "IPv6 address" +msgstr "Adresse IPv6" + +#: templates/machines/sidebar.html:39 +msgid "Machine types" +msgstr "Types de machine" + +#: templates/machines/sidebar.html:45 +msgid "Extensions and zones" +msgstr "Extensions et zones" + +#: templates/machines/sidebar.html:69 +msgid "Services (DHCP, DNS, ...)" +msgstr "Services (DHCP, DNS, ...)" + +#: templates/machines/sidebar.html:75 +msgid "Server roles" +msgstr "Rôles de serveur" + +#: templates/machines/sidebar.html:81 +msgid "Ports openings" +msgstr "Ouvertures de ports" + +#: views.py:156 +msgid "Select a machine type first.}," +msgstr "Sélectionnez un type de machine d'abord.}," + +#: views.py:258 +msgid "The machine was created." +msgstr "La machine a été créée." + +#: views.py:270 +msgid "Create a machine" +msgstr "Créer une machine" + +#: views.py:310 +msgid "The machine was edited." +msgstr "La machine a été modifiée." + +#: views.py:322 views.py:446 views.py:512 views.py:568 views.py:630 +#: views.py:691 views.py:749 views.py:806 views.py:863 views.py:919 +#: views.py:977 views.py:1034 views.py:1106 views.py:1169 views.py:1226 +#: views.py:1292 views.py:1349 +msgid "Edit" +msgstr "Modifier" + +#: views.py:335 +msgid "The machine was deleted." +msgstr "La machine a été supprimée." + +#: views.py:364 +msgid "The interface was created." +msgstr "L'interface a été créée." + +#: views.py:391 +msgid "The interface was deleted." +msgstr "L'interface a été supprimée." + +#: views.py:416 +msgid "The IPv6 addresses list was created." +msgstr "La liste d'adresses IPv6 a été créée." + +#: views.py:422 +msgid "Create an IPv6 addresses list" +msgstr "Créer une liste d'adresses IPv6" + +#: views.py:440 +msgid "The IPv6 addresses list was edited." +msgstr "La liste d'adresses IPv6 a été modifiée." + +#: views.py:459 +msgid "The IPv6 addresses list was deleted." +msgstr "La liste d'adresses IPv6 a été supprimée." + +#: views.py:483 +msgid "The SSHFP record was created." +msgstr "L'enregistrement SSHFP a été créé." + +#: views.py:489 +msgid "Create a SSHFP record" +msgstr "Créer un enregistrement SSHFP" + +#: views.py:506 +msgid "The SSHFP record was edited." +msgstr "L'enregistrement SSHFP a été modifié." + +#: views.py:525 +msgid "The SSHFP record was deleted." +msgstr "L'enregistrement SSHFP a été supprimé." + +#: views.py:546 +msgid "The IP type was created." +msgstr "Le type d'IP a été créé." + +#: views.py:549 +msgid "Create an IP type" +msgstr "Créer un type d'IP" + +#: views.py:565 +msgid "The IP type was edited." +msgstr "Le type d'IP a été modifié." + +#: views.py:584 +msgid "The IP type was deleted." +msgstr "Le type d'IP a été supprimé." + +#: views.py:588 +#, python-format +msgid "" +"The IP type %s is assigned to at least one machine, you can't delete it." +msgstr "" +"Le type d'IP %s est assigné à au moins une machine, vous ne pouvez pas le " +"supprimer." + +#: views.py:593 views.py:655 views.py:716 views.py:773 views.py:830 +#: views.py:887 views.py:944 views.py:1001 views.py:1058 views.py:1136 +#: views.py:1193 views.py:1250 views.py:1316 views.py:1373 +msgid "Delete" +msgstr "Supprimer" + +#: views.py:606 +msgid "The machine type was created." +msgstr "Le type de machine a été créé." + +#: views.py:609 +msgid "Create a machine type" +msgstr "Créer un type de machine" + +#: views.py:627 +msgid "The machine type was edited." +msgstr "Le type de machine a été modifié." + +#: views.py:646 +msgid "The machine type was deleted." +msgstr "Le type de machine a été supprimé." + +#: views.py:650 +#, python-format +msgid "" +"The machine type %s is assigned to at least one machine, you can't delete it." +msgstr "" +"Le type de machine %s est assigné à au moins un machine, vous ne pouvez pas " +"le supprimer." + +#: views.py:668 +msgid "The extension was created." +msgstr "L'extension a été créée." + +#: views.py:671 +msgid "Create an extension" +msgstr "Créer une extension" + +#: views.py:688 +msgid "The extension was edited." +msgstr "L'extension a été modifiée." + +#: views.py:707 +msgid "The extension was deleted." +msgstr "L'extension a été supprimée." + +#: views.py:711 +#, python-format +msgid "" +"The extension %s is assigned to at least one machine type, you can't delete " +"it." +msgstr "" +"L'extension %s est assignée à au moins un type de machine, vous ne pouvez " +"pas le supprimer." + +#: views.py:729 +msgid "The SOA record was created." +msgstr "L'enregistrement SOA a été créé." + +#: views.py:732 +msgid "Create an SOA record" +msgstr "Créer un enregistrement SOA" + +#: views.py:746 +msgid "The SOA record was edited." +msgstr "L'enregistrement SOA a été modifié." + +#: views.py:765 +msgid "The SOA record was deleted." +msgstr "L'enregistrement SOA a été supprimé." + +#: views.py:769 +#, python-format +msgid "Error: the SOA record %s can't be deleted." +msgstr "Erreur : l'enregistrement SOA %s ne peut pas être supprimé." + +#: views.py:786 +msgid "The MX record was created." +msgstr "L'enregistrement MX a été créé." + +#: views.py:789 +msgid "Create an MX record" +msgstr "Créer un enregistrement MX" + +#: views.py:803 +msgid "The MX record was edited." +msgstr "L'enregistrement MX a été modifié." + +#: views.py:822 +msgid "The MX record was deleted." +msgstr "L'enregistrement MX a été supprimé." + +#: views.py:826 +#, python-format +msgid "Error: the MX record %s can't be deleted." +msgstr "Erreur : l'enregistrement MX %s ne peut pas être supprimé." + +#: views.py:843 +msgid "The NS record was created." +msgstr "L'enregistrement NS a été créé." + +#: views.py:846 +msgid "Create an NS record" +msgstr "Créer un enregistrement NS" + +#: views.py:860 +msgid "The NS record was edited." +msgstr "L'enregistrement NS a été modifié." + +#: views.py:879 +msgid "The NS record was deleted." +msgstr "L'enregistrement NS a été supprimé." + +#: views.py:883 +#, python-format +msgid "Error: the NS record %s can't be deleted." +msgstr "Erreur : l'enregistrement NS %s ne peut pas être supprimé." + +#: views.py:899 +msgid "The DNAME record was created." +msgstr "L'enregistrement DNAME a été créé." + +#: views.py:902 +msgid "Create a DNAME record" +msgstr "Créer un enregistrement DNAME" + +#: views.py:916 +msgid "The DNAME record was edited." +msgstr "L'enregistrement DNAME a été modifié." + +#: views.py:935 +msgid "The DNAME record was deleted." +msgstr "L'enregistrement DNAME a été supprimé." + +#: views.py:939 +#, python-format +msgid "Error: the DNAME record %s can't be deleted." +msgstr "Erreur : l'enregistrement DNAME %s ne peut pas être supprimé." + +#: views.py:957 +msgid "The TXT record was created." +msgstr "L'enregistrement TXT a été créé." + +#: views.py:960 +msgid "Create a TXT record" +msgstr "Créer un enregistrement TXT" + +#: views.py:974 +msgid "The TXT record was edited." +msgstr "L'enregistrement TXT a été modifié." + +#: views.py:993 +msgid "The TXT record was deleted." +msgstr "L'enregistrement TXT a été supprimé." + +#: views.py:997 +#, python-format +msgid "Error: the TXT record %s can't be deleted." +msgstr "Erreur : l'enregistrement %s ne peut pas être supprimé." + +#: views.py:1014 +msgid "The SRV record was created." +msgstr "L'enregistrement SRV a été créé." + +#: views.py:1017 +msgid "Create an SRV record" +msgstr "Créer un enregistrement SRV" + +#: views.py:1031 +msgid "The SRV record was edited." +msgstr "L'enregistrement SRV a été modifié." + +#: views.py:1050 +msgid "The SRV record was deleted." +msgstr "L'enregistrement SRV a été supprimé." + +#: views.py:1054 +#, python-format +msgid "Error: the SRV record %s can't be deleted." +msgstr "Erreur : l'enregistrement SRV %s ne peut pas être supprimé." + +#: views.py:1074 +msgid "The alias was created." +msgstr "L'alias a été créé." + +#: views.py:1080 +msgid "Create an alias" +msgstr "Créer un alias" + +#: views.py:1098 +msgid "The alias was edited." +msgstr "L'alias a été modifié." + +#: views.py:1124 +#, python-format +msgid "The alias %s was deleted." +msgstr "L'alias %s a été supprimé." + +#: views.py:1129 +#, python-format +msgid "Error: the alias %s can't be deleted." +msgstr "Erreur : l'alias %s ne peut pas être supprimé." + +#: views.py:1149 +msgid "The role was created." +msgstr "Le rôle a été créé." + +#: views.py:1152 +msgid "Create a role" +msgstr "Créer un rôle" + +#: views.py:1166 +msgid "The role was edited." +msgstr "Le rôle a été modifié." + +#: views.py:1185 +msgid "The role was deleted." +msgstr "Le rôle a été supprimé." + +#: views.py:1189 +#, python-format +msgid "Error: the role %s can't be deleted." +msgstr "Erreur : le rôle %s ne peut pas être supprimé." + +#: views.py:1206 +msgid "The service was created." +msgstr "Le service a été créé." + +#: views.py:1209 +msgid "Create a service" +msgstr "Créer un service" + +#: views.py:1223 +msgid "The service was edited." +msgstr "Le service a été modifié." + +#: views.py:1242 +msgid "The service was deleted." +msgstr "Le service a été supprimé." + +#: views.py:1246 +#, python-format +msgid "Error: the service %s can't be deleted." +msgstr "Erreur : le service %s ne peut pas être supprimé." + +#: views.py:1272 +msgid "The VLAN was created." +msgstr "Le VLAN a été créé." + +#: views.py:1275 +msgid "Create a VLAN" +msgstr "Créer un VLAN" + +#: views.py:1289 +msgid "The VLAN was edited." +msgstr "Le VLAN a été modifié." + +#: views.py:1308 +msgid "The VLAN was deleted." +msgstr "Le VLAN a été supprimé." + +#: views.py:1312 +#, python-format +msgid "Error: the VLAN %s can't be deleted." +msgstr "Erreur : le VLAN %s ne peut pas être supprimé." + +#: views.py:1329 +msgid "The NAS device was created." +msgstr "Le dispositif NAS a été créé." + +#: views.py:1332 +msgid "Create a NAS device" +msgstr "Créer un dispositif NAS" + +#: views.py:1346 +msgid "The NAS device was edited." +msgstr "Le dispositif NAS a été modifié." + +#: views.py:1365 +msgid "The NAS device was deleted." +msgstr "Le dispositif NAS a été supprimé." + +#: views.py:1369 +#, python-format +msgid "Error: the NAS device %s can't be deleted." +msgstr "Erreur : le dispositif NAS %s ne peut pas être supprimé." + +#: views.py:1625 +msgid "The ports list was edited." +msgstr "La liste de ports a été modifiée." + +#: views.py:1639 +msgid "The ports list was deleted." +msgstr "La liste de ports a été supprimée." + +#: views.py:1664 +msgid "The ports list was created." +msgstr "La liste de ports a été créée." + +#: views.py:1682 +msgid "Warning: the IPv4 isn't public, the opening won't have effect in v4." +msgstr "" +"Attention : l'adresse IPv4 n'est pas publique, l'ouverture n'aura pas " +"d'effet en v4." + +#: views.py:1692 +msgid "The ports configuration was edited." +msgstr "La configuration de ports a été modifiée." + +#: views.py:1695 +msgid "Edit the configuration" +msgstr "Modifier la configuration" diff --git a/machines/migrations/0086_role.py b/machines/migrations/0086_role.py new file mode 100644 index 00000000..a23de26f --- /dev/null +++ b/machines/migrations/0086_role.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-23 14:07 +from __future__ import unicode_literals + +from django.db import migrations, models +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0085_sshfingerprint'), + ] + + operations = [ + migrations.CreateModel( + name='Role', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role_type', models.CharField(max_length=255, unique=True)), + ('servers', models.ManyToManyField(to='machines.Interface')), + ('specific_role', models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'Radius server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gatewaw')], max_length=32, null=True)) + ], + options={'permissions': (('view_role', 'Can view a role.'),), 'verbose_name': 'Server role'}, + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), + ), + ] diff --git a/machines/migrations/0087_dnssec.py b/machines/migrations/0087_dnssec.py new file mode 100644 index 00000000..cc2a25ec --- /dev/null +++ b/machines/migrations/0087_dnssec.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-06-25 15:06 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0086_role'), + ] + + operations = [ + migrations.AddField( + model_name='iptype', + name='dnssec_reverse_v4', + field=models.BooleanField(default=False, help_text='Activer DNSSEC sur le reverse DNS IPv4'), + ), + migrations.AddField( + model_name='iptype', + name='dnssec_reverse_v6', + field=models.BooleanField(default=False, help_text='Activer DNSSEC sur le reverse DNS IPv6'), + ), + ] diff --git a/machines/migrations/0088_iptype_prefix_v6_length.py b/machines/migrations/0088_iptype_prefix_v6_length.py new file mode 100644 index 00000000..e061167c --- /dev/null +++ b/machines/migrations/0088_iptype_prefix_v6_length.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-07-16 18:46 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0087_dnssec'), + ] + + operations = [ + migrations.AddField( + model_name='iptype', + name='prefix_v6_length', + field=models.IntegerField(default=64, validators=[django.core.validators.MaxValueValidator(128), django.core.validators.MinValueValidator(0)]), + ), + ] diff --git a/machines/migrations/0089_auto_20180805_1148.py b/machines/migrations/0089_auto_20180805_1148.py new file mode 100644 index 00000000..76962283 --- /dev/null +++ b/machines/migrations/0089_auto_20180805_1148.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-05 09:48 +from __future__ import unicode_literals + +from django.db import migrations +import macaddress.fields + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0088_iptype_prefix_v6_length'), + ] + + operations = [ + migrations.AlterField( + model_name='interface', + name='mac_address', + field=macaddress.fields.MACAddressField(integer=False, max_length=17), + ), + ] diff --git a/machines/migrations/0090_auto_20180805_1459.py b/machines/migrations/0090_auto_20180805_1459.py new file mode 100644 index 00000000..08af8587 --- /dev/null +++ b/machines/migrations/0090_auto_20180805_1459.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-05 12:59 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0089_auto_20180805_1148'), + ] + + operations = [ + migrations.AlterField( + model_name='ipv6list', + name='ipv6', + field=models.GenericIPAddressField(protocol='IPv6'), + ), + ] diff --git a/machines/migrations/0091_auto_20180806_2310.py b/machines/migrations/0091_auto_20180806_2310.py new file mode 100644 index 00000000..cd756cad --- /dev/null +++ b/machines/migrations/0091_auto_20180806_2310.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-06 21:10 +from __future__ import unicode_literals + +import django.core.validators +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0090_auto_20180805_1459'), + ] + + operations = [ + migrations.AddField( + model_name='iptype', + name='domaine_ip_netmask', + field=models.IntegerField(default=24, help_text='Netmask for the ipv4 range domain', validators=[django.core.validators.MaxValueValidator(31), django.core.validators.MinValueValidator(8)]), + ), + migrations.AddField( + model_name='iptype', + name='domaine_ip_network', + field=models.GenericIPAddressField(blank=True, help_text='Network containing the ipv4 range domain ip start/stop. Optional', null=True, protocol='IPv4'), + ), + ] diff --git a/machines/migrations/0092_auto_20180807_0926.py b/machines/migrations/0092_auto_20180807_0926.py new file mode 100644 index 00000000..f109a650 --- /dev/null +++ b/machines/migrations/0092_auto_20180807_0926.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-07 07:26 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0091_auto_20180806_2310'), + ] + + operations = [ + migrations.RenameField( + model_name='iptype', + old_name='dnssec_reverse_v4', + new_name='reverse_v4', + ), + migrations.RenameField( + model_name='iptype', + old_name='dnssec_reverse_v6', + new_name='reverse_v6', + ), + ] diff --git a/machines/migrations/0093_auto_20180807_1115.py b/machines/migrations/0093_auto_20180807_1115.py new file mode 100644 index 00000000..866cb87d --- /dev/null +++ b/machines/migrations/0093_auto_20180807_1115.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-07 09:15 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0092_auto_20180807_0926'), + ] + + operations = [ + migrations.AlterField( + model_name='mx', + name='name', + field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='machines.Domain'), + ), + migrations.AlterField( + model_name='mx', + name='priority', + field=models.PositiveIntegerField(), + ), + ] diff --git a/machines/migrations/0094_auto_20180815_1918.py b/machines/migrations/0094_auto_20180815_1918.py new file mode 100644 index 00000000..775ac2c7 --- /dev/null +++ b/machines/migrations/0094_auto_20180815_1918.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-15 17:18 +from __future__ import unicode_literals + +import datetime +import django.core.validators +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0093_auto_20180807_1115'), + ] + + operations = [ + migrations.AlterModelOptions( + name='dname', + options={'permissions': (('view_dname', 'Can view a DNAME record object'),), 'verbose_name': 'DNAME record', 'verbose_name_plural': 'DNAME records'}, + ), + migrations.AlterModelOptions( + name='domain', + options={'permissions': (('view_domain', 'Can view a domain object'),), 'verbose_name': 'domain', 'verbose_name_plural': 'domains'}, + ), + migrations.AlterModelOptions( + name='extension', + options={'permissions': (('view_extension', 'Can view an extension object'), ('use_all_extension', 'Can use all extensions')), 'verbose_name': 'DNS extension', 'verbose_name_plural': 'DNS extensions'}, + ), + migrations.AlterModelOptions( + name='interface', + options={'permissions': (('view_interface', 'Can view an interface object'), ('change_interface_machine', 'Can change the owner of an interface')), 'verbose_name': 'interface', 'verbose_name_plural': 'interfaces'}, + ), + migrations.AlterModelOptions( + name='iplist', + options={'permissions': (('view_iplist', 'Can view an IPv4 addresses list object'),), 'verbose_name': 'IPv4 addresses list', 'verbose_name_plural': 'IPv4 addresses lists'}, + ), + migrations.AlterModelOptions( + name='iptype', + options={'permissions': (('view_iptype', 'Can view an IP type object'), ('use_all_iptype', 'Can use all IP types')), 'verbose_name': 'IP type', 'verbose_name_plural': 'IP types'}, + ), + migrations.AlterModelOptions( + name='ipv6list', + options={'permissions': (('view_ipv6list', 'Can view an IPv6 addresses list object'), ('change_ipv6list_slaac_ip', 'Can change the SLAAC value of an IPv6 addresses list')), 'verbose_name': 'IPv6 addresses list', 'verbose_name_plural': 'IPv6 addresses lists'}, + ), + migrations.AlterModelOptions( + name='machine', + options={'permissions': (('view_machine', 'Can view a machine object'), ('change_machine_user', 'Can change the user of a machine')), 'verbose_name': 'machine', 'verbose_name_plural': 'machines'}, + ), + migrations.AlterModelOptions( + name='machinetype', + options={'permissions': (('view_machinetype', 'Can view a machine type object'), ('use_all_machinetype', 'Can use all machine types')), 'verbose_name': 'machine type', 'verbose_name_plural': 'machine types'}, + ), + migrations.AlterModelOptions( + name='mx', + options={'permissions': (('view_mx', 'Can view an MX record object'),), 'verbose_name': 'MX record', 'verbose_name_plural': 'MX records'}, + ), + migrations.AlterModelOptions( + name='nas', + options={'permissions': (('view_nas', 'Can view a NAS device object'),), 'verbose_name': 'NAS device', 'verbose_name_plural': 'NAS devices'}, + ), + migrations.AlterModelOptions( + name='ns', + options={'permissions': (('view_ns', 'Can view an NS record object'),), 'verbose_name': 'NS record', 'verbose_name_plural': 'NS records'}, + ), + migrations.AlterModelOptions( + name='ouvertureport', + options={'verbose_name': 'ports openings'}, + ), + migrations.AlterModelOptions( + name='ouvertureportlist', + options={'permissions': (('view_ouvertureportlist', 'Can view a ports opening list object'),), 'verbose_name': 'ports opening list', 'verbose_name_plural': 'ports opening lists'}, + ), + migrations.AlterModelOptions( + name='role', + options={'permissions': (('view_role', 'Can view a role object'),), 'verbose_name': 'server role', 'verbose_name_plural': 'server roles'}, + ), + migrations.AlterModelOptions( + name='service', + options={'permissions': (('view_service', 'Can view a service object'),), 'verbose_name': 'service to generate (DHCP, DNS, ...)', 'verbose_name_plural': 'services to generate (DHCP, DNS, ...)'}, + ), + migrations.AlterModelOptions( + name='service_link', + options={'permissions': (('view_service_link', 'Can view a service server link object'),), 'verbose_name': 'link between service and server', 'verbose_name_plural': 'links between service and server'}, + ), + migrations.AlterModelOptions( + name='soa', + options={'permissions': (('view_soa', 'Can view an SOA record object'),), 'verbose_name': 'SOA record', 'verbose_name_plural': 'SOA records'}, + ), + migrations.AlterModelOptions( + name='srv', + options={'permissions': (('view_srv', 'Can view an SRV record object'),), 'verbose_name': 'SRV record', 'verbose_name_plural': 'SRV records'}, + ), + migrations.AlterModelOptions( + name='sshfp', + options={'permissions': (('view_sshfp', 'Can view an SSHFP record object'),), 'verbose_name': 'SSHFP record', 'verbose_name_plural': 'SSHFP records'}, + ), + migrations.AlterModelOptions( + name='txt', + options={'permissions': (('view_txt', 'Can view a TXT record object'),), 'verbose_name': 'TXT record', 'verbose_name_plural': 'TXT records'}, + ), + migrations.AlterModelOptions( + name='vlan', + options={'permissions': (('view_vlan', 'Can view a VLAN object'),), 'verbose_name': 'VLAN', 'verbose_name_plural': 'VLANs'}, + ), + migrations.AlterField( + model_name='domain', + name='name', + field=models.CharField(help_text='Mandatory and unique, must not contain dots.', max_length=255), + ), + migrations.AlterField( + model_name='extension', + name='name', + field=models.CharField(help_text='Zone name, must begin with a dot (.example.org)', max_length=255, unique=True), + ), + migrations.AlterField( + model_name='extension', + name='origin', + field=models.ForeignKey(blank=True, help_text='A record associated with the zone', null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.IpList'), + ), + migrations.AlterField( + model_name='extension', + name='origin_v6', + field=models.GenericIPAddressField(blank=True, help_text='AAAA record associated with the zone', null=True, protocol='IPv6'), + ), + migrations.AlterField( + model_name='iptype', + name='domaine_ip_netmask', + field=models.IntegerField(default=24, help_text="Netmask for the domain's IPv4 range", validators=[django.core.validators.MaxValueValidator(31), django.core.validators.MinValueValidator(8)]), + ), + migrations.AlterField( + model_name='iptype', + name='domaine_ip_network', + field=models.GenericIPAddressField(blank=True, help_text="Network containing the domain's IPv4 range (optional)", null=True, protocol='IPv4'), + ), + migrations.AlterField( + model_name='iptype', + name='reverse_v4', + field=models.BooleanField(default=False, help_text='Enable reverse DNS for IPv4'), + ), + migrations.AlterField( + model_name='iptype', + name='reverse_v6', + field=models.BooleanField(default=False, help_text='Enable reverse DNS for IPv6'), + ), + migrations.AlterField( + model_name='machine', + name='name', + field=models.CharField(blank=True, help_text='Optional', max_length=255, null=True), + ), + migrations.AlterField( + model_name='ouvertureportlist', + name='name', + field=models.CharField(help_text='Name of the ports configuration', max_length=255), + ), + migrations.AlterField( + model_name='role', + name='specific_role', + field=models.CharField(blank=True, choices=[('dhcp-server', 'DHCP server'), ('switch-conf-server', 'Switches configuration server'), ('dns-recursif-server', 'Recursive DNS server'), ('ntp-server', 'NTP server'), ('radius-server', 'RADIUS server'), ('log-server', 'Log server'), ('ldap-master-server', 'LDAP master server'), ('ldap-backup-server', 'LDAP backup server'), ('smtp-server', 'SMTP server'), ('postgresql-server', 'postgreSQL server'), ('mysql-server', 'mySQL server'), ('sql-client', 'SQL client'), ('gateway', 'Gateway')], max_length=32, null=True), + ), + migrations.AlterField( + model_name='service', + name='min_time_regen', + field=models.DurationField(default=datetime.timedelta(0, 60), help_text='Minimal time before regeneration of the service.'), + ), + migrations.AlterField( + model_name='service', + name='regular_time_regen', + field=models.DurationField(default=datetime.timedelta(0, 3600), help_text='Maximal time before regeneration of the service.'), + ), + migrations.AlterField( + model_name='soa', + name='expire', + field=models.PositiveIntegerField(default=3600000, help_text='Seconds before the secondary DNS stop answering requests in case of primary DNS timeout'), + ), + migrations.AlterField( + model_name='soa', + name='mail', + field=models.EmailField(help_text='Contact email address for the zone', max_length=254), + ), + migrations.AlterField( + model_name='soa', + name='refresh', + field=models.PositiveIntegerField(default=86400, help_text='Seconds before the secondary DNS have to ask the primary DNS serial to detect a modification'), + ), + migrations.AlterField( + model_name='soa', + name='retry', + field=models.PositiveIntegerField(default=7200, help_text='Seconds before the secondary DNS ask the serial again in case of a primary DNS timeout'), + ), + migrations.AlterField( + model_name='soa', + name='ttl', + field=models.PositiveIntegerField(default=172800, help_text='Time to Live'), + ), + migrations.AlterField( + model_name='srv', + name='port', + field=models.PositiveIntegerField(help_text='TCP/UDP port', validators=[django.core.validators.MaxValueValidator(65535)]), + ), + migrations.AlterField( + model_name='srv', + name='priority', + field=models.PositiveIntegerField(default=0, help_text='Priority of the target server (positive integer value, the lower it is, the more the server will be used if available)', validators=[django.core.validators.MaxValueValidator(65535)]), + ), + migrations.AlterField( + model_name='srv', + name='target', + field=models.ForeignKey(help_text='Target server', on_delete=django.db.models.deletion.PROTECT, to='machines.Domain'), + ), + migrations.AlterField( + model_name='srv', + name='ttl', + field=models.PositiveIntegerField(default=172800, help_text='Time to Live'), + ), + migrations.AlterField( + model_name='srv', + name='weight', + field=models.PositiveIntegerField(default=0, help_text='Relative weight for records with the same priority (integer value between 0 and 65535)', validators=[django.core.validators.MaxValueValidator(65535)]), + ), + ] diff --git a/machines/models.py b/machines/models.py index 7be76e74..4de0b012 100644 --- a/machines/models.py +++ b/machines/models.py @@ -36,12 +36,14 @@ import hashlib import base64 from django.db import models +from django.db.models import Q from django.db.models.signals import post_save, post_delete from django.dispatch import receiver from django.forms import ValidationError from django.utils.functional import cached_property from django.utils import timezone -from django.core.validators import MaxValueValidator +from django.core.validators import MaxValueValidator, MinValueValidator +from django.utils.translation import ugettext_lazy as _ from macaddress.fields import MACAddressField @@ -55,12 +57,10 @@ import preferences.models class Machine(RevMixin, FieldPermissionModelMixin, models.Model): """ Class définissant une machine, object parent user, objets fils interfaces""" - PRETTY_NAME = "Machine" - user = models.ForeignKey('users.User', on_delete=models.PROTECT) name = models.CharField( max_length=255, - help_text="Optionnel", + help_text=_("Optional"), blank=True, null=True ) @@ -68,10 +68,12 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): class Meta: permissions = ( - ("view_machine", "Peut voir un objet machine quelquonque"), + ("view_machine", _("Can view a machine object")), ("change_machine_user", - "Peut changer le propriétaire d'une machine"), + _("Can change the user of a machine")), ) + verbose_name = _("machine") + verbose_name_plural = _("machines") @classmethod def get_instance(cls, machineid, *_args, **_kwargs): @@ -104,7 +106,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): explanation message. """ return (user_request.has_perm('machines.change_machine_user'), - "Vous ne pouvez pas modifier l'utilisateur de la machine.") + _("You don't have the right to change the machine's user.")) @staticmethod def can_view_all(user_request, *_args, **_kwargs): @@ -113,8 +115,8 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): :param user_request: instance user qui fait l'edition :return: True ou False avec la raison de l'échec le cas échéant""" if not user_request.has_perm('machines.view_machine'): - return False, (u"Vous ne pouvez pas afficher l'ensemble des " - "machines sans permission") + return False, _("You don't have the right to view all the" + " machines.") return True, None @staticmethod @@ -127,7 +129,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): try: user = users.models.User.objects.get(pk=userid) except users.models.User.DoesNotExist: - return False, u"Utilisateur inexistant" + return False, _("Nonexistent user.") max_lambdauser_interfaces = (preferences.models.OptionalMachine .get_cached_value( 'max_lambdauser_interfaces' @@ -135,14 +137,14 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): if not user_request.has_perm('machines.add_machine'): if not (preferences.models.OptionalMachine .get_cached_value('create_machine')): - return False, u"Vous ne pouvez pas ajouter une machine" + return False, (_("You don't have the right to add a machine.")) if user != user_request: - return False, (u"Vous ne pouvez pas ajouter une machine à un " - "autre user que vous sans droit") + return False, (_("You don't have the right to add a machine" + " to another user.")) if user.user_interfaces().count() >= max_lambdauser_interfaces: - return False, (u"Vous avez atteint le maximum d'interfaces " - "autorisées que vous pouvez créer vous même " - "(%s) " % max_lambdauser_interfaces) + return False, (_("You reached the maximum number of interfaces" + " that you are allowed to create yourself" + " (%s)." % max_lambdauser_interfaces)) return True, None def can_edit(self, user_request, *args, **kwargs): @@ -159,8 +161,8 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): *args, **kwargs )[0]): - return False, (u"Vous ne pouvez pas éditer une machine " - "d'un autre user que vous sans droit") + return False, (_("You don't have the right to edit a machine" + " of another user.")) return True, None def can_delete(self, user_request, *args, **kwargs): @@ -177,8 +179,8 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): *args, **kwargs )[0]): - return False, (u"Vous ne pouvez pas éditer une machine " - "d'un autre user que vous sans droit") + return False, _("You don't have the right to delete a machine" + " of another user.") return True, None def can_view(self, user_request, *_args, **_kwargs): @@ -189,10 +191,31 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): :return: True ou False avec la raison de l'échec le cas échéant""" if (not user_request.has_perm('machines.view_machine') and self.user != user_request): - return False, (u"Vous n'avez pas droit de voir les machines autre " - "que les vôtres") + return False, _("You don't have the right to view other machines" + " than yours.") return True, None + @cached_property + def short_name(self): + """Par defaut, renvoie le nom de la première interface + de cette machine""" + return str(self.interface_set.first().domain.name) + + @cached_property + def all_short_names(self): + """Renvoie de manière unique, le nom des interfaces de cette + machine""" + return Domain.objects.filter( + interface_parent__machine=self + ).values_list('name', flat=True).distinct() + + @cached_property + def all_complete_names(self): + """Renvoie tous les tls complets de la machine""" + return [str(domain) for domain in Domain.objects.filter( + Q(cname__interface_parent__machine=self) | Q(interface_parent__machine=self) + )] + def __init__(self, *args, **kwargs): super(Machine, self).__init__(*args, **kwargs) self.field_permissions = { @@ -205,8 +228,6 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): class MachineType(RevMixin, AclMixin, models.Model): """ Type de machine, relié à un type d'ip, affecté aux interfaces""" - PRETTY_NAME = "Type de machine" - type = models.CharField(max_length=255) ip_type = models.ForeignKey( 'IpType', @@ -217,10 +238,11 @@ class MachineType(RevMixin, AclMixin, models.Model): class Meta: permissions = ( - ("view_machinetype", "Peut voir un objet machinetype"), - ("use_all_machinetype", - "Peut utiliser n'importe quel type de machine"), + ("view_machinetype", _("Can view a machine type object")), + ("use_all_machinetype", _("Can use all machine types")), ) + verbose_name = _("machine type") + verbose_name_plural = _("machine types") def all_interfaces(self): """ Renvoie toutes les interfaces (cartes réseaux) de type @@ -238,8 +260,8 @@ class MachineType(RevMixin, AclMixin, models.Model): message is acces is not allowed. """ if not user_request.has_perm('machines.use_all_machinetype'): - return False, (u"Vous n'avez pas le droit d'utiliser tout types " - "de machines") + return False, (_("You don't have the right to use all machine" + " types.")) return True, None def __str__(self): @@ -248,18 +270,45 @@ class MachineType(RevMixin, AclMixin, models.Model): class IpType(RevMixin, AclMixin, models.Model): """ Type d'ip, définissant un range d'ip, affecté aux machine types""" - PRETTY_NAME = "Type d'ip" - type = models.CharField(max_length=255) extension = models.ForeignKey('Extension', on_delete=models.PROTECT) need_infra = models.BooleanField(default=False) domaine_ip_start = models.GenericIPAddressField(protocol='IPv4') domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4') + domaine_ip_network = models.GenericIPAddressField( + protocol='IPv4', + null=True, + blank=True, + help_text=_("Network containing the domain's IPv4 range (optional)") + ) + domaine_ip_netmask = models.IntegerField( + default=24, + validators=[ + MaxValueValidator(31), + MinValueValidator(8) + ], + help_text=_("Netmask for the domain's IPv4 range") + ) + reverse_v4 = models.BooleanField( + default=False, + help_text=_("Enable reverse DNS for IPv4"), + ) prefix_v6 = models.GenericIPAddressField( protocol='IPv6', null=True, blank=True ) + prefix_v6_length = models.IntegerField( + default=64, + validators=[ + MaxValueValidator(128), + MinValueValidator(0) + ] + ) + reverse_v6 = models.BooleanField( + default=False, + help_text=_("Enable reverse DNS for IPv6"), + ) vlan = models.ForeignKey( 'Vlan', on_delete=models.PROTECT, @@ -274,9 +323,11 @@ class IpType(RevMixin, AclMixin, models.Model): class Meta: permissions = ( - ("view_iptype", "Peut voir un objet iptype"), - ("use_all_iptype", "Peut utiliser tous les iptype"), + ("view_iptype", _("Can view an IP type object")), + ("use_all_iptype", _("Can use all IP types")), ) + verbose_name = _("IP type") + verbose_name_plural = ("IP types") @cached_property def ip_range(self): @@ -293,6 +344,61 @@ class IpType(RevMixin, AclMixin, models.Model): """ Renvoie une liste des ip en string""" return [str(x) for x in self.ip_set] + @cached_property + def ip_set_cidrs_as_str(self): + """Renvoie la liste des cidrs du range en str""" + return [str(ip_range) for ip_range in self.ip_set.iter_cidrs()] + + @cached_property + def ip_set_full_info(self): + """Iter sur les range cidr, et renvoie network, broacast , etc""" + return [ + { + 'network': str(ip_set.network), + 'netmask': str(ip_set.netmask), + 'netmask_cidr': str(ip_set.prefixlen), + 'broadcast': str(ip_set.broadcast), + 'vlan': str(self.vlan), + 'vlan_id': self.vlan.vlan_id + } for ip_set in self.ip_set.iter_cidrs() + ] + + @cached_property + def ip6_set_full_info(self): + if self.prefix_v6: + return { + 'network' : str(self.prefix_v6), + 'netmask' : 'ffff:ffff:ffff:ffff::', + 'netmask_cidr' : str(self.prefix_v6_length), + 'vlan': str(self.vlan), + 'vlan_id': self.vlan.vlan_id + } + else: + return None + + @cached_property + def ip_network(self): + """Renvoie le network parent du range start-stop, si spécifié + Différent de ip_set_cidrs ou iP_set, car lui est supérieur ou égal""" + if self.domaine_ip_network: + return IPNetwork(str(self.domaine_ip_network) + '/' + str(self.domaine_ip_netmask)) + return None + + @cached_property + def ip_net_full_info(self): + """Renvoie les infos du network contenant du range""" + return { + 'network' : str(self.ip_network.network), + 'netmask' : str(self.ip_network.netmask), + 'broadcast' : str(self.ip_network.broadcast), + 'netmask_cidr' : str(self.ip_network.prefixlen), + } + + @cached_property + def complete_prefixv6(self): + """Return the complete prefix v6 as cidr""" + return str(self.prefix_v6) + "/" + str(self.prefix_v6_length) + def ip_objects(self): """ Renvoie tous les objets ipv4 relié à ce type""" return IpList.objects.filter(ip_type=self) @@ -308,12 +414,9 @@ class IpType(RevMixin, AclMixin, models.Model): crée les ip une par une. Si elles existent déjà, met à jour le type associé à l'ip""" # Creation du range d'ip dans les objets iplist - networks = [] - for net in self.ip_range.cidrs(): - networks += net.iter_hosts() - ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in networks] + ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in self.ip_range] listes_ip = IpList.objects.filter( - ipv4__in=[str(ip) for ip in networks] + ipv4__in=[str(ip) for ip in self.ip_range] ) # Si il n'y a pas d'ip, on les crée if not listes_ip: @@ -327,8 +430,9 @@ class IpType(RevMixin, AclMixin, models.Model): """ Methode dépréciée, IpList est en mode cascade et supprimé automatiquement""" if Interface.objects.filter(ipv4__in=self.ip_objects()): - raise ValidationError("Une ou plusieurs ip du range sont\ - affectées, impossible de supprimer le range") + raise ValidationError(_("One or several IP addresses from the" + " range are affected, impossible to delete" + " the range.")) for ip in self.ip_objects(): ip.delete() @@ -338,12 +442,29 @@ class IpType(RevMixin, AclMixin, models.Model): return else: for ipv6 in Ipv6List.objects.filter( - interface__in=Interface.objects.filter( - type__in=MachineType.objects.filter(ip_type=self) - ) - ): + interface__in=Interface.objects.filter( + type__in=MachineType.objects.filter(ip_type=self) + ) + ): ipv6.check_and_replace_prefix(prefix=self.prefix_v6) + def get_associated_ptr_records(self): + from re2o.utils import all_active_assigned_interfaces + if self.reverse_v4: + return (all_active_assigned_interfaces() + .filter(type__ip_type=self) + .filter(ipv4__isnull=False)) + else: + return None + + def get_associated_ptr_v6_records(self): + from re2o.utils import all_active_interfaces + if self.reverse_v6: + return (all_active_interfaces(full=True) + .filter(type__ip_type=self)) + else: + return None + def clean(self): """ Nettoyage. Vérifie : - Que ip_stop est après ip_start @@ -351,19 +472,25 @@ class IpType(RevMixin, AclMixin, models.Model): - Que le range crée ne recoupe pas un range existant - Formate l'ipv6 donnée en /64""" if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop): - raise ValidationError("Domaine end doit être après start...") + raise ValidationError(_("Range end must be after range start...")) # On ne crée pas plus grand qu'un /16 if self.ip_range.size > 65536: - raise ValidationError("Le range est trop gros, vous ne devez\ - pas créer plus grand qu'un /16") + raise ValidationError(_("The range is too large, you can't create" + " a larger one than a /16.")) # On check que les / ne se recoupent pas for element in IpType.objects.all().exclude(pk=self.pk): if not self.ip_set.isdisjoint(element.ip_set): - raise ValidationError("Le range indiqué n'est pas disjoint\ - des ranges existants") + raise ValidationError(_("The specified range is not disjoint" + " from existing ranges.")) # On formate le prefix v6 if self.prefix_v6: self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network) + # On vérifie qu'un domaine network/netmask contiens bien le domaine ip start-stop + if self.domaine_ip_network: + if not self.domaine_ip_start in self.ip_network or not self.domaine_ip_stop in self.ip_network: + raise ValidationError(_("If you specify a domain network or" + " netmask, it must contain the" + " domain's IP range.")) return def save(self, *args, **kwargs): @@ -385,16 +512,16 @@ class IpType(RevMixin, AclMixin, models.Model): class Vlan(RevMixin, AclMixin, models.Model): """ Un vlan : vlan_id et nom On limite le vlan id entre 0 et 4096, comme défini par la norme""" - PRETTY_NAME = "Vlans" - vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)]) name = models.CharField(max_length=256) comment = models.CharField(max_length=256, blank=True) class Meta: permissions = ( - ("view_vlan", "Peut voir un objet vlan"), + ("view_vlan", _("Can view a VLAN object")), ) + verbose_name = _("VLAN") + verbose_name_plural = _("VLANs") def __str__(self): return self.name @@ -404,8 +531,6 @@ class Nas(RevMixin, AclMixin, models.Model): """ Les nas. Associé à un machine_type. Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour le radius. Champ autocapture de la mac à true ou false""" - PRETTY_NAME = "Correspondance entre les nas et les machines connectées" - default_mode = '802.1X' AUTH = ( ('802.1X', '802.1X'), @@ -432,8 +557,10 @@ class Nas(RevMixin, AclMixin, models.Model): class Meta: permissions = ( - ("view_nas", "Peut voir un objet Nas"), + ("view_nas", _("Can view a NAS device object")), ) + verbose_name = _("NAS device") + verbose_name_plural = _("NAS devices") def __str__(self): return self.name @@ -445,36 +572,36 @@ class SOA(RevMixin, AclMixin, models.Model): Les valeurs par défault viennent des recommandations RIPE : https://www.ripe.net/publications/docs/ripe-203 """ - PRETTY_NAME = "Enregistrement SOA" - name = models.CharField(max_length=255) mail = models.EmailField( - help_text='Email du contact pour la zone' + help_text=_("Contact email address for the zone") ) refresh = models.PositiveIntegerField( default=86400, # 24 hours - help_text='Secondes avant que les DNS secondaires doivent demander le\ - serial du DNS primaire pour détecter une modification' + help_text=_("Seconds before the secondary DNS have to ask the primary" + " DNS serial to detect a modification") ) retry = models.PositiveIntegerField( default=7200, # 2 hours - help_text='Secondes avant que les DNS secondaires fassent une nouvelle\ - demande de serial en cas de timeout du DNS primaire' + help_text=_("Seconds before the secondary DNS ask the serial again in" + " case of a primary DNS timeout") ) expire = models.PositiveIntegerField( default=3600000, # 1000 hours - help_text='Secondes après lesquelles les DNS secondaires arrêtent de\ - de répondre aux requêtes en cas de timeout du DNS primaire' + help_text=_("Seconds before the secondary DNS stop answering requests" + " in case of primary DNS timeout") ) ttl = models.PositiveIntegerField( default=172800, # 2 days - help_text='Time To Live' + help_text=_("Time to Live") ) class Meta: permissions = ( - ("view_soa", "Peut voir un objet soa"), + ("view_soa", _("Can view an SOA record object")), ) + verbose_name = _("SOA record") + verbose_name_plural = _("SOA records") def __str__(self): return str(self.name) @@ -513,7 +640,7 @@ class SOA(RevMixin, AclMixin, models.Model): /!\ Ne jamais supprimer ou renommer cette fonction car elle est utilisée dans les migrations de la BDD. """ return cls.objects.get_or_create( - name="SOA to edit", + name=_("SOA to edit"), mail="postmaser@example.com" )[0].pk @@ -521,12 +648,10 @@ class SOA(RevMixin, AclMixin, models.Model): class Extension(RevMixin, AclMixin, models.Model): """ Extension dns type example.org. Précise si tout le monde peut l'utiliser, associé à un origin (ip d'origine)""" - PRETTY_NAME = "Extensions dns" - name = models.CharField( max_length=255, unique=True, - help_text="Nom de la zone, doit commencer par un point (.example.org)" + help_text=_("Zone name, must begin with a dot (.example.org)") ) need_infra = models.BooleanField(default=False) origin = models.ForeignKey( @@ -534,13 +659,13 @@ class Extension(RevMixin, AclMixin, models.Model): on_delete=models.PROTECT, blank=True, null=True, - help_text="Enregistrement A associé à la zone" + help_text=_("A record associated with the zone") ) origin_v6 = models.GenericIPAddressField( protocol='IPv6', null=True, blank=True, - help_text="Enregistrement AAAA associé à la zone" + help_text=_("AAAA record associated with the zone") ) soa = models.ForeignKey( 'SOA', @@ -549,9 +674,11 @@ class Extension(RevMixin, AclMixin, models.Model): class Meta: permissions = ( - ("view_extension", "Peut voir un objet extension"), - ("use_all_extension", "Peut utiliser toutes les extension"), + ("view_extension", _("Can view an extension object")), + ("use_all_extension", _("Can use all extensions")), ) + verbose_name = _("DNS extension") + verbose_name_plural = _("DNS extensions") @cached_property def dns_entry(self): @@ -586,8 +713,7 @@ class Extension(RevMixin, AclMixin, models.Model): from re2o.utils import all_active_assigned_interfaces return (Domain.objects .filter(extension=self) - .filter(cname__isnull=False) - .filter(interface_parent__in=all_active_assigned_interfaces()) + .filter(cname__interface_parent__in=all_active_assigned_interfaces()) .prefetch_related('cname')) @staticmethod @@ -603,7 +729,7 @@ class Extension(RevMixin, AclMixin, models.Model): def clean(self, *args, **kwargs): if self.name and self.name[0] != '.': - raise ValidationError("Une extension doit commencer par un point") + raise ValidationError(_("An extension must begin with a dot.")) super(Extension, self).clean(*args, **kwargs) @@ -611,16 +737,16 @@ class Mx(RevMixin, AclMixin, models.Model): """ Entrées des MX. Enregistre la zone (extension) associée et la priorité Todo : pouvoir associer un MX à une interface """ - PRETTY_NAME = "Enregistrements MX" - zone = models.ForeignKey('Extension', on_delete=models.PROTECT) - priority = models.PositiveIntegerField(unique=True) - name = models.OneToOneField('Domain', on_delete=models.PROTECT) + priority = models.PositiveIntegerField() + name = models.ForeignKey('Domain', on_delete=models.PROTECT) class Meta: permissions = ( - ("view_mx", "Peut voir un objet mx"), + ("view_mx", _("Can view an MX record object")), ) + verbose_name = _("MX record") + verbose_name_plural = _("MX records") @cached_property def dns_entry(self): @@ -637,15 +763,15 @@ class Mx(RevMixin, AclMixin, models.Model): class Ns(RevMixin, AclMixin, models.Model): """Liste des enregistrements name servers par zone considéérée""" - PRETTY_NAME = "Enregistrements NS" - zone = models.ForeignKey('Extension', on_delete=models.PROTECT) ns = models.ForeignKey('Domain', on_delete=models.PROTECT) class Meta: permissions = ( - ("view_ns", "Peut voir un objet ns"), + ("view_ns", _("Can view an NS record object")), ) + verbose_name = _("NS record") + verbose_name_plural = _("NS records") @cached_property def dns_entry(self): @@ -658,16 +784,16 @@ class Ns(RevMixin, AclMixin, models.Model): class Txt(RevMixin, AclMixin, models.Model): """ Un enregistrement TXT associé à une extension""" - PRETTY_NAME = "Enregistrement TXT" - zone = models.ForeignKey('Extension', on_delete=models.PROTECT) field1 = models.CharField(max_length=255) field2 = models.TextField(max_length=2047) class Meta: permissions = ( - ("view_txt", "Peut voir un objet txt"), + ("view_txt", _("Can view a TXT record object")), ) + verbose_name = _("TXT record") + verbose_name_plural = _("TXT records") def __str__(self): return str(self.zone) + " : " + str(self.field1) + " " +\ @@ -686,10 +812,10 @@ class DName(RevMixin, AclMixin, models.Model): class Meta: permissions = ( - ("view_dname", "Can see a dname object"), + ("view_dname", _("Can view a DNAME record object")), ) - verbose_name = "DNAME entry" - verbose_name_plural = "DNAME entries" + verbose_name = _("DNAME record") + verbose_name_plural = _("DNAME records") def __str__(self): return str(self.zone) + " : " + str(self.alias) @@ -702,8 +828,6 @@ class DName(RevMixin, AclMixin, models.Model): class Srv(RevMixin, AclMixin, models.Model): """ A SRV record """ - PRETTY_NAME = "Enregistrement Srv" - TCP = 'TCP' UDP = 'UDP' @@ -713,41 +837,43 @@ class Srv(RevMixin, AclMixin, models.Model): choices=( (TCP, 'TCP'), (UDP, 'UDP'), - ), + ), default=TCP, ) extension = models.ForeignKey('Extension', on_delete=models.PROTECT) ttl = models.PositiveIntegerField( default=172800, # 2 days - help_text='Time To Live' + help_text=_("Time to Live") ) priority = models.PositiveIntegerField( default=0, validators=[MaxValueValidator(65535)], - help_text=("La priorité du serveur cible (valeur entière non " - "négative, plus elle est faible, plus ce serveur sera " - "utilisé s'il est disponible)") + help_text=_("Priority of the target server (positive integer value," + " the lower it is, the more the server will be used if" + " available)") ) weight = models.PositiveIntegerField( default=0, validators=[MaxValueValidator(65535)], - help_text="Poids relatif pour les enregistrements de même priorité\ - (valeur entière de 0 à 65535)" + help_text=_("Relative weight for records with the same priority" + " (integer value between 0 and 65535)") ) port = models.PositiveIntegerField( validators=[MaxValueValidator(65535)], - help_text="Port (tcp/udp)" + help_text=_("TCP/UDP port") ) target = models.ForeignKey( 'Domain', on_delete=models.PROTECT, - help_text="Serveur cible" + help_text=_("Target server") ) class Meta: permissions = ( - ("view_srv", "Peut voir un objet srv"), + ("view_srv", _("Can view an SRV record object")), ) + verbose_name = _("SRV record") + verbose_name_plural = _("SRV records") def __str__(self): return str(self.service) + ' ' + str(self.protocole) + ' ' +\ @@ -811,10 +937,10 @@ class SshFp(RevMixin, AclMixin, models.Model): class Meta: permissions = ( - ("view_sshfp", "Can see an SSHFP record"), + ("view_sshfp", _("Can view an SSHFP record object")), ) - verbose_name = "SSHFP record" - verbose_name_plural = "SSHFP records" + verbose_name = _("SSHFP record") + verbose_name_plural = _("SSHFP records") def can_view(self, user_request, *_args, **_kwargs): return self.machine.can_view(user_request, *_args, **_kwargs) @@ -838,15 +964,13 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): - le type parent associé au range ip et à l'extension - un objet domain associé contenant son nom - la liste des ports oiuvert""" - PRETTY_NAME = "Interface" - ipv4 = models.OneToOneField( 'IpList', on_delete=models.PROTECT, blank=True, null=True ) - mac_address = MACAddressField(integer=False, unique=True) + mac_address = MACAddressField(integer=False) machine = models.ForeignKey('Machine', on_delete=models.CASCADE) type = models.ForeignKey('MachineType', on_delete=models.PROTECT) details = models.CharField(max_length=255, blank=True) @@ -854,10 +978,12 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Meta: permissions = ( - ("view_interface", "Peut voir un objet interface"), + ("view_interface", _("Can view an interface object")), ("change_interface_machine", - "Peut changer le propriétaire d'une interface"), + _("Can change the owner of an interface")), ) + verbose_name = _("interface") + verbose_name_plural = _("interfaces") @cached_property def is_active(self): @@ -880,7 +1006,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @cached_property def gen_ipv6_dhcpv6(self): """Cree une ip, à assigner avec dhcpv6 sur une machine""" - prefix_v6 = self.type.ip_type.prefix_v6 + prefix_v6 = self.type.ip_type.prefix_v6.encode().decode('utf-8') if not prefix_v6: return None return IPv6Address( @@ -951,7 +1077,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): try: self.mac_address = str(EUI(self.mac_address)) except: - raise ValidationError("La mac donnée est invalide") + raise ValidationError(_("The given MAC address is invalid.")) def clean(self, *args, **kwargs): """ Formate l'addresse mac en mac_bare (fonction filter_mac) @@ -964,7 +1090,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): # But in our case, it's impossible to create a type value so we raise # the error. if not hasattr(self, 'type'): - raise ValidationError("Le type d'ip choisi n'est pas valide") + raise ValidationError(_("The selected IP type is invalid.")) self.filter_macaddress() self.mac_address = str(EUI(self.mac_address)) or None if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: @@ -977,8 +1103,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): if free_ips: self.ipv4 = free_ips[0] else: - raise ValidationError("Il n'y a plus d'ip disponibles\ - dans le slash") + raise ValidationError(_("There is no IP address available in the" + " slash.")) return def unassign_ipv4(self): @@ -995,8 +1121,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): # On verifie la cohérence en forçant l'extension par la méthode if self.ipv4: if self.type.ip_type != self.ipv4.ip_type: - raise ValidationError("L'ipv4 et le type de la machine ne\ - correspondent pas") + raise ValidationError(_("The IPv4 address and the machine type" + " don't match.")) super(Interface, self).save(*args, **kwargs) @staticmethod @@ -1009,23 +1135,23 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): try: machine = Machine.objects.get(pk=machineid) except Machine.DoesNotExist: - return False, u"Machine inexistante" + return False, _("Nonexistent machine.") if not user_request.has_perm('machines.add_interface'): if not (preferences.models.OptionalMachine .get_cached_value('create_machine')): - return False, u"Vous ne pouvez pas ajouter une machine" + return False, _("You can't add a machine.") max_lambdauser_interfaces = (preferences.models.OptionalMachine .get_cached_value( 'max_lambdauser_interfaces' )) if machine.user != user_request: - return False, u"Vous ne pouvez pas ajouter une interface à une\ - machine d'un autre user que vous sans droit" + return False, _("You don't have the right to add an interface" + " to a machine of another user.") if (machine.user.user_interfaces().count() >= max_lambdauser_interfaces): - return False, u"Vous avez atteint le maximum d'interfaces\ - autorisées que vous pouvez créer vous même (%s) "\ - % max_lambdauser_interfaces + return False, (_("You reached the maximum number of interfaces" + " that you are allowed to create yourself" + " (%s)." % max_lambdauser_interfaces)) return True, None @staticmethod @@ -1033,7 +1159,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """Check if a user can change the machine associated with an Interface object """ return (user_request.has_perm('machines.change_interface_machine'), - "Droit requis pour changer la machine") + _("Permission required to edit the machine.")) def can_edit(self, user_request, *args, **kwargs): """Verifie que l'user a les bons droits infra pour editer @@ -1048,8 +1174,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): *args, **kwargs )[0]): - return False, (u"Vous ne pouvez pas éditer une machine " - "d'un autre user que vous sans droit") + return False, _("You don't have the right to edit a machine of" + " another user.") return True, None def can_delete(self, user_request, *args, **kwargs): @@ -1065,8 +1191,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): *args, **kwargs )[0]): - return False, (u"Vous ne pouvez pas éditer une machine " - "d'un autre user que vous sans droit") + return False, _("You don't have the right to edit a machine of" + " another user.") return True, None def can_view(self, user_request, *_args, **_kwargs): @@ -1077,8 +1203,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): :return: True ou False avec la raison de l'échec le cas échéant""" if (not user_request.has_perm('machines.view_interface') and self.machine.user != user_request): - return False, (u"Vous n'avez pas le droit de voir des machines " - "autre que les vôtres") + return False, _("You don't have the right to view machines other" + " than yours.") return True, None def __init__(self, *args, **kwargs): @@ -1110,11 +1236,9 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ A list of IPv6 """ - PRETTY_NAME = 'Enregistrements Ipv6 des machines' ipv6 = models.GenericIPAddressField( protocol='IPv6', - unique=True ) interface = models.ForeignKey( 'Interface', @@ -1125,10 +1249,12 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Meta: permissions = ( - ("view_ipv6list", "Peut voir un objet ipv6"), - ("change_ipv6list_slaac_ip", - "Peut changer la valeur slaac sur une ipv6"), + ("view_ipv6list", _("Can view an IPv6 addresses list object")), + ("change_ipv6list_slaac_ip", _("Can change the SLAAC value of an" + " IPv6 addresses list")), ) + verbose_name = _("IPv6 addresses list") + verbose_name_plural = _("IPv6 addresses lists") @staticmethod def can_create(user_request, interfaceid, *_args, **_kwargs): @@ -1140,18 +1266,19 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): try: interface = Interface.objects.get(pk=interfaceid) except Interface.DoesNotExist: - return False, u"Interface inexistante" + return False, _("Nonexistent interface.") if not user_request.has_perm('machines.add_ipv6list'): if interface.machine.user != user_request: - return False, u"Vous ne pouvez pas ajouter un alias à une\ - machine d'un autre user que vous sans droit" + return False, _("You don't have the right to add an alias to a" + " machine of another user.") return True, None @staticmethod def can_change_slaac_ip(user_request, *_args, **_kwargs): """ Check if a user can change the slaac value """ return (user_request.has_perm('machines.change_ipv6list_slaac_ip'), - "Droit requis pour changer la valeur slaac ip") + _("Permission required to change the SLAAC value of an IPv6" + " address")) def can_edit(self, user_request, *args, **kwargs): """Verifie que l'user a les bons droits infra pour editer @@ -1166,8 +1293,8 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): *args, **kwargs )[0]): - return False, (u"Vous ne pouvez pas éditer une machine " - "d'un autre user que vous sans droit") + return False, _("You don't have the right to edit a machine of" + " another user.") return True, None def can_delete(self, user_request, *args, **kwargs): @@ -1183,8 +1310,8 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): *args, **kwargs )[0]): - return False, (u"Vous ne pouvez pas éditer une machine " - "d'un autre user que vous sans droit") + return False, _("You don't have the right to edit a machine of" + " another user.") return True, None def can_view(self, user_request, *_args, **_kwargs): @@ -1195,8 +1322,8 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): :return: True ou False avec la raison de l'échec le cas échéant""" if (not user_request.has_perm('machines.view_ipv6list') and self.interface.machine.user != user_request): - return False, (u"Vous n'avez pas le droit de voir des machines " - "autre que les vôtres") + return False, _("You don't have the right to view machines other" + " than yours.") return True, None def __init__(self, *args, **kwargs): @@ -1207,14 +1334,14 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): def check_and_replace_prefix(self, prefix=None): """Si le prefixe v6 est incorrect, on maj l'ipv6""" - prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6 + prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') if not prefix_v6: return - if (IPv6Address(self.ipv6).exploded[:20] != + if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]): self.ipv6 = IPv6Address( IPv6Address(prefix_v6).exploded[:20] + - IPv6Address(self.ipv6).exploded[20:] + IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[20:] ) self.save() @@ -1222,15 +1349,14 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): if self.slaac_ip and (Ipv6List.objects .filter(interface=self.interface, slaac_ip=True) .exclude(id=self.id)): - raise ValidationError("Une ip slaac est déjà enregistrée") - prefix_v6 = self.interface.type.ip_type.prefix_v6 + raise ValidationError(_("A SLAAC IP address is already registered.")) + prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') if prefix_v6: - if (IPv6Address(self.ipv6).exploded[:20] != + if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]): - raise ValidationError( - "Le prefixv6 est incorrect et ne correspond pas au type " - "associé à la machine" - ) + raise ValidationError(_("The v6 prefix is incorrect and" + " doesn't match the type associated" + " with the machine.")) super(Ipv6List, self).clean(*args, **kwargs) def save(self, *args, **kwargs): @@ -1246,7 +1372,6 @@ class Domain(RevMixin, AclMixin, models.Model): """ Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les alias et les nom de machines, suivant si interface_parent ou cname sont remplis""" - PRETTY_NAME = "Domaine dns" interface_parent = models.OneToOneField( 'Interface', @@ -1255,7 +1380,7 @@ class Domain(RevMixin, AclMixin, models.Model): null=True ) name = models.CharField( - help_text="Obligatoire et unique, ne doit pas comporter de points", + help_text=_("Mandatory and unique, must not contain dots."), max_length=255 ) extension = models.ForeignKey('Extension', on_delete=models.PROTECT) @@ -1269,8 +1394,10 @@ class Domain(RevMixin, AclMixin, models.Model): class Meta: unique_together = (("name", "extension"),) permissions = ( - ("view_domain", "Peut voir un objet domain"), + ("view_domain", _("Can view a domain object")), ) + verbose_name = _("domain") + verbose_name_plural = _("domains") def get_extension(self): """ Retourne l'extension de l'interface parente si c'est un A @@ -1292,20 +1419,22 @@ class Domain(RevMixin, AclMixin, models.Model): if self.get_extension(): self.extension = self.get_extension() if self.interface_parent and self.cname: - raise ValidationError("On ne peut créer à la fois A et CNAME") + raise ValidationError(_("You can't create a both A and CNAME" + " record.")) if self.cname == self: - raise ValidationError("On ne peut créer un cname sur lui même") + raise ValidationError(_("You can't create a CNAME record pointing" + " to itself.")) HOSTNAME_LABEL_PATTERN = re.compile( r"(?!-)[A-Z\d-]+(? 63: - raise ValidationError("Le nom de domaine %s est trop long\ - (maximum de 63 caractères)." % dns) + raise ValidationError(_("The domain name %s is too long (over 63" + " characters).") % dns) if not HOSTNAME_LABEL_PATTERN.match(dns): - raise ValidationError("Ce nom de domaine %s contient des\ - carractères interdits." % dns) + raise ValidationError(_("The domain name %s contains forbidden" + " characters.") % dns) self.validate_unique() super(Domain, self).clean() @@ -1322,7 +1451,7 @@ class Domain(RevMixin, AclMixin, models.Model): """ Empèche le save sans extension valide. Force à avoir appellé clean avant""" if not self.get_extension(): - raise ValidationError("Extension invalide") + raise ValidationError(_("Invalid extension.")) self.full_clean() super(Domain, self).save(*args, **kwargs) @@ -1348,24 +1477,24 @@ class Domain(RevMixin, AclMixin, models.Model): try: interface = Interface.objects.get(pk=interfaceid) except Interface.DoesNotExist: - return False, u"Interface inexistante" + return False, _("Nonexistent interface.") if not user_request.has_perm('machines.add_domain'): max_lambdauser_aliases = (preferences.models.OptionalMachine .get_cached_value( 'max_lambdauser_aliases' )) if interface.machine.user != user_request: - return False, (u"Vous ne pouvez pas ajouter un alias à une " - "machine d'un autre user que vous sans droit") + return False, _("You don't have the right to add an alias to a" + " machine of another user.") if Domain.objects.filter( cname__in=Domain.objects.filter( interface_parent__in=(interface.machine.user .user_interfaces()) ) ).count() >= max_lambdauser_aliases: - return False, (u"Vous avez atteint le maximum d'alias " - "autorisés que vous pouvez créer vous même " - "(%s) " % max_lambdauser_aliases) + return False, _("You reached the maximum number of alias that" + " you are allowed to create yourself (%s). " + % max_lambdauser_aliases) return True, None def can_edit(self, user_request, *_args, **_kwargs): @@ -1376,8 +1505,8 @@ class Domain(RevMixin, AclMixin, models.Model): :return: soit True, soit False avec la raison de l'échec""" if (not user_request.has_perm('machines.change_domain') and self.get_source_interface.machine.user != user_request): - return False, (u"Vous ne pouvez pas editer un alias à une machine " - "d'un autre user que vous sans droit") + return False, _("You don't have the right to edit an alias of a" + " machine of another user.") return True, None def can_delete(self, user_request, *_args, **_kwargs): @@ -1388,8 +1517,8 @@ class Domain(RevMixin, AclMixin, models.Model): :return: soit True, soit False avec la raison de l'échec""" if (not user_request.has_perm('machines.delete_domain') and self.get_source_interface.machine.user != user_request): - return False, (u"Vous ne pouvez pas supprimer un alias à une " - "machine d'un autre user que vous sans droit") + return False, _("You don't have the right to delete an alias of a" + " machine of another user.") return True, None def can_view(self, user_request, *_args, **_kwargs): @@ -1400,8 +1529,8 @@ class Domain(RevMixin, AclMixin, models.Model): :return: True ou False avec la raison de l'échec le cas échéant""" if (not user_request.has_perm('machines.view_domain') and self.get_source_interface.machine.user != user_request): - return False, (u"Vous n'avez pas le droit de voir des machines " - "autre que les vôtres") + return False, _("You don't have the right to view machines other" + " than yours.") return True, None def __str__(self): @@ -1410,15 +1539,16 @@ class Domain(RevMixin, AclMixin, models.Model): class IpList(RevMixin, AclMixin, models.Model): """ A list of IPv4 """ - PRETTY_NAME = "Addresses ipv4" ipv4 = models.GenericIPAddressField(protocol='IPv4', unique=True) ip_type = models.ForeignKey('IpType', on_delete=models.CASCADE) class Meta: permissions = ( - ("view_iplist", "Peut voir un objet iplist"), + ("view_iplist", _("Can view an IPv4 addresses list object")), ) + verbose_name = _("IPv4 addresses list") + verbose_name_plural = _("IPv4 addresses lists") @cached_property def need_infra(self): @@ -1429,8 +1559,8 @@ class IpList(RevMixin, AclMixin, models.Model): def clean(self): """ Erreur si l'ip_type est incorrect""" if not str(self.ipv4) in self.ip_type.ip_set_as_str: - raise ValidationError("L'ipv4 et le range de l'iptype ne\ - correspondent pas!") + raise ValidationError(_("The IPv4 address and the range of the IP" + " type don't match.")) return def save(self, *args, **kwargs): @@ -1441,25 +1571,96 @@ class IpList(RevMixin, AclMixin, models.Model): return self.ipv4 +class Role(RevMixin, AclMixin, models.Model): + """Define the role of a machine. + Allow automated generation of the server configuration. + """ + + ROLE = ( + ('dhcp-server', _("DHCP server")), + ('switch-conf-server', _("Switches configuration server")), + ('dns-recursif-server', _("Recursive DNS server")), + ('ntp-server', _("NTP server")), + ('radius-server', _("RADIUS server")), + ('log-server', _("Log server")), + ('ldap-master-server', _("LDAP master server")), + ('ldap-backup-server', _("LDAP backup server")), + ('smtp-server', _("SMTP server")), + ('postgresql-server', _("postgreSQL server")), + ('mysql-server', _("mySQL server")), + ('sql-client', _("SQL client")), + ('gateway', _("Gateway")), + ) + + role_type = models.CharField(max_length=255, unique=True) + servers = models.ManyToManyField('Interface') + specific_role = models.CharField( + choices=ROLE, + null=True, + blank=True, + max_length=32, + ) + + class Meta: + permissions = ( + ("view_role", _("Can view a role object")), + ) + verbose_name = _("server role") + verbose_name_plural = _("server roles") + + @classmethod + def get_instance(cls, roleid, *_args, **_kwargs): + """Get the Role instance with roleid. + + Args: + roleid: The id + + Returns: + The role. + """ + return cls.objects.get(pk=roleid) + + @classmethod + def interface_for_roletype(cls, roletype): + """Return interfaces for a roletype""" + return Interface.objects.filter( + role=cls.objects.filter(specific_role=roletype) + ) + + @classmethod + def all_interfaces_for_roletype(cls, roletype): + """Return all interfaces for a roletype""" + return Interface.objects.filter( + machine__interface__role=cls.objects.filter(specific_role=roletype) + ) + + def save(self, *args, **kwargs): + super(Role, self).save(*args, **kwargs) + + def __str__(self): + return str(self.role_type) + + class Service(RevMixin, AclMixin, models.Model): """ Definition d'un service (dhcp, dns, etc)""" - PRETTY_NAME = "Services à générer (dhcp, dns, etc)" service_type = models.CharField(max_length=255, blank=True, unique=True) min_time_regen = models.DurationField( default=timedelta(minutes=1), - help_text="Temps minimal avant nouvelle génération du service" + help_text=_("Minimal time before regeneration of the service.") ) regular_time_regen = models.DurationField( default=timedelta(hours=1), - help_text="Temps maximal avant nouvelle génération du service" + help_text=_("Maximal time before regeneration of the service.") ) servers = models.ManyToManyField('Interface', through='Service_link') class Meta: permissions = ( - ("view_service", "Peut voir un objet service"), + ("view_service", _("Can view a service object")), ) + verbose_name = _("service to generate (DHCP, DNS, ...)") + verbose_name_plural = _("services to generate (DHCP, DNS, ...)") def ask_regen(self): """ Marque à True la demande de régénération pour un service x """ @@ -1471,8 +1672,8 @@ class Service(RevMixin, AclMixin, models.Model): """ Django ne peut créer lui meme les relations manytomany avec table intermediaire explicite""" for serv in servers.exclude( - pk__in=Interface.objects.filter(service=self) - ): + pk__in=Interface.objects.filter(service=self) + ): link = Service_link(service=self, server=serv) link.save() Service_link.objects.filter(service=self).exclude(server__in=servers)\ @@ -1497,7 +1698,6 @@ def regen(service): class Service_link(RevMixin, AclMixin, models.Model): """ Definition du lien entre serveurs et services""" - PRETTY_NAME = "Relation entre service et serveur" service = models.ForeignKey('Service', on_delete=models.CASCADE) server = models.ForeignKey('Interface', on_delete=models.CASCADE) @@ -1506,8 +1706,10 @@ class Service_link(RevMixin, AclMixin, models.Model): class Meta: permissions = ( - ("view_service_link", "Peut voir un objet service_link"), + ("view_service_link", _("Can view a service server link object")), ) + verbose_name = _("link between service and server") + verbose_name_plural = _("links between service and server") def done_regen(self): """ Appellé lorsqu'un serveur a regénéré son service""" @@ -1547,17 +1749,19 @@ class Service_link(RevMixin, AclMixin, models.Model): class OuverturePortList(RevMixin, AclMixin, models.Model): """Liste des ports ouverts sur une interface.""" - PRETTY_NAME = "Profil d'ouverture de ports" name = models.CharField( - help_text="Nom de la configuration des ports.", + help_text=_("Name of the ports configuration"), max_length=255 ) class Meta: permissions = ( - ("view_ouvertureportlist", "Peut voir un objet ouvertureport"), + ("view_ouvertureportlist", _("Can view a ports opening list" + " object")), ) + verbose_name = _("ports opening list") + verbose_name_plural = _("ports opening lists") def can_delete(self, user_request, *_args, **_kwargs): """Verifie que l'user a les bons droits bureau pour delete @@ -1566,10 +1770,10 @@ class OuverturePortList(RevMixin, AclMixin, models.Model): :param user_request: Utilisateur qui fait la requête :return: soit True, soit False avec la raison de l'échec""" if not user_request.has_perm('machines.delete_ouvertureportlist'): - return False, (u"Vous n'avez pas le droit de supprimer une " - "ouverture de port") + return False, _("You don't have the right to delete a ports" + " opening list.") if self.interface_set.all(): - return False, u"Cette liste de ports est utilisée" + return False, _("This ports opening list is used.") return True, None def __str__(self): @@ -1613,7 +1817,6 @@ class OuverturePort(RevMixin, AclMixin, models.Model): On limite les ports entre 0 et 65535, tels que défini par la RFC """ - PRETTY_NAME = "Plage de port ouverte" TCP = 'T' UDP = 'U' @@ -1630,7 +1833,7 @@ class OuverturePort(RevMixin, AclMixin, models.Model): choices=( (TCP, 'TCP'), (UDP, 'UDP'), - ), + ), default=TCP, ) io = models.CharField( @@ -1638,14 +1841,18 @@ class OuverturePort(RevMixin, AclMixin, models.Model): choices=( (IN, 'IN'), (OUT, 'OUT'), - ), + ), default=OUT, ) + + class Meta: + verbose_name = _("ports opening") + verbose_name = _("ports openings") def __str__(self): if self.begin == self.end: return str(self.begin) - return '-'.join([str(self.begin), str(self.end)]) + return ':'.join([str(self.begin), str(self.end)]) def show_port(self): """Formatage plus joli, alias pour str""" @@ -1806,3 +2013,4 @@ def srv_post_save(**_kwargs): def srv_post_delete(**_kwargs): """Regeneration dns après modification d'un SRV""" regen('dns') + diff --git a/machines/templates/machines/aff_alias.html b/machines/templates/machines/aff_alias.html index 184db6f4..17266a6e 100644 --- a/machines/templates/machines/aff_alias.html +++ b/machines/templates/machines/aff_alias.html @@ -23,25 +23,26 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load i18n %} {% load logs_extra %} - + {% for alias in alias_list %} - - - - + + + + {% endfor %}
Alias{% trans "Aliases" %}
{{ alias }} - {% can_edit alias %} - {% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %} - {% acl_end %} - {% history_button alias %} -
{{ alias }} + {% can_edit alias %} + {% include 'buttons/edit.html' with href='machines:edit-alias' id=alias.id %} + {% acl_end %} + {% history_button alias %} +
diff --git a/machines/templates/machines/aff_dname.html b/machines/templates/machines/aff_dname.html index 8797be72..7e043d7c 100644 --- a/machines/templates/machines/aff_dname.html +++ b/machines/templates/machines/aff_dname.html @@ -22,16 +22,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load logs_extra %} +{% load i18n %} - - - - - - - - - {% for dname in dname_list %} +
Target zoneRecord
+ + + + + + + + {% for dname in dname_list %} @@ -39,10 +40,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% can_edit dname %} {% include 'buttons/edit.html' with href='machines:edit-dname' id=dname.id %} {% acl_end %} - {% history_button dname %} + {% history_button dname %} - {% endfor %} -
{% trans "Target zone" %}{% trans "Record" %}
{{ dname.zone }} {{ dname.dns_entry }}
- + {% endfor %} + diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index ba444eca..43bb9e39 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -25,17 +25,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load logs_extra %} {% load design %} +{% load i18n %}
- - - - + + + + {% if ipv6_enabled %} - + {% endif %} @@ -44,7 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc., - + {% if ipv6_enabled %} @@ -59,3 +60,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
ExtensionDroit infra pour utiliser ?Enregistrement SOAEnregistrement A origin{% trans "Extension" %}{% trans "'infra' right required" %}{% trans "SOA record" %}{% trans "A record origin" %}Enregistrement AAAA origin{% trans "AAAA record origin" %}
{{ extension.name }} {{ extension.need_infra|tick }}{{ extension.soa}}{{ extension.soa }} {{ extension.origin }}{{ extension.origin_v6 }}
+ diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html index fa2a2767..b8ed5293 100644 --- a/machines/templates/machines/aff_iptype.html +++ b/machines/templates/machines/aff_iptype.html @@ -26,18 +26,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load logs_extra %} +{% load i18n %} +
- - - - - - - - + + + + + + + + @@ -46,8 +48,9 @@ with this program; if not, write to the Free Software Foundation, Inc., - - + + +
Type d'ipExtensionNécessite l'autorisation infraPlage ipv4Préfixe v6Sur vlanOuverture ports par défault{% trans "IP type" %}{% trans "Extension" %}{% trans "'infra' right required" %}{% trans "IPv4 range" %}{% trans "v6 prefix" %}{% trans "DNSSEC reverse v4/v6" %}{% trans "On VLAN(s)" %}{% trans "Default ports opening" %}
{{ type.type }} {{ type.extension }} {{ type.need_infra|tick }}{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}{{ type.prefix_v6 }}{{ type.domaine_ip_start }}-{{ type.domaine_ip_stop }}{% if type.ip_network %} on {{ type.ip_network }}{% endif %}{{ type.prefix_v6 }}/{{ type.prefix_v6_length }}{{ type.reverse_v4|tick }}/{{ type.reverse_v6|tick }} {{ type.vlan }} {{ type.ouverture_ports }} @@ -60,3 +63,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
+ diff --git a/machines/templates/machines/aff_ipv6.html b/machines/templates/machines/aff_ipv6.html index d5323f61..a98c0327 100644 --- a/machines/templates/machines/aff_ipv6.html +++ b/machines/templates/machines/aff_ipv6.html @@ -24,29 +24,30 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load logs_extra %} +{% load i18n %} - - + + - + {% for ipv6 in ipv6_list %} - - - - - + + + + + {% endfor %}
Ipv6Slaac{% trans "IPv6 addresses" %}{% trans "SLAAC" %}
{{ ipv6.ipv6 }}{{ ipv6.slaac_ip }} - {% can_edit ipv6 %} - {% include 'buttons/edit.html' with href='machines:edit-ipv6list' id=ipv6.id %} - {% acl_end %} - {% can_delete ipv6 %} - {% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %} - {% acl_end %} - {% history_button ipv6 %} -
{{ ipv6.ipv6 }}{{ ipv6.slaac_ip }} + {% can_edit ipv6 %} + {% include 'buttons/edit.html' with href='machines:edit-ipv6list' id=ipv6.id %} + {% acl_end %} + {% can_delete ipv6 %} + {% include 'buttons/suppr.html' with href='machines:del-ipv6list' id=ipv6.id %} + {% acl_end %} + {% history_button ipv6 %} +
diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index ba736f10..33ae617a 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load logs_extra %} +{% load i18n %}
{% if machines_list.paginator %} @@ -39,23 +40,27 @@ with this program; if not, write to the Free Software Foundation, Inc., - {% include "buttons/sort.html" with prefix='machine' col='name' text='Nom DNS' %} - Type - MAC - IP - Actions + {% trans "DNS name" as tr_dns_name %} + {% include "buttons/sort.html" with prefix='machine' col='name' text=tr_dns_name %} + {% trans "Type" %} + {% trans "MAC address" %} + {% trans "IP address" %} + {% trans "Actions" %} - {% for machine in machines_list %} + {% for machine in machines_list %} - {{ machine.name|default:'Pas de nom' }} - + {% trans "No name" as tr_no_name %} + {% trans "View the profile" as tr_view_the_profile %} + {{ machine.name|default:tr_no_name }} + {{ machine.user }} {% can_create Interface machine.id %} - {% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc='Ajouter une interface' %} + {% trans "Create an interface" as tr_create_an_interface %} + {% include 'buttons/add.html' with href='machines:new-interface' id=machine.id desc=tr_create_an_interface %} {% acl_end %} {% history_button machine %} {% can_delete machine %} @@ -68,8 +73,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if interface.domain.related_domain.all %} {{ interface.domain }} - {% else %} {{ interface.domain }} @@ -77,7 +82,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ interface.type }} - + {{ interface.mac_address }} @@ -86,8 +91,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if ipv6_enabled and interface.ipv6 != 'None'%} IPv6 - {% endif %} @@ -97,39 +102,44 @@ with this program; if not, write to the Free Software Foundation, Inc., -
- {% if ipv6_enabled and interface.ipv6 != 'None'%}
    - {% for ipv6 in interface.ipv6.all %} + {% for ipv6 in interface.ipv6.all %}
  • - {{ipv6}} + {{ ipv6 }}
  • - {% endfor %} + {% endfor %}
- - {% endif %} - - - {% if interface.domain.related_domain.all %} - - -
-
    - {% for al in interface.domain.related_domain.all %} -
  • - - {{ al }} - - -
  • - {% endfor %} -
-
- - - {% endif %} - {% endfor %} - - - - {% endfor %} + + {% endif %} + {% if interface.domain.related_domain.all %} + + +
+
    + {% for al in interface.domain.related_domain.all %} +
  • + + {{ al }} + + +
  • + {% endfor %} +
+
+ + + {% endif %} + {% endfor %} + + + + {% endfor %} + - - {% endblock %} + diff --git a/machines/templates/machines/index.html b/machines/templates/machines/index.html index 3d85dd59..509334a0 100644 --- a/machines/templates/machines/index.html +++ b/machines/templates/machines/index.html @@ -24,11 +24,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Machines

+

{% trans "Machines" %}

{% include "machines/aff_machines.html" with machines_list=machines_list %}

diff --git a/machines/templates/machines/index_alias.html b/machines/templates/machines/index_alias.html index 07750754..2d33177f 100644 --- a/machines/templates/machines/index_alias.html +++ b/machines/templates/machines/index_alias.html @@ -24,16 +24,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des alias de l'interface

- Ajouter un alias - Supprimer un ou plusieurs alias - {% include "machines/aff_alias.html" with alias_list=alias_list %} -
-
-
+

{% trans "List of the aliases of the interface" %}

+ {% trans " Add an alias" %} + {% trans " Delete one or several aliases" %} + {% include "machines/aff_alias.html" with alias_list=alias_list %} +
+
+
{% endblock %} diff --git a/machines/templates/machines/index_extension.html b/machines/templates/machines/index_extension.html index 29c5a0bd..6669d197 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -28,57 +28,60 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load acl %} {% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des extensions

+

{% trans "List of extensions" %}

{% can_create Extension %} - Ajouter une extension + {% trans " Add an extension" %} {% acl_end %} - Supprimer une ou plusieurs extensions + {% trans " Delete one or several extensions" %} {% include "machines/aff_extension.html" with extension_list=extension_list %} -

Liste des enregistrements SOA

+

{% trans "List of SOA records" %}

{% can_create SOA %} - Ajouter un enregistrement SOA + {% trans " Add an SOA record" %} {% acl_end %} - Supprimer un enregistrement SOA + {% trans " Delete one or several SOA records" %} {% include "machines/aff_soa.html" with soa_list=soa_list %} -

Liste des enregistrements MX

+ +

{% trans "List of MX records" %}

{% can_create Mx %} - Ajouter un enregistrement MX + {% trans " Add an MX record" %} {% acl_end %} - Supprimer un enregistrement MX + {% trans " Delete one or several MX records" %} {% include "machines/aff_mx.html" with mx_list=mx_list %} -

Liste des enregistrements NS

+ +

{% trans "List of NS records" %}

{% can_create Ns %} - Ajouter un enregistrement NS + {% trans " Add an NS record" %} {% acl_end %} - Supprimer un enregistrement NS + {% trans " Delete one or several NS records" %} {% include "machines/aff_ns.html" with ns_list=ns_list %} -

Liste des enregistrements TXT

+ +

{% trans "List of TXT records" %}

{% can_create Txt %} - Ajouter un enregistrement TXT + {% trans " Add a TXT record" %} {% acl_end %} - Supprimer un enregistrement TXT + {% trans " Delete one or several TXT records" %} {% include "machines/aff_txt.html" with txt_list=txt_list %} -

DNAME records

+

{% trans "List of DNAME records" %}

{% can_create DName %} - {% trans "Add a DNAME record" %} + {% trans " Add a DNAME record" %} {% acl_end %} - {% trans "Delete DNAME records" %} + {% trans " Delete one or several DNAME records" %} {% include "machines/aff_dname.html" with dname_list=dname_list %} -

Liste des enregistrements SRV

+

{% trans "List of SRV records" %}

{% can_create Srv %} - Ajouter un enregistrement SRV + {% trans " Add an SRV record" %} {% acl_end %} - Supprimer un enregistrement SRV + {% trans " Delete one or several SRV records" %} {% include "machines/aff_srv.html" with srv_list=srv_list %}

diff --git a/machines/templates/machines/index_iptype.html b/machines/templates/machines/index_iptype.html index 4dacf96e..5be4561d 100644 --- a/machines/templates/machines/index_iptype.html +++ b/machines/templates/machines/index_iptype.html @@ -26,15 +26,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Ip{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des types d'ip

+

{% trans "List of IP types" %}

{% can_create IpType %} - Ajouter un type d'ip + {% trans " Add an IP type" %} {% acl_end %} - Supprimer un ou plusieurs types d'ip + {% trans " Delete one or several IP types" %} {% include "machines/aff_iptype.html" with iptype_list=iptype_list %}

diff --git a/machines/templates/machines/index_ipv6.html b/machines/templates/machines/index_ipv6.html index 584dc00a..06e287e2 100644 --- a/machines/templates/machines/index_ipv6.html +++ b/machines/templates/machines/index_ipv6.html @@ -25,13 +25,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des ipv6 de l'interface

+

{% trans "List of the IPv6 addresses of the interface" %}

{% can_create Ipv6List interface_id %} - Ajouter une ipv6 + {% trans " Add an IPv6 address" %} {% acl_end %} {% include "machines/aff_ipv6.html" with ipv6_list=ipv6_list %}
diff --git a/machines/templates/machines/index_machinetype.html b/machines/templates/machines/index_machinetype.html index a0bfd7e5..de1e2f58 100644 --- a/machines/templates/machines/index_machinetype.html +++ b/machines/templates/machines/index_machinetype.html @@ -26,15 +26,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des types de machines

+

{% trans "List of machine types" %}

{% can_create MachineType %} - Ajouter un type de machine + {% trans " Add a machine type" %} {% acl_end %} - Supprimer un ou plusieurs types de machines + {% trans " Delete one or several machine types" %} {% include "machines/aff_machinetype.html" with machinetype_list=machinetype_list %}

diff --git a/machines/templates/machines/index_nas.html b/machines/templates/machines/index_nas.html index 3f1cb90f..88f68213 100644 --- a/machines/templates/machines/index_nas.html +++ b/machines/templates/machines/index_nas.html @@ -26,17 +26,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des nas

-
La correpondance nas-machinetype relie le type de nas à un type de machine. - Elle est utile pour l'autoenregistrement des macs par radius, et permet de choisir le type de machine à affecter aux machines en fonction du type de nas
+

{% trans "List of NAS devices" %}

+
{% trans "The NAS device type and machine type are linked. It is useful for MAC address auto capture by RADIUS, and allows to choose the machine type to assign to the machines according to the NAS device type." %}
{% can_create Nas %} - Ajouter un type de nas + {% trans " Add a NAS device type" %} {% acl_end %} - Supprimer un ou plusieurs types nas + {% trans " Delete one or several NAS device types" %} {% include "machines/aff_nas.html" with nas_list=nas_list %}

diff --git a/machines/templates/machines/index_portlist.html b/machines/templates/machines/index_portlist.html index e505ad43..0d3d0741 100644 --- a/machines/templates/machines/index_portlist.html +++ b/machines/templates/machines/index_portlist.html @@ -3,23 +3,24 @@ {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Configuration de ports{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des configurations de ports

+

{% trans "List of ports configurations" %}

{% can_create OuverturePortList %} - Ajouter une configuration + {% trans " Add a configuration" %} {% acl_end %} - - - - - - + + + + + + @@ -48,13 +49,13 @@ {% endif %} + {% can_delete pl %} + {% include 'buttons/suppr.html' with href='machines:del-portlist' id=pl.id %} + {% acl_end %} + {%endfor%}
NomTCP (entrée)TCP (sortie)UDP (entrée)UDP (sortie)Machines{% trans "Name" %}{% trans "TCP (input)" %}{% trans "TCP (output)" %}{% trans "UDP (input)" %}{% trans "UDP (output)" %}{% trans "Machines" %}
- {% can_delete pl %} - {% include 'buttons/suppr.html' with href='machines:del-portlist' id=pl.id %} - {% acl_end %} {% can_edit pl %} {% include 'buttons/edit.html' with href='machines:edit-portlist' id=pl.id %} {% acl_end %} -
@@ -63,3 +64,4 @@
{% endblock %} + diff --git a/machines/templates/machines/index_role.html b/machines/templates/machines/index_role.html new file mode 100644 index 00000000..ddc2ea8b --- /dev/null +++ b/machines/templates/machines/index_role.html @@ -0,0 +1,42 @@ +{% extends "machines/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 Gabriel Détraz +Copyright © 2017 Goulven Kermarec +Copyright © 2017 Augustin Lemesle + +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 acl %} +{% load i18n %} + +{% block title %}{% trans "Machines" %}{% endblock %} + +{% block content %} +

{% trans "List of roles" %}

+ {% can_create Role %} + {% trans " Add a role"%} + {% acl_end %} + {% trans " Delete one or several roles" %} + {% include "machines/aff_role.html" with role_list=role_list %} +
+
+{% endblock %} + diff --git a/machines/templates/machines/index_service.html b/machines/templates/machines/index_service.html index 3bc88189..9dec7032 100644 --- a/machines/templates/machines/index_service.html +++ b/machines/templates/machines/index_service.html @@ -25,17 +25,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des services

+

{% trans "List of services" %}

{% can_create machines.Service %} - Ajouter un service + {% trans " Add a service" %} {% acl_end %} - Supprimer un ou plusieurs service + {% trans " Delete one or several services" %} {% include "machines/aff_service.html" with service_list=service_list %} -

Etat des serveurs

+

{% trans "States of servers" %}

{% include "machines/aff_servers.html" with servers_list=servers_list %}

diff --git a/machines/templates/machines/index_sshfp.html b/machines/templates/machines/index_sshfp.html index 2c8d1581..ce16d621 100644 --- a/machines/templates/machines/index_sshfp.html +++ b/machines/templates/machines/index_sshfp.html @@ -23,16 +23,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

SSH fingerprints

-{% can_create SshFp machine_id %} - - Add an SSH fingerprint - -{% acl_end %} -{% include "machines/aff_sshfp.html" with sshfp_list=sshfp_list %} +

{% trans "SSH fingerprints" %}

+ {% can_create SshFp machine_id %} + + {% trans " Add an SSH fingerprint" %} + + {% acl_end %} + {% include "machines/aff_sshfp.html" with sshfp_list=sshfp_list %} {% endblock %} diff --git a/machines/templates/machines/index_vlan.html b/machines/templates/machines/index_vlan.html index beb6c80e..7af31fd5 100644 --- a/machines/templates/machines/index_vlan.html +++ b/machines/templates/machines/index_vlan.html @@ -26,15 +26,16 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load acl %} +{% load i18n %} -{% block title %}Machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

Liste des vlans

+

{% trans "List of VLANs" %}

{% can_create Vlan %} - Ajouter un vlan + {% trans " Add a VLAN" %} {% acl_end %} - Supprimer un ou plusieurs vlan + {% trans " Delete one or several VLANs" %} {% include "machines/aff_vlan.html" with vlan_list=vlan_list %}

diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 7ec4212a..432d11f8 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -26,8 +26,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load massive_bootstrap_form %} +{% load i18n %} -{% block title %}Création et modification de machines{% endblock %} +{% block title %}{% trans "Machines" %}{% endblock %} {% block content %} {% if machineform %} @@ -72,6 +73,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if sshfpform %} {% bootstrap_form_errors sshfpform %} {% endif %} +{% if roleform %} + {% bootstrap_form_errors roleform %} +{% endif %} {% if vlanform %} {% bootstrap_form_errors vlanform %} {% endif %} @@ -85,11 +89,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% if machineform %} -

Machine

+

{% trans "Machine" %}

{% massive_bootstrap_form machineform 'user' %} {% endif %} {% if interfaceform %} -

Interface

+

{% trans "Interface" %}

{% if i_mbf_param %} {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} {% else %} @@ -97,67 +101,71 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% endif %} {% if domainform %} -

Domaine

+

{% trans "Domain" %}

{% bootstrap_form domainform %} {% endif %} {% if iptypeform %} -

Type d'IP

+

{% trans "IP type" %}

{% bootstrap_form iptypeform %} {% endif %} {% if machinetypeform %} -

Type de machine

+

{% trans "Machine type" %}

{% bootstrap_form machinetypeform %} {% endif %} {% if extensionform %} -

Extension

+

{% trans "Extension" %}

{% massive_bootstrap_form extensionform 'origin' %} {% endif %} {% if soaform %} -

Enregistrement SOA

+

{% trans "SOA record" %}

{% bootstrap_form soaform %} {% endif %} {% if mxform %} -

Enregistrement MX

+

{% trans "MX record" %}

{% massive_bootstrap_form mxform 'name' %} {% endif %} {% if nsform %} -

Enregistrement NS

+

{% trans "NS record" %}

{% massive_bootstrap_form nsform 'ns' %} {% endif %} {% if txtform %} -

Enregistrement TXT

+

{% trans "TXT record" %}

{% bootstrap_form txtform %} {% endif %} {% if dnameform %} -

DNAME record

+

{% trans "DNAME record" %}

{% bootstrap_form dnameform %} {% endif %} {% if srvform %} -

Enregistrement SRV

+

{% trans "SRV record" %}

{% massive_bootstrap_form srvform 'target' %} {% endif %} {% if sshfpform %} -

SSHFP record

+

{% trans "SSHFP record" %}

{% bootstrap_form sshfpform %} {% endif %} {% if aliasform %} -

Alias

+

{% trans "Alias" %}

{% bootstrap_form aliasform %} {% endif %} {% if serviceform %} -

Service

+

{% trans "Service" %}

{% massive_bootstrap_form serviceform 'servers' %} {% endif %} + {% if roleform %} +

Role

+ {% massive_bootstrap_form roleform 'servers' %} + {% endif %} {% if vlanform %} -

Vlan

+

{% trans "VLAN" %}

{% bootstrap_form vlanform %} {% endif %} {% if nasform %} -

NAS

+

{% trans "NAS device" %}

{% bootstrap_form nasform %} {% endif %} {% if ipv6form %} -

Ipv6

+

{% trans "IPv6 address" %}

{% bootstrap_form ipv6form %} {% endif %} {% bootstrap_button action_name button_type="submit" icon="star" %} @@ -166,3 +174,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% endblock %} + diff --git a/machines/templates/machines/sidebar.html b/machines/templates/machines/sidebar.html index 5a0f975d..68e14ae0 100644 --- a/machines/templates/machines/sidebar.html +++ b/machines/templates/machines/sidebar.html @@ -24,54 +24,62 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} +{% load i18n %} {% block sidebar %} {% can_view_all Machine %} - Machines + {% trans "Machines" %} {% acl_end %} {% can_view_all MachineType %} - Types de machines + {% trans "Machine types" %} {% acl_end %} {% can_view_all Extension %} - Extensions et zones + {% trans "Extensions and zones" %} {% acl_end %} {% can_view_all IpType %} - Plages d'IP + {% trans "IP ranges" %} {% acl_end %} {% can_view_all Vlan %} - Vlans + {% trans "VLANs" %} {% acl_end %} {% can_view_all Nas %} - Gestion des nas + {% trans "NAS devices" %} {% acl_end %} {% can_view_all machines.Service %} - Services (dhcp, dns...) + {% trans "Services (DHCP, DNS, ...)" %} + + {% acl_end %} + {% can_view_all Role %} + + + {% trans "Server roles" %} {% acl_end %} {% can_view_all OuverturePortList %} - Ouverture de ports + {% trans "Ports openings" %} {% acl_end %} {% endblock %} + diff --git a/machines/urls.py b/machines/urls.py index ce0a7a78..d6f3a541 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -21,7 +21,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """machines.urls -The defined URLs for the Cotisations app +The defined URLs for the Machines app """ from __future__ import unicode_literals @@ -124,7 +124,14 @@ urlpatterns = [ views.edit_service, name='edit-service'), url(r'^del_service/$', views.del_service, name='del-service'), + url(r'^regen_service/(?P[0-9]+)$', views.regen_service, name='regen-service'), url(r'^index_service/$', views.index_service, name='index-service'), + url(r'^add_role/$', views.add_role, name='add-role'), + url(r'^edit_role/(?P[0-9]+)$', + views.edit_role, + name='edit-role'), + url(r'^del_role/$', views.del_role, name='del-role'), + url(r'^index_role/$', views.index_role, name='index-role'), url(r'^add_vlan/$', views.add_vlan, name='add-vlan'), url(r'^edit_vlan/(?P[0-9]+)$', views.edit_vlan, name='edit-vlan'), url(r'^del_vlan/$', views.del_vlan, name='del-vlan'), diff --git a/machines/views.py b/machines/views.py index 398b9250..02c3c671 100644 --- a/machines/views.py +++ b/machines/views.py @@ -33,13 +33,14 @@ The views for the Machines app from __future__ import unicode_literals from django.urls import reverse -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import render, redirect from django.contrib import messages from django.contrib.auth.decorators import login_required, permission_required from django.db.models import ProtectedError, F from django.forms import modelformset_factory from django.views.decorators.csrf import csrf_exempt +from django.utils.translation import ugettext as _ from rest_framework.renderers import JSONRenderer @@ -101,6 +102,8 @@ from .forms import ( DelMxForm, VlanForm, DelVlanForm, + RoleForm, + DelRoleForm, ServiceForm, DelServiceForm, SshFpForm, @@ -122,8 +125,10 @@ from .models import ( Mx, Ns, Domain, + Role, Service, Service_link, + regen, Vlan, Nas, Txt, @@ -148,7 +153,7 @@ def generate_ipv4_choices(form_obj): """ f_ipv4 = form_obj.fields['ipv4'] used_mtype_id = [] - choices = '{"":[{key:"",value:"Choisissez d\'abord un type de machine"},' + choices = '{"":[{key:"",value:"'+_("Select a machine type first.") + '"}' mtype_id = -1 for ip in (f_ipv4.queryset @@ -178,14 +183,14 @@ def generate_ipv4_engine(is_type_tt): """ return ( 'new Bloodhound( {{' - 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),' - 'queryTokenizer: Bloodhound.tokenizers.whitespace,' - 'local: choices_ipv4[ $( "#{type_id}" ).val() ],' - 'identify: function( obj ) {{ return obj.key; }}' + 'datumTokenizer: Bloodhound.tokenizers.obj.whitespace( "value" ),' + 'queryTokenizer: Bloodhound.tokenizers.whitespace,' + 'local: choices_ipv4[ $( "#{type_id}" ).val() ],' + 'identify: function( obj ) {{ return obj.key; }}' '}} )' - ).format( - type_id=f_type_id(is_type_tt) - ) + ).format( + type_id=f_type_id(is_type_tt) + ) def generate_ipv4_match_func(is_type_tt): @@ -193,17 +198,17 @@ def generate_ipv4_match_func(is_type_tt): """ return ( 'function(q, sync) {{' - 'if (q === "") {{' - 'var first = choices_ipv4[$("#{type_id}").val()].slice(0, 5);' - 'first = first.map( function (obj) {{ return obj.key; }} );' - 'sync(engine_ipv4.get(first));' - '}} else {{' - 'engine_ipv4.search(q, sync);' - '}}' + 'if (q === "") {{' + 'var first = choices_ipv4[$("#{type_id}").val()].slice(0, 5);' + 'first = first.map( function (obj) {{ return obj.key; }} );' + 'sync(engine_ipv4.get(first));' + '}} else {{' + 'engine_ipv4.search(q, sync);' '}}' - ).format( - type_id=f_type_id(is_type_tt) - ) + '}}' + ).format( + type_id=f_type_id(is_type_tt) + ) def generate_ipv4_mbf_param(form_obj, is_type_tt): @@ -250,7 +255,7 @@ def new_machine(request, user, **_kwargs): new_interface_obj.save() new_domain.interface_parent = new_interface_obj new_domain.save() - messages.success(request, "La machine a été créée") + messages.success(request, _("The machine was created.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(user.id)} @@ -262,7 +267,7 @@ def new_machine(request, user, **_kwargs): 'interfaceform': interface, 'domainform': domain, 'i_mbf_param': i_mbf_param, - 'action_name': 'Créer une machine' + 'action_name': _("Create a machine") }, 'machines/machine.html', request @@ -302,7 +307,7 @@ def edit_interface(request, interface_instance, **_kwargs): new_interface_obj.save() if domain_form.changed_data: new_domain_obj.save() - messages.success(request, "La machine a été modifiée") + messages.success(request, _("The machine was edited.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(interface_instance.machine.user.id)} @@ -314,7 +319,7 @@ def edit_interface(request, interface_instance, **_kwargs): 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param, - 'action_name': 'Editer une interface' + 'action_name': _("Edit") }, 'machines/machine.html', request @@ -327,7 +332,7 @@ def del_machine(request, machine, **_kwargs): """ Supprime une machine, interfaces en mode cascade""" if request.method == "POST": machine.delete() - messages.success(request, "La machine a été détruite") + messages.success(request, _("The machine was deleted.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(machine.user.id)} @@ -356,7 +361,7 @@ def new_interface(request, machine, **_kwargs): new_interface_obj.save() new_domain_obj.interface_parent = new_interface_obj new_domain_obj.save() - messages.success(request, "L'interface a été ajoutée") + messages.success(request, _("The interface was created.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(machine.user.id)} @@ -367,7 +372,7 @@ def new_interface(request, machine, **_kwargs): 'interfaceform': interface_form, 'domainform': domain_form, 'i_mbf_param': i_mbf_param, - 'action_name': 'Créer une interface' + 'action_name': _("Create an interface") }, 'machines/machine.html', request @@ -383,7 +388,7 @@ def del_interface(request, interface, **_kwargs): interface.delete() if not machine.interface_set.all(): machine.delete() - messages.success(request, "L'interface a été détruite") + messages.success(request, _("The interface was deleted.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} @@ -408,13 +413,13 @@ def new_ipv6list(request, interface, **_kwargs): ) if ipv6.is_valid(): ipv6.save() - messages.success(request, "Ipv6 ajoutée") + messages.success(request, _("The IPv6 addresses list was created.")) return redirect(reverse( 'machines:index-ipv6', kwargs={'interfaceid': str(interface.id)} )) return form( - {'ipv6form': ipv6, 'action_name': 'Créer'}, + {'ipv6form': ipv6, 'action_name': _("Create an IPv6 addresses list")}, 'machines/machine.html', request ) @@ -432,13 +437,13 @@ def edit_ipv6list(request, ipv6list_instance, **_kwargs): if ipv6.is_valid(): if ipv6.changed_data: ipv6.save() - messages.success(request, "Ipv6 modifiée") + messages.success(request, _("The IPv6 addresses list was edited.")) return redirect(reverse( 'machines:index-ipv6', kwargs={'interfaceid': str(ipv6list_instance.interface.id)} )) return form( - {'ipv6form': ipv6, 'action_name': 'Editer'}, + {'ipv6form': ipv6, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -451,7 +456,7 @@ def del_ipv6list(request, ipv6list, **_kwargs): if request.method == "POST": interfaceid = ipv6list.interface.id ipv6list.delete() - messages.success(request, "L'ipv6 a été détruite") + messages.success(request, _("The IPv6 addresses list was deleted.")) return redirect(reverse( 'machines:index-ipv6', kwargs={'interfaceid': str(interfaceid)} @@ -475,13 +480,13 @@ def new_sshfp(request, machine, **_kwargs): ) if sshfp.is_valid(): sshfp.save() - messages.success(request, "The SSHFP record was added") + messages.success(request, _("The SSHFP record was created.")) return redirect(reverse( 'machines:index-sshfp', kwargs={'machineid': str(machine.id)} )) return form( - {'sshfpform': sshfp, 'action_name': 'Create'}, + {'sshfpform': sshfp, 'action_name': _("Create a SSHFP record")}, 'machines/machine.html', request ) @@ -498,13 +503,13 @@ def edit_sshfp(request, sshfp_instance, **_kwargs): if sshfp.is_valid(): if sshfp.changed_data: sshfp.save() - messages.success(request, "The SSHFP record was edited") + messages.success(request, _("The SSHFP record was edited.")) return redirect(reverse( 'machines:index-sshfp', kwargs={'machineid': str(sshfp_instance.machine.id)} )) return form( - {'sshfpform': sshfp, 'action_name': 'Edit'}, + {'sshfpform': sshfp, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -517,7 +522,7 @@ def del_sshfp(request, sshfp, **_kwargs): if request.method == "POST": machineid = sshfp.machine.id sshfp.delete() - messages.success(request, "The SSHFP record was deleted") + messages.success(request, _("The SSHFP record was deleted.")) return redirect(reverse( 'machines:index-sshfp', kwargs={'machineid': str(machineid)} @@ -538,10 +543,10 @@ def add_iptype(request): iptype = IpTypeForm(request.POST or None) if iptype.is_valid(): iptype.save() - messages.success(request, "Ce type d'ip a été ajouté") + messages.success(request, _("The IP type was created.")) return redirect(reverse('machines:index-iptype')) return form( - {'iptypeform': iptype, 'action_name': 'Créer'}, + {'iptypeform': iptype, 'action_name': _("Create an IP type")}, 'machines/machine.html', request ) @@ -557,10 +562,10 @@ def edit_iptype(request, iptype_instance, **_kwargs): if iptype.is_valid(): if iptype.changed_data: iptype.save() - messages.success(request, "Type d'ip modifié") + messages.success(request, _("The IP type was edited.")) return redirect(reverse('machines:index-iptype')) return form( - {'iptypeform': iptype, 'action_name': 'Editer'}, + {'iptypeform': iptype, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -576,16 +581,16 @@ def del_iptype(request, instances): for iptype_del in iptype_dels: try: iptype_del.delete() - messages.success(request, "Le type d'ip a été supprimé") + messages.success(request, _("The IP type was deleted.")) except ProtectedError: messages.error( request, - ("Le type d'ip %s est affectée à au moins une machine, " - "vous ne pouvez pas le supprimer" % iptype_del) + (_("The IP type %s is assigned to at least one machine," + " you can't delete it.") % iptype_del) ) return redirect(reverse('machines:index-iptype')) return form( - {'iptypeform': iptype, 'action_name': 'Supprimer'}, + {'iptypeform': iptype, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -598,10 +603,11 @@ def add_machinetype(request): machinetype = MachineTypeForm(request.POST or None) if machinetype.is_valid(): machinetype.save() - messages.success(request, "Ce type de machine a été ajouté") + messages.success(request, _("The machine type was created.")) return redirect(reverse('machines:index-machinetype')) return form( - {'machinetypeform': machinetype, 'action_name': 'Créer'}, + {'machinetypeform': machinetype, 'action_name': _("Create a machine" + " type")}, 'machines/machine.html', request ) @@ -618,10 +624,10 @@ def edit_machinetype(request, machinetype_instance, **_kwargs): if machinetype.is_valid(): if machinetype.changed_data: machinetype.save() - messages.success(request, "Type de machine modifié") + messages.success(request, _("The machine type was edited.")) return redirect(reverse('machines:index-machinetype')) return form( - {'machinetypeform': machinetype, 'action_name': 'Editer'}, + {'machinetypeform': machinetype, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -637,17 +643,16 @@ def del_machinetype(request, instances): for machinetype_del in machinetype_dels: try: machinetype_del.delete() - messages.success(request, "Le type de machine a été supprimé") + messages.success(request, _("The machine type was deleted.")) except ProtectedError: messages.error( request, - ("Le type de machine %s est affectée à au moins une " - "machine, vous ne pouvez pas le supprimer" - % machinetype_del) + (_("The machine type %s is assigned to at least one" + " machine, you can't delete it.") % machinetype_del) ) return redirect(reverse('machines:index-machinetype')) return form( - {'machinetypeform': machinetype, 'action_name': 'Supprimer'}, + {'machinetypeform': machinetype, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -660,10 +665,10 @@ def add_extension(request): extension = ExtensionForm(request.POST or None) if extension.is_valid(): extension.save() - messages.success(request, "Cette extension a été ajoutée") + messages.success(request, _("The extension was created.")) return redirect(reverse('machines:index-extension')) return form( - {'extensionform': extension, 'action_name': 'Créer'}, + {'extensionform': extension, 'action_name': _("Create an extension")}, 'machines/machine.html', request ) @@ -680,10 +685,10 @@ def edit_extension(request, extension_instance, **_kwargs): if extension.is_valid(): if extension.changed_data: extension.save() - messages.success(request, "Extension modifiée") + messages.success(request, _("The extension was edited.")) return redirect(reverse('machines:index-extension')) return form( - {'extensionform': extension, 'action_name': 'Editer'}, + {'extensionform': extension, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -699,17 +704,16 @@ def del_extension(request, instances): for extension_del in extension_dels: try: extension_del.delete() - messages.success(request, "L'extension a été supprimée") + messages.success(request, _("The extension was deleted.")) except ProtectedError: messages.error( request, - ("L'extension %s est affectée à au moins un type de " - "machine, vous ne pouvez pas la supprimer" - % extension_del) + (_("The extension %s is assigned to at least one machine" + " type, you can't delete it." % extension_del)) ) return redirect(reverse('machines:index-extension')) return form( - {'extensionform': extension, 'action_name': 'Supprimer'}, + {'extensionform': extension, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -722,10 +726,10 @@ def add_soa(request): soa = SOAForm(request.POST or None) if soa.is_valid(): soa.save() - messages.success(request, "Cet enregistrement SOA a été ajouté") + messages.success(request, _("The SOA record was created.")) return redirect(reverse('machines:index-extension')) return form( - {'soaform': soa, 'action_name': 'Créer'}, + {'soaform': soa, 'action_name': _("Create an SOA record")}, 'machines/machine.html', request ) @@ -739,10 +743,10 @@ def edit_soa(request, soa_instance, **_kwargs): if soa.is_valid(): if soa.changed_data: soa.save() - messages.success(request, "SOA modifié") + messages.success(request, _("The SOA record was edited.")) return redirect(reverse('machines:index-extension')) return form( - {'soaform': soa, 'action_name': 'Editer'}, + {'soaform': soa, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -758,16 +762,15 @@ def del_soa(request, instances): for soa_del in soa_dels: try: soa_del.delete() - messages.success(request, "Le SOA a été supprimée") + messages.success(request, _("The SOA record was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le SOA suivant %s ne peut être supprimé" - % soa_del) + (_("Error: the SOA record %s can't be deleted.") % soa_del) ) return redirect(reverse('machines:index-extension')) return form( - {'soaform': soa, 'action_name': 'Supprimer'}, + {'soaform': soa, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -780,10 +783,10 @@ def add_mx(request): mx = MxForm(request.POST or None) if mx.is_valid(): mx.save() - messages.success(request, "Cet enregistrement mx a été ajouté") + messages.success(request, _("The MX record was created.")) return redirect(reverse('machines:index-extension')) return form( - {'mxform': mx, 'action_name': 'Créer'}, + {'mxform': mx, 'action_name': _("Create an MX record")}, 'machines/machine.html', request ) @@ -797,10 +800,10 @@ def edit_mx(request, mx_instance, **_kwargs): if mx.is_valid(): if mx.changed_data: mx.save() - messages.success(request, "Mx modifié") + messages.success(request, _("The MX record was edited.")) return redirect(reverse('machines:index-extension')) return form( - {'mxform': mx, 'action_name': 'Editer'}, + {'mxform': mx, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -816,16 +819,15 @@ def del_mx(request, instances): for mx_del in mx_dels: try: mx_del.delete() - messages.success(request, "L'mx a été supprimée") + messages.success(request, _("The MX record was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le Mx suivant %s ne peut être supprimé" - % mx_del) + (_("Error: the MX record %s can't be deleted.") % mx_del) ) return redirect(reverse('machines:index-extension')) return form( - {'mxform': mx, 'action_name': 'Supprimer'}, + {'mxform': mx, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -838,10 +840,10 @@ def add_ns(request): ns = NsForm(request.POST or None) if ns.is_valid(): ns.save() - messages.success(request, "Cet enregistrement ns a été ajouté") + messages.success(request, _("The NS record was created.")) return redirect(reverse('machines:index-extension')) return form( - {'nsform': ns, 'action_name': 'Créer'}, + {'nsform': ns, 'action_name': _("Create an NS record")}, 'machines/machine.html', request ) @@ -855,10 +857,10 @@ def edit_ns(request, ns_instance, **_kwargs): if ns.is_valid(): if ns.changed_data: ns.save() - messages.success(request, "Ns modifié") + messages.success(request, _("The NS record was edited.")) return redirect(reverse('machines:index-extension')) return form( - {'nsform': ns, 'action_name': 'Editer'}, + {'nsform': ns, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -874,16 +876,15 @@ def del_ns(request, instances): for ns_del in ns_dels: try: ns_del.delete() - messages.success(request, "Le ns a été supprimée") + messages.success(request, _("The NS record was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le Ns suivant %s ne peut être supprimé" - % ns_del) + (_("Error: the NS record %s can't be deleted.") % ns_del) ) return redirect(reverse('machines:index-extension')) return form( - {'nsform': ns, 'action_name': 'Supprimer'}, + {'nsform': ns, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -895,10 +896,10 @@ def add_dname(request): dname = DNameForm(request.POST or None) if dname.is_valid(): dname.save() - messages.success(request, "This DNAME record has been added") + messages.success(request, _("The DNAME record was created.")) return redirect(reverse('machines:index-extension')) return form( - {'dnameform': dname, 'action_name': "Create"}, + {'dnameform': dname, 'action_name': _("Create a DNAME record")}, 'machines/machine.html', request ) @@ -912,10 +913,10 @@ def edit_dname(request, dname_instance, **_kwargs): if dname.is_valid(): if dname.changed_data: dname.save() - messages.success(request, "DName successfully edited") + messages.success(request, _("The DNAME record was edited.")) return redirect(reverse('machines:index-extension')) return form( - {'dnameform': dname, 'action_name': "Edit"}, + {'dnameform': dname, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -931,16 +932,16 @@ def del_dname(request, instances): for dname_del in dname_dels: try: dname_del.delete() - messages.success(request, - "The DNAME %s has been deleted" % dname_del) + messages.success(request, _("The DNAME record was deleted.")) except ProtectedError: messages.error( - request, - "The DNAME %s can not be deleted" % dname_del + request, + _("Error: the DNAME record %s can't be deleted.") + % dname_del ) return redirect(reverse('machines:index-extension')) return form( - {'dnameform': dname, 'action_name': 'Delete'}, + {'dnameform': dname, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -953,10 +954,10 @@ def add_txt(request): txt = TxtForm(request.POST or None) if txt.is_valid(): txt.save() - messages.success(request, "Cet enregistrement text a été ajouté") + messages.success(request, _("The TXT record was created.")) return redirect(reverse('machines:index-extension')) return form( - {'txtform': txt, 'action_name': 'Créer'}, + {'txtform': txt, 'action_name': _("Create a TXT record")}, 'machines/machine.html', request ) @@ -970,10 +971,10 @@ def edit_txt(request, txt_instance, **_kwargs): if txt.is_valid(): if txt.changed_data: txt.save() - messages.success(request, "Txt modifié") + messages.success(request, _("The TXT record was edited.")) return redirect(reverse('machines:index-extension')) return form( - {'txtform': txt, 'action_name': 'Editer'}, + {'txtform': txt, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -989,16 +990,15 @@ def del_txt(request, instances): for txt_del in txt_dels: try: txt_del.delete() - messages.success(request, "Le txt a été supprimé") + messages.success(request, _("The TXT record was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le Txt suivant %s ne peut être supprimé" - % txt_del) + (_("Error: the TXT record %s can't be deleted.") % txt_del) ) return redirect(reverse('machines:index-extension')) return form( - {'txtform': txt, 'action_name': 'Supprimer'}, + {'txtform': txt, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -1011,10 +1011,10 @@ def add_srv(request): srv = SrvForm(request.POST or None) if srv.is_valid(): srv.save() - messages.success(request, "Cet enregistrement srv a été ajouté") + messages.success(request, _("The SRV record was created.")) return redirect(reverse('machines:index-extension')) return form( - {'srvform': srv, 'action_name': 'Créer'}, + {'srvform': srv, 'action_name': _("Create an SRV record")}, 'machines/machine.html', request ) @@ -1028,10 +1028,10 @@ def edit_srv(request, srv_instance, **_kwargs): if srv.is_valid(): if srv.changed_data: srv.save() - messages.success(request, "Srv modifié") - return redirect(reverse('machines:index-extension')) + messages.success(request, _("The SRV record was edited.")) + return redirect(reverse('machines:1index-extension')) return form( - {'srvform': srv, 'action_name': 'Editer'}, + {'srvform': srv, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -1047,16 +1047,15 @@ def del_srv(request, instances): for srv_del in srv_dels: try: srv_del.delete() - messages.success(request, "L'srv a été supprimée") + messages.success(request, _("The SRV record was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le Srv suivant %s ne peut être supprimé" - % srv_del) + (_("Error: the SRV record %s can't be deleted.") % srv_del) ) return redirect(reverse('machines:index-extension')) return form( - {'srvform': srv, 'action_name': 'Supprimer'}, + {'srvform': srv, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -1072,13 +1071,13 @@ def add_alias(request, interface, interfaceid): alias = alias.save(commit=False) alias.cname = interface.domain alias.save() - messages.success(request, "Cet alias a été ajouté") + messages.success(request, _("The alias was created.")) return redirect(reverse( 'machines:index-alias', kwargs={'interfaceid': str(interfaceid)} )) return form( - {'aliasform': alias, 'action_name': 'Créer'}, + {'aliasform': alias, 'action_name': _("Create an alias")}, 'machines/machine.html', request ) @@ -1096,7 +1095,7 @@ def edit_alias(request, domain_instance, **_kwargs): if alias.is_valid(): if alias.changed_data: domain_instance = alias.save() - messages.success(request, "Alias modifié") + messages.success(request, _("The alias was edited.")) return redirect(reverse( 'machines:index-alias', kwargs={ @@ -1104,7 +1103,7 @@ def edit_alias(request, domain_instance, **_kwargs): } )) return form( - {'aliasform': alias, 'action_name': 'Editer'}, + {'aliasform': alias, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -1122,20 +1121,76 @@ def del_alias(request, interface, interfaceid): alias_del.delete() messages.success( request, - "L'alias %s a été supprimé" % alias_del + _("The alias %s was deleted.") % alias_del ) except ProtectedError: messages.error( request, - ("Erreur l'alias suivant %s ne peut être supprimé" - % alias_del) + (_("Error: the alias %s can't be deleted.") % alias_del) ) return redirect(reverse( 'machines:index-alias', kwargs={'interfaceid': str(interfaceid)} )) return form( - {'aliasform': alias, 'action_name': 'Supprimer'}, + {'aliasform': alias, 'action_name': _("Delete")}, + 'machines/machine.html', + request + ) + + +@login_required +@can_create(Role) +def add_role(request): + """ View used to add a Role object """ + role = RoleForm(request.POST or None) + if role.is_valid(): + role.save() + messages.success(request, _("The role was created.")) + return redirect(reverse('machines:index-role')) + return form( + {'roleform': role, 'action_name': _("Create a role")}, + 'machines/machine.html', + request + ) + + +@login_required +@can_edit(Role) +def edit_role(request, role_instance, **_kwargs): + """ View used to edit a Role object """ + role = RoleForm(request.POST or None, instance=role_instance) + if role.is_valid(): + if role.changed_data: + role.save() + messages.success(request, _("The role was edited.")) + return redirect(reverse('machines:index-role')) + return form( + {'roleform': role, 'action_name': _("Edit")}, + 'machines/machine.html', + request + ) + + +@login_required +@can_delete_set(Role) +def del_role(request, instances): + """ View used to delete a Service object """ + role = DelRoleForm(request.POST or None, instances=instances) + if role.is_valid(): + role_dels = role.cleaned_data['role'] + for role_del in role_dels: + try: + role_del.delete() + messages.success(request, _("The role was deleted.")) + except ProtectedError: + messages.error( + request, + (_("Error: the role %s can't be deleted.") % role_del) + ) + return redirect(reverse('machines:index-role')) + return form( + {'roleform': role, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -1148,10 +1203,10 @@ def add_service(request): service = ServiceForm(request.POST or None) if service.is_valid(): service.save() - messages.success(request, "Cet enregistrement service a été ajouté") + messages.success(request, _("The service was created.")) return redirect(reverse('machines:index-service')) return form( - {'serviceform': service, 'action_name': 'Créer'}, + {'serviceform': service, 'action_name': _("Create a service")}, 'machines/machine.html', request ) @@ -1165,10 +1220,10 @@ def edit_service(request, service_instance, **_kwargs): if service.is_valid(): if service.changed_data: service.save() - messages.success(request, "Service modifié") + messages.success(request, _("The service was edited.")) return redirect(reverse('machines:index-service')) return form( - {'serviceform': service, 'action_name': 'Editer'}, + {'serviceform': service, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -1184,20 +1239,28 @@ def del_service(request, instances): for service_del in service_dels: try: service_del.delete() - messages.success(request, "Le service a été supprimée") + messages.success(request, _("The service was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le service suivant %s ne peut être supprimé" - % service_del) + (_("Error: the service %s can't be deleted.") % service_del) ) return redirect(reverse('machines:index-service')) return form( - {'serviceform': service, 'action_name': 'Supprimer'}, + {'serviceform': service, 'action_name': _("Delete")}, 'machines/machine.html', request ) +@login_required +@can_edit(Service) +def regen_service(request,service, **_kwargs): + """Ask for a regen of the service""" + + regen(service) + return index_service(request) + + @login_required @can_create(Vlan) @@ -1206,10 +1269,10 @@ def add_vlan(request): vlan = VlanForm(request.POST or None) if vlan.is_valid(): vlan.save() - messages.success(request, "Cet enregistrement vlan a été ajouté") + messages.success(request, _("The VLAN was created.")) return redirect(reverse('machines:index-vlan')) return form( - {'vlanform': vlan, 'action_name': 'Créer'}, + {'vlanform': vlan, 'action_name': _("Create a VLAN")}, 'machines/machine.html', request ) @@ -1223,10 +1286,10 @@ def edit_vlan(request, vlan_instance, **_kwargs): if vlan.is_valid(): if vlan.changed_data: vlan.save() - messages.success(request, "Vlan modifié") + messages.success(request, _("The VLAN was edited.")) return redirect(reverse('machines:index-vlan')) return form( - {'vlanform': vlan, 'action_name': 'Editer'}, + {'vlanform': vlan, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -1242,16 +1305,15 @@ def del_vlan(request, instances): for vlan_del in vlan_dels: try: vlan_del.delete() - messages.success(request, "Le vlan a été supprimée") + messages.success(request, _("The VLAN was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le Vlan suivant %s ne peut être supprimé" - % vlan_del) + (_("Error: the VLAN %s can't be deleted.") % vlan_del) ) return redirect(reverse('machines:index-vlan')) return form( - {'vlanform': vlan, 'action_name': 'Supprimer'}, + {'vlanform': vlan, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -1264,10 +1326,10 @@ def add_nas(request): nas = NasForm(request.POST or None) if nas.is_valid(): nas.save() - messages.success(request, "Cet enregistrement nas a été ajouté") + messages.success(request, _("The NAS device was created.")) return redirect(reverse('machines:index-nas')) return form( - {'nasform': nas, 'action_name': 'Créer'}, + {'nasform': nas, 'action_name': _("Create a NAS device")}, 'machines/machine.html', request ) @@ -1281,10 +1343,10 @@ def edit_nas(request, nas_instance, **_kwargs): if nas.is_valid(): if nas.changed_data: nas.save() - messages.success(request, "Nas modifié") + messages.success(request, _("The NAS device was edited.")) return redirect(reverse('machines:index-nas')) return form( - {'nasform': nas, 'action_name': 'Editer'}, + {'nasform': nas, 'action_name': _("Edit")}, 'machines/machine.html', request ) @@ -1300,16 +1362,15 @@ def del_nas(request, instances): for nas_del in nas_dels: try: nas_del.delete() - messages.success(request, "Le nas a été supprimé") + messages.success(request, _("The NAS device was deleted.")) except ProtectedError: messages.error( request, - ("Erreur le Nas suivant %s ne peut être supprimé" - % nas_del) + (_("Error: the NAS device %s can't be deleted.") % nas_del) ) return redirect(reverse('machines:index-nas')) return form( - {'nasform': nas, 'action_name': 'Supprimer'}, + {'nasform': nas, 'action_name': _("Delete")}, 'machines/machine.html', request ) @@ -1470,7 +1531,7 @@ def index_sshfp(request, machine, machineid): @login_required -@can_view_all(Interface) +@can_view(Interface) def index_ipv6(request, interface, interfaceid): """ View used to display the list of existing IPv6 of an interface """ ipv6_list = Ipv6List.objects.filter(interface=interface) @@ -1481,6 +1542,21 @@ def index_ipv6(request, interface, interfaceid): ) +@login_required +@can_view_all(Role) +def index_role(request): + """ View used to display the list of existing roles """ + role_list = (Role.objects + .prefetch_related( + 'servers__domain__extension' + ).all()) + return render( + request, + 'machines/index_role.html', + {'role_list': role_list} + ) + + @login_required @can_view_all(Service) def index_service(request): @@ -1546,7 +1622,7 @@ def edit_portlist(request, ouvertureportlist_instance, **_kwargs): for port in instances: port.port_list = pl port.save() - messages.success(request, "Liste de ports modifiée") + messages.success(request, _("The ports list was edited.")) return redirect(reverse('machines:index-portlist')) return form( {'port_list': port_list, 'ports': port_formset}, @@ -1560,7 +1636,7 @@ def edit_portlist(request, ouvertureportlist_instance, **_kwargs): def del_portlist(request, port_list_instance, **_kwargs): """ View used to delete a port policy """ port_list_instance.delete() - messages.success(request, "La liste de ports a été supprimée") + messages.success(request, _("The ports list was deleted.")) return redirect(reverse('machines:index-portlist')) @@ -1570,12 +1646,12 @@ def add_portlist(request): """ View used to add a port policy """ port_list = EditOuverturePortListForm(request.POST or None) port_formset = modelformset_factory( - OuverturePort, - fields=('begin', 'end', 'protocole', 'io'), - extra=0, - can_delete=True, - min_num=1, - validate_min=True, + OuverturePort, + fields=('begin', 'end', 'protocole', 'io'), + extra=0, + can_delete=True, + min_num=1, + validate_min=True, )(request.POST or None, queryset=OuverturePort.objects.none()) if port_list.is_valid() and port_formset.is_valid(): pl = port_list.save() @@ -1585,7 +1661,7 @@ def add_portlist(request): for port in instances: port.port_list = pl port.save() - messages.success(request, "Liste de ports créée") + messages.success(request, _("The ports list was created.")) return redirect(reverse('machines:index-portlist')) return form( {'port_list': port_list, 'ports': port_formset}, @@ -1603,8 +1679,8 @@ def configure_ports(request, interface_instance, **_kwargs): if not interface_instance.may_have_port_open(): messages.error( request, - ("Attention, l'ipv4 n'est pas publique, l'ouverture n'aura pas " - "d'effet en v4") + (_("Warning: the IPv4 isn't public, the opening won't have effect" + " in v4.")) ) interface = EditOuverturePortConfigForm( request.POST or None, @@ -1613,20 +1689,22 @@ def configure_ports(request, interface_instance, **_kwargs): if interface.is_valid(): if interface.changed_data: interface.save() - messages.success(request, "Configuration des ports mise à jour.") + messages.success(request, _("The ports configuration was edited.")) return redirect(reverse('machines:index')) return form( - {'interfaceform': interface, 'action_name': 'Editer la configuration'}, + {'interfaceform': interface, 'action_name': _("Edit the" + " configuration")}, 'machines/machine.html', request ) -## Framework Rest +# Framework Rest class JSONResponse(HttpResponse): """ Class to build a JSON response. Used for API """ + def __init__(self, data, **kwargs): content = JSONRenderer().render(data) kwargs['content_type'] = 'application/json' @@ -1861,3 +1939,4 @@ def regen_achieved(request): if obj: obj.first().done_regen() return HttpResponse("Ok") + diff --git a/preferences/acl.py b/preferences/acl.py index 1f3f666e..d4b22cfe 100644 --- a/preferences/acl.py +++ b/preferences/acl.py @@ -25,6 +25,7 @@ Here are defined some functions to check acl on the application. """ +from django.utils.translation import ugettext as _ def can_view(user): @@ -38,4 +39,6 @@ def can_view(user): viewing is granted and msg is a message (can be None). """ can = user.has_module_perms('preferences') - return can, None if can else "Vous ne pouvez pas voir cette application." + return can, None if can else _("You don't have the right to view this" + " application.") + diff --git a/preferences/forms.py b/preferences/forms.py index 99910f9c..b9ccd531 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -27,7 +27,7 @@ from __future__ import unicode_literals from django.forms import ModelForm, Form from django import forms - +from django.utils.translation import ugettext_lazy as _ from re2o.mixins import FormRevMixin from .models import ( OptionalUser, @@ -56,9 +56,13 @@ class EditOptionalUserForm(ModelForm): **kwargs ) self.fields['is_tel_mandatory'].label = ( - 'Exiger un numéro de téléphone' + _("Telephone number required") ) - self.fields['self_adhesion'].label = 'Auto inscription' + 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['self_adhesion'].label = _("Self registration") + self.fields['shell_default'].label = _("Default shell") class EditOptionalMachineForm(ModelForm): @@ -74,12 +78,17 @@ class EditOptionalMachineForm(ModelForm): prefix=prefix, **kwargs ) - self.fields['password_machine'].label = "Possibilité d'attribuer\ - un mot de passe par interface" - self.fields['max_lambdauser_interfaces'].label = "Maximum\ - d'interfaces autorisées pour un user normal" - self.fields['max_lambdauser_aliases'].label = "Maximum d'alias\ - dns autorisés pour un user normal" + self.fields['password_machine'].label = _("Possibility to set a" + " password per machine") + self.fields['max_lambdauser_interfaces'].label = _("Maximum number of" + " interfaces" + " allowed for a" + " standard user") + self.fields['max_lambdauser_aliases'].label = _("Maximum number of DNS" + " aliases allowed for" + " a standard user") + self.fields['ipv6_mode'].label = _("IPv6 mode") + self.fields['create_machine'].label = _("Can create a machine") class EditOptionalTopologieForm(ModelForm): @@ -95,10 +104,11 @@ class EditOptionalTopologieForm(ModelForm): prefix=prefix, **kwargs ) - self.fields['vlan_decision_ok'].label = "Vlan où placer les\ - machines après acceptation RADIUS" - self.fields['vlan_decision_nok'].label = "Vlan où placer les\ - machines après rejet RADIUS" + self.fields['radius_general_policy'].label = _("RADIUS general policy") + self.fields['vlan_decision_ok'].label = _("VLAN for machines accepted" + " by RADIUS") + self.fields['vlan_decision_nok'].label = _("VLAN for machines rejected" + " by RADIUS") class EditGeneralOptionForm(ModelForm): @@ -114,18 +124,26 @@ class EditGeneralOptionForm(ModelForm): prefix=prefix, **kwargs ) - self.fields['search_display_page'].label = 'Resultats\ - affichés dans une recherche' - self.fields['pagination_number'].label = 'Items par page,\ - taille normale (ex users)' - self.fields['pagination_large_number'].label = 'Items par page,\ - taille élevée (machines)' - self.fields['req_expire_hrs'].label = 'Temps avant expiration du lien\ - de reinitialisation de mot de passe (en heures)' - self.fields['site_name'].label = 'Nom du site web' - self.fields['email_from'].label = "Adresse mail d\ - 'expedition automatique" - self.fields['GTU_sum_up'].label = "Résumé des CGU" + self.fields['general_message_fr'].label = _("General message in French") + self.fields['general_message_en'].label = _("General message in English") + self.fields['search_display_page'].label = _("Number of results" + " displayed when" + " searching") + self.fields['pagination_number'].label = _("Number of items per page," + " standard size (e.g." + " users)") + self.fields['pagination_large_number'].label = _("Number of items per" + " page, large size" + " (e.g. machines)") + self.fields['req_expire_hrs'].label = _("Time before expiration of the" + " reset password link (in" + " hours)") + self.fields['site_name'].label = _("Website name") + self.fields['email_from'].label = _("Email address for automatic" + " emailing") + self.fields['GTU_sum_up'].label = _("Summary of the General Terms of" + " Use") + self.fields['GTU'].label = _("General Terms of Use") class EditAssoOptionForm(ModelForm): @@ -141,33 +159,16 @@ class EditAssoOptionForm(ModelForm): prefix=prefix, **kwargs ) - self.fields['name'].label = 'Nom de l\'asso' - self.fields['siret'].label = 'SIRET' - self.fields['adresse1'].label = 'Adresse (ligne 1)' - self.fields['adresse2'].label = 'Adresse (ligne 2)' - self.fields['contact'].label = 'Email de contact' - self.fields['telephone'].label = 'Numéro de téléphone' - self.fields['pseudo'].label = 'Pseudo d\'usage' - self.fields['utilisateur_asso'].label = 'Compte utilisé pour\ - faire les modifications depuis /admin' - - def clean(self): - cleaned_data = super().clean() - payment = cleaned_data.get('payment') - - if payment == 'NONE': - return cleaned_data - - if not cleaned_data.get('payment_id', ''): - msg = forms.ValidationError("Vous devez spécifier un identifiant \ - de paiement.") - self.add_error('payment_id', msg) - if not cleaned_data.get('payment_pass', ''): - msg = forms.ValidationError("Vous devez spécifier un mot de passe \ - de paiement.") - self.add_error('payment_pass', msg) - - return cleaned_data + self.fields['name'].label = _("Organisation name") + self.fields['siret'].label = _("SIRET number") + self.fields['adresse1'].label = _("Address (line 1)") + self.fields['adresse2'].label = _("Address (line 2)") + self.fields['contact'].label = _("Contact email address") + self.fields['telephone'].label = _("Telephone number") + self.fields['pseudo'].label = _("Usual name") + self.fields['utilisateur_asso'].label = _("Account used for editing" + " from /admin") + self.fields['description'].label = _("Description") class EditMailMessageOptionForm(ModelForm): @@ -183,10 +184,10 @@ class EditMailMessageOptionForm(ModelForm): prefix=prefix, **kwargs ) - self.fields['welcome_mail_fr'].label = 'Message dans le\ - mail de bienvenue en français' - self.fields['welcome_mail_en'].label = 'Message dans le\ - mail de bienvenue en anglais' + self.fields['welcome_mail_fr'].label = _("Message for the French" + " welcome email") + self.fields['welcome_mail_en'].label = _("Message for the English" + " welcome email") class EditHomeOptionForm(ModelForm): @@ -202,6 +203,9 @@ class EditHomeOptionForm(ModelForm): prefix=prefix, **kwargs ) + self.fields['facebook_url'].label = _("Facebook URL") + self.fields['twitter_url'].label = _("Twitter URL") + self.fields['twitter_account_name'].label = _("Twitter account name") class ServiceForm(ModelForm): @@ -213,13 +217,17 @@ class ServiceForm(ModelForm): def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) + self.fields['name'].label = _("Name") + self.fields['url'].label = _("URL") + self.fields['description'].label = _("Description") + self.fields['image'].label = _("Image") class DelServiceForm(Form): """Suppression de services sur la page d'accueil""" services = forms.ModelMultipleChoiceField( queryset=Service.objects.none(), - label="Enregistrements service actuels", + label=_("Current services"), widget=forms.CheckboxSelectMultiple ) @@ -257,3 +265,4 @@ class DelMailContactForm(Form): self.fields['mailcontacts'].queryset = instances else: self.fields['mailcontacts'].queryset = MailContact.objects.all() + diff --git a/preferences/locale/fr/LC_MESSAGES/django.mo b/preferences/locale/fr/LC_MESSAGES/django.mo index 21ed01a4..657adab3 100644 Binary files a/preferences/locale/fr/LC_MESSAGES/django.mo and b/preferences/locale/fr/LC_MESSAGES/django.mo differ diff --git a/preferences/locale/fr/LC_MESSAGES/django.po b/preferences/locale/fr/LC_MESSAGES/django.po index 8a4ce095..3535b8d7 100644 --- a/preferences/locale/fr/LC_MESSAGES/django.po +++ b/preferences/locale/fr/LC_MESSAGES/django.po @@ -1,70 +1,609 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# 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. # -#, fuzzy +# Copyright © 2018 Maël Kervella +# +# 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. msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" +"Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-07-26 21:49+0200\n" -"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"Last-Translator: FULL NAME \n" -"Language: \n" +"POT-Creation-Date: 2018-08-18 13:26+0200\n" +"PO-Revision-Date: 2018-06-24 15:54+0200\n" +"Last-Translator: Laouen Fernet \n" +"Language-Team: \n" +"Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"Plural-Forms: nplurals=2; plural=(n > 1);\n" -#: models.py:256 -msgid "Contact email adress" -msgstr "Adresse email de contact" +#: acl.py:42 +msgid "You don't have the right to view this application." +msgstr "Vous n'avez pas le droit de voir cette application." -#: models.py:263 -msgid "Description of the associated email adress." +#: forms.py:59 templates/preferences/display_preferences.html:41 +msgid "Telephone number required" +msgstr "Numéro de téléphone requis" + +#: forms.py:61 +msgid "GPG fingerprint" +msgstr "Empreinte GPG" + +#: forms.py:62 +msgid "All can create a club" +msgstr "Tous peuvent créer un club" + +#: forms.py:63 +msgid "All can create a member" +msgstr "Tous peuvent créer un adhérent" + +#: forms.py:64 templates/preferences/display_preferences.html:43 +msgid "Self registration" +msgstr "Autoinscription" + +#: forms.py:65 +msgid "Default shell" +msgstr "Interface système par défaut" + +#: forms.py:81 +msgid "Possibility to set a password per machine" +msgstr "Possibilité de mettre un mot de passe par machine" + +#: forms.py:83 templates/preferences/display_preferences.html:87 +msgid "Maximum number of interfaces allowed for a standard user" +msgstr "Nombre maximum d'interfaces autorisé pour un utilisateur standard" + +#: forms.py:87 templates/preferences/display_preferences.html:91 +msgid "Maximum number of DNS aliases allowed for a standard user" +msgstr "Nombre maximum d'alias DNS autorisé pour un utilisateur standard" + +#: forms.py:90 +msgid "IPv6 mode" +msgstr "Mode IPv6" + +#: forms.py:91 +msgid "Can create a machine" +msgstr "Peut créer une machine" + +#: forms.py:107 +msgid "RADIUS general policy" +msgstr "Politique générale de RADIUS" + +#: forms.py:108 templates/preferences/display_preferences.html:116 +msgid "VLAN for machines accepted by RADIUS" +msgstr "VLAN pour les machines acceptées par RADIUS" + +#: forms.py:110 templates/preferences/display_preferences.html:118 +msgid "VLAN for machines rejected by RADIUS" +msgstr "VLAN pour les machines rejetées par RADIUS" + +#: forms.py:127 +msgid "General message" +msgstr "Message général" + +#: forms.py:128 templates/preferences/display_preferences.html:137 +msgid "Number of results displayed when searching" +msgstr "Nombre de résultats affichés lors de la recherche" + +#: forms.py:131 +msgid "Number of items per page, standard size (e.g. users)" +msgstr "Nombre d'éléments par page, taille standard (ex : utilisateurs)" + +#: forms.py:134 +msgid "Number of items per page, large size (e.g. machines)" +msgstr "Nombre d'éléments par page, taille importante (ex : machines)" + +#: forms.py:137 templates/preferences/display_preferences.html:145 +msgid "Time before expiration of the reset password link (in hours)" +msgstr "" +"Temps avant expiration du lien de réinitialisation de mot de passe (en " +"heures)" + +#: forms.py:140 templates/preferences/display_preferences.html:131 +msgid "Website name" +msgstr "Nom du site" + +#: forms.py:141 templates/preferences/display_preferences.html:133 +msgid "Email address for automatic emailing" +msgstr "Adresse mail pour les mails automatiques" + +#: forms.py:143 templates/preferences/display_preferences.html:151 +msgid "Summary of the General Terms of Use" +msgstr "Résumé des Conditions Générales d'Utilisation" + +#: forms.py:145 templates/preferences/display_preferences.html:155 +msgid "General Terms of Use" +msgstr "Conditions Générales d'Utilisation" + +#: forms.py:161 +msgid "Organisation name" +msgstr "Nom de l'association" + +#: forms.py:162 templates/preferences/display_preferences.html:170 +msgid "SIRET number" +msgstr "Numéro SIRET" + +#: forms.py:163 +msgid "Address (line 1)" +msgstr "Adresse (ligne 1)" + +#: forms.py:164 +msgid "Address (line 2)" +msgstr "Adresse (ligne 2)" + +#: forms.py:165 models.py:288 +#: templates/preferences/display_preferences.html:178 +msgid "Contact email address" +msgstr "Adresse mail de contact" + +#: forms.py:166 templates/preferences/display_preferences.html:182 +msgid "Telephone number" +msgstr "Numéro de téléphone" + +#: forms.py:167 templates/preferences/display_preferences.html:184 +msgid "Usual name" +msgstr "Nom d'usage" + +#: forms.py:168 +msgid "Account used for editing from /admin" +msgstr "Compte utilisé pour les modifications depuis /admin" + +#: forms.py:170 +msgid "Payment" +msgstr "Paiement" + +#: forms.py:171 +msgid "Payment ID" +msgstr "ID de paiement" + +#: forms.py:172 +msgid "Payment password" +msgstr "Mot de passe de paiement" + +#: forms.py:173 forms.py:224 templates/preferences/aff_service.html:33 +msgid "Description" +msgstr "Description" + +#: forms.py:189 +msgid "Message for the French welcome email" +msgstr "Message pour le mail de bienvenue en français" + +#: forms.py:191 +msgid "Message for the English welcome email" +msgstr "Message pour le mail de bienvenue en anglais" + +#: forms.py:208 +msgid "Facebook URL" +msgstr "URL du compte Facebook" + +#: forms.py:209 +msgid "Twitter URL" +msgstr "URL du compte Twitter" + +#: forms.py:210 templates/preferences/display_preferences.html:233 +msgid "Twitter account name" +msgstr "Nom du compte Twitter" + +#: forms.py:222 templates/preferences/aff_service.html:31 +#: templates/preferences/display_preferences.html:168 +msgid "Name" +msgstr "Nom" + +#: forms.py:223 templates/preferences/aff_service.html:32 +msgid "URL" +msgstr "URL" + +#: forms.py:225 templates/preferences/aff_service.html:34 +msgid "Image" +msgstr "Image" + +#: forms.py:232 +msgid "Current services" +msgstr "Services actuels" + +#: models.py:71 +msgid "Users can create a club" +msgstr "Les utilisateurs peuvent créer un club" + +#: models.py:75 +msgid "Users can create a member" +msgstr "Les utilisateurs peuvent créer un adhérent" + +#: models.py:79 +msgid "A new user can create their account on Re2o" +msgstr "Un nouvel utilisateur peut créer son compte sur Re2o" + +#: models.py:89 templates/preferences/display_preferences.html:49 +msgid "Users can edit their shell" +msgstr "Les utilisateurs peuvent modifier leur interface système" + +#: models.py:93 +msgid "Enable local email accounts for users" +msgstr "Active les comptes mail locaux pour les utilisateurs" + +#: models.py:98 +msgid "Domain to use for local email accounts" +msgstr "Domaine à utiliser pour les comptes mail locaux" + +#: models.py:102 +msgid "Maximum number of local email addresses for a standard user" +msgstr "" +"Nombre maximum d'adresses mail locales autorisé pour un utilisateur standard" + +#: models.py:108 +msgid "Can view the user options" +msgstr "Peut voir les options d'utilisateur" + +#: models.py:110 +msgid "user options" +msgstr "options d'utilisateur" + +#: models.py:117 +msgid "Email domain must begin with @" +msgstr "Un domaine mail doit commencer par @" + +#: models.py:135 +msgid "Autoconfiguration by RA" +msgstr "Configuration automatique par RA" + +#: models.py:136 +msgid "IP addresses assigning by DHCPv6" +msgstr "Attribution d'adresses IP par DHCPv6" + +#: models.py:137 +msgid "Disabled" +msgstr "Désactivé" + +#: models.py:159 +msgid "Can view the machine options" +msgstr "Peut voir les options de machine" + +#: models.py:161 +msgid "machine options" +msgstr "options de machine" + +#: models.py:180 +msgid "On the IP range's VLAN of the machine" +msgstr "Sur le VLAN de la plage d'IP de la machine" + +#: models.py:181 +msgid "Preset in 'VLAN for machines accepted by RADIUS'" +msgstr "Prédéfinie dans 'VLAN pour les machines acceptées par RADIUS'" + +#: models.py:206 +msgid "Can view the topology options" +msgstr "Peut voir les options de topologie" + +#: models.py:208 +msgid "topology options" +msgstr "options de topologie" + +#: models.py:225 +msgid "" +"General message displayed on the French version of the website (e.g. in case " +"of maintenance)" +msgstr "" +"Message général affiché sur la version française du site (ex : en cas de " +"maintenance)" + +#: models.py:231 +msgid "" +"General message displayed on the English version of the website (e.g. in " +"case of maintenance)" +msgstr "" +"Message général affiché sur la version anglaise du site (ex : en cas de " +"maintenance)" + +#: models.py:253 +msgid "Can view the general options" +msgstr "Peut voir les options générales" + +#: models.py:255 +msgid "general options" +msgstr "options générales" + +#: models.py:275 +msgid "Can view the service options" +msgstr "Peut voir les options de service" + +#: models.py:277 +msgid "service" +msgstr "service" + +#: models.py:278 +msgid "services" +msgstr "services" + +#: models.py:295 +msgid "Description of the associated email address." msgstr "Description de l'adresse mail associée." -#: models.py:273 -msgid "Can see contact email" -msgstr "Peut voir un mail de contact" +#: models.py:305 +msgid "Can view a contact email address object" +msgstr "Peut voir un objet adresse mail de contact" -#: templates/preferences/aff_mailcontact.html:30 -msgid "Adress" -msgstr "Adresse" +#: models.py:307 +msgid "contact email address" +msgstr "adresse mail de contact" + +#: models.py:308 +msgid "contact email addresses" +msgstr "adresses mail de contact" + +#: models.py:318 +msgid "Networking organisation school Something" +msgstr "Association de réseau de l'école Machin" + +#: models.py:322 +msgid "Threadneedle Street" +msgstr "1 rue de la Vrillière" + +#: models.py:323 +msgid "London EC2R 8AH" +msgstr "75001 Paris" + +#: models.py:326 +msgid "Organisation" +msgstr "Association" + +#: models.py:340 +msgid "Can view the organisation options" +msgstr "Peut voir les options d'association" + +#: models.py:342 +msgid "organisation options" +msgstr "options d'association" + +#: models.py:371 +msgid "Can view the homepage options" +msgstr "Peut voir les options de page d'accueil" + +#: models.py:373 +msgid "homepage options" +msgstr "options de page d'accueil" + +#: models.py:391 +msgid "Can view the email message options" +msgstr "Peut voir les options de message pour les mails" + +#: models.py:394 +msgid "email message options" +msgstr "options de messages pour les mails" #: templates/preferences/aff_mailcontact.html:31 -msgid "Remark" +#: templates/preferences/display_preferences.html:174 +msgid "Address" +msgstr "Adresse" + +#: templates/preferences/aff_mailcontact.html:32 +msgid "Comment" msgstr "Commentaire" -#: templates/preferences/display_preferences.html:205 -msgid "Contact email adresses list" -msgstr "Liste des adresses email de contact" +#: templates/preferences/display_preferences.html:31 +#: templates/preferences/edit_preferences.html:30 +#: templates/preferences/preferences.html:29 +msgid "Preferences" +msgstr "Préférences" + +#: templates/preferences/display_preferences.html:34 +msgid "User preferences" +msgstr "Préférences d'utilisateur" + +#: templates/preferences/display_preferences.html:37 +#: templates/preferences/display_preferences.html:79 +#: templates/preferences/display_preferences.html:104 +#: templates/preferences/display_preferences.html:125 +#: templates/preferences/display_preferences.html:162 +#: templates/preferences/display_preferences.html:197 +#: templates/preferences/display_preferences.html:219 +#: templates/preferences/edit_preferences.html:40 views.py:170 views.py:234 +msgid "Edit" +msgstr "Modifier" + +#: templates/preferences/display_preferences.html:47 +msgid "Default shell for users" +msgstr "Interface système par défaut pour les utilisateurs" + +#: templates/preferences/display_preferences.html:53 +msgid "Creation of members by everyone" +msgstr "Création d'adhérents par tous" + +#: templates/preferences/display_preferences.html:55 +msgid "Creation of clubs by everyone" +msgstr "Création de clubs par tous" + +#: templates/preferences/display_preferences.html:59 +msgid "GPG fingerprint field" +msgstr "Champ empreinte GPG" + +#: templates/preferences/display_preferences.html:63 +msgid "Email accounts preferences" +msgstr "Préférences de comptes mail" + +#: templates/preferences/display_preferences.html:66 +msgid "Local email accounts enabled" +msgstr "Comptes mail locaux activés" + +#: templates/preferences/display_preferences.html:68 +msgid "Local email domain" +msgstr "Domaine de mail local" + +#: templates/preferences/display_preferences.html:72 +msgid "Maximum number of email aliases allowed" +msgstr "Nombre maximum d'alias mail autorisé pour un utilisateur standard" + +#: templates/preferences/display_preferences.html:76 +msgid "Machines preferences" +msgstr "Préférences de machines" + +#: templates/preferences/display_preferences.html:85 +msgid "Password per machine" +msgstr "Mot de passe par machine" + +#: templates/preferences/display_preferences.html:93 +msgid "IPv6 support" +msgstr "Support de l'IPv6" + +#: templates/preferences/display_preferences.html:97 +msgid "Creation of machines" +msgstr "Création de machines" + +#: templates/preferences/display_preferences.html:101 +msgid "Topology preferences" +msgstr "Préférences de topologie" + +#: templates/preferences/display_preferences.html:110 +msgid "General policy for VLAN setting" +msgstr "Politique générale pour le placement sur un VLAN" + +#: templates/preferences/display_preferences.html:112 +msgid "" +"This setting defines the VLAN policy after acceptance by RADIUS: either on " +"the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines " +"accepted by RADIUS'" +msgstr "" +"Ce réglage définit la politique de placement sur un VLAN après acceptation " +"par RADIUS: soit sur le VLAN de la plage d'IP de la machine, soit sur le " +"VLAN prédéfini dans 'VLAN pour les machines acceptées par RADIUS'" + +#: templates/preferences/display_preferences.html:122 +msgid "General preferences" +msgstr "Préférences générales" + +#: templates/preferences/display_preferences.html:139 +msgid "Number of items per page (standard size)" +msgstr "Nombre d'éléments par page (taille standard)" + +#: templates/preferences/display_preferences.html:143 +msgid "Number of items per page (large size)" +msgstr "Nombre d'éléments par page (taille importante)" + +#: templates/preferences/display_preferences.html:149 +msgid "General message displayed on the website" +msgstr "Message général affiché sur le site" + +#: templates/preferences/display_preferences.html:159 +msgid "Information about the organisation" +msgstr "Informations sur l'association" + +#: templates/preferences/display_preferences.html:188 +msgid "User object of the organisation" +msgstr "Objet utilisateur de l'association" + +#: templates/preferences/display_preferences.html:190 +msgid "Description of the organisation" +msgstr "Description de l'association" + +#: templates/preferences/display_preferences.html:194 +msgid "Custom email message" +msgstr "Message personnalisé pour les mails" + +#: templates/preferences/display_preferences.html:203 +msgid "Welcome email (in French)" +msgstr "Mail de bienvenue (en français)" #: templates/preferences/display_preferences.html:207 -msgid "Add an adress" +msgid "Welcome email (in English)" +msgstr "Mail de bienvenue (en anglais)" + +#: templates/preferences/display_preferences.html:211 +msgid "List of services and homepage preferences" +msgstr "Liste des services et préférences de page d'accueil" + +#: templates/preferences/display_preferences.html:213 +msgid " Add a service" +msgstr " Ajouter un service" + +#: templates/preferences/display_preferences.html:215 +msgid " Delete one or several services" +msgstr " Supprimer un ou plusieurs services" + +#: templates/preferences/display_preferences.html:221 +msgid "List of contact email addresses" +msgstr "Liste des adresses mail de contact" + +#: templates/preferences/display_preferences.html:223 +msgid "Add an address" msgstr "Ajouter une adresse" -#: templates/preferences/display_preferences.html:209 -msgid "Delete one or multiple adresses" -msgstr "Supprimer une ou plusieurs adresses" +#: templates/preferences/display_preferences.html:225 +msgid "Delete one or several addresses" +msgstr " Supprimer une ou plusieurs adresses" -#: views.py:210 -msgid "The adress was created." -msgstr "L'adresse a été créée." +#: templates/preferences/display_preferences.html:231 +msgid "Twitter account URL" +msgstr "URL du compte Twitter" -#: views.py:230 -msgid "Email adress updated." -msgstr "L'adresse email a été mise à jour." +#: templates/preferences/display_preferences.html:237 +msgid "Facebook account URL" +msgstr "URL du compte Facebook" -#: views.py:233 -msgid "Edit" -msgstr "Éditer" +#: templates/preferences/edit_preferences.html:35 +msgid "Editing of preferences" +msgstr "Modification des préférences" -#: views.py:251 -msgid "The email adress was deleted." -msgstr "L'adresse email a été supprimée." +#: views.py:98 +msgid "Unknown object" +msgstr "Objet inconnu" -#: views.py:254 +#: views.py:104 +msgid "You don't have the right to edit this option." +msgstr "Vous n'avez pas le droit de modifier cette option." + +#: views.py:121 +msgid "The preferences were edited." +msgstr "Les préférences ont été modifiées." + +#: views.py:140 +msgid "The service was added." +msgstr "Le service a été ajouté." + +#: views.py:143 +msgid "Add a service" +msgstr " Ajouter un service" + +#: views.py:167 +msgid "The service was edited." +msgstr "Le service a été modifié." + +#: views.py:188 +msgid "The service was deleted." +msgstr "Le service a été supprimé." + +#: views.py:190 +#, python-format +msgid "Error: the service %s can't be deleted." +msgstr "Erreur : le service %s ne peut pas être supprimé." + +#: views.py:194 views.py:256 msgid "Delete" msgstr "Supprimer" + +#: views.py:210 +msgid "The contact email address was created." +msgstr "L'adresse mail de contact a été supprimée." + +#: views.py:214 +msgid "Add a contact email address" +msgstr "Ajouter une adresse mail de contact" + +#: views.py:231 +msgid "The contact email address was edited." +msgstr "L'adresse mail de contact a été modifiée." + +#: views.py:253 +msgid "The contact email adress was deleted." +msgstr "L'adresse mail de contact a été supprimée." diff --git a/preferences/migrations/0048_auto_20180811_1515.py b/preferences/migrations/0048_auto_20180811_1515.py new file mode 100644 index 00000000..cf55a660 --- /dev/null +++ b/preferences/migrations/0048_auto_20180811_1515.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-11 13:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0047_mailcontact'), + ] + + operations = [ + migrations.RenameField( + model_name='generaloption', + old_name='general_message', + new_name='general_message_fr', + ), + migrations.AddField( + model_name='generaloption', + name='general_message_en', + field=models.TextField(blank=True, default='', help_text='General message displayed on the English version of the website.'), + ), + migrations.AlterField( + model_name='generaloption', + name='general_message_fr', + field=models.TextField(blank=True, default='', help_text='Message général affiché sur le site (maintenance, etc)'), + ), + ] diff --git a/preferences/migrations/0049_optionaluser_self_change_shell.py b/preferences/migrations/0049_optionaluser_self_change_shell.py new file mode 100644 index 00000000..161792eb --- /dev/null +++ b/preferences/migrations/0049_optionaluser_self_change_shell.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-13 17:18 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0048_auto_20180811_1515'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='self_change_shell', + field=models.BooleanField(default=False, help_text='Users can change their shell'), + ), + ] diff --git a/preferences/migrations/0050_auto_20180818_1329.py b/preferences/migrations/0050_auto_20180818_1329.py new file mode 100644 index 00000000..1cd4a269 --- /dev/null +++ b/preferences/migrations/0050_auto_20180818_1329.py @@ -0,0 +1,146 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-18 11:29 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0049_optionaluser_self_change_shell'), + ] + + operations = [ + migrations.AlterModelOptions( + name='assooption', + options={'permissions': (('view_assooption', 'Can view the organisation options'),), 'verbose_name': 'organisation options'}, + ), + migrations.AlterModelOptions( + name='generaloption', + options={'permissions': (('view_generaloption', 'Can view the general options'),), 'verbose_name': 'general options'}, + ), + migrations.AlterModelOptions( + name='homeoption', + options={'permissions': (('view_homeoption', 'Can view the homepage options'),), 'verbose_name': 'homepage options'}, + ), + migrations.AlterModelOptions( + name='mailcontact', + options={'permissions': (('view_mailcontact', 'Can view a contact email address object'),), 'verbose_name': 'contact email address', 'verbose_name_plural': 'contact email addresses'}, + ), + migrations.AlterModelOptions( + name='mailmessageoption', + options={'permissions': (('view_mailmessageoption', 'Can view the email message options'),), 'verbose_name': 'email message options'}, + ), + migrations.AlterModelOptions( + name='optionalmachine', + options={'permissions': (('view_optionalmachine', 'Can view the machine options'),), 'verbose_name': 'machine options'}, + ), + migrations.AlterModelOptions( + name='optionaltopologie', + options={'permissions': (('view_optionaltopologie', 'Can view the topology options'),), 'verbose_name': 'topology options'}, + ), + migrations.AlterModelOptions( + name='optionaluser', + options={'permissions': (('view_optionaluser', 'Can view the user options'),), 'verbose_name': 'user options'}, + ), + migrations.AlterModelOptions( + name='service', + options={'permissions': (('view_service', 'Can view the service options'),), 'verbose_name': 'service', 'verbose_name_plural': 'services'}, + ), + migrations.AlterField( + model_name='assooption', + name='adresse1', + field=models.CharField(default='Threadneedle Street', max_length=128), + ), + migrations.AlterField( + model_name='assooption', + name='adresse2', + field=models.CharField(default='London EC2R 8AH', max_length=128), + ), + migrations.AlterField( + model_name='assooption', + name='name', + field=models.CharField(default='Networking organisation school Something', max_length=256), + ), + migrations.AlterField( + model_name='assooption', + name='pseudo', + field=models.CharField(default='Organisation', max_length=32), + ), + migrations.AlterField( + model_name='generaloption', + name='general_message_en', + field=models.TextField(blank=True, default='', help_text='General message displayed on the English version of the website (e.g. in case of maintenance)'), + ), + migrations.AlterField( + model_name='generaloption', + name='general_message_fr', + field=models.TextField(blank=True, default='', help_text='General message displayed on the French version of the website (e.g. in case of maintenance)'), + ), + migrations.AlterField( + model_name='homeoption', + name='facebook_url', + field=models.URLField(blank=True, null=True), + ), + migrations.AlterField( + model_name='homeoption', + name='twitter_account_name', + field=models.CharField(blank=True, max_length=32, null=True), + ), + migrations.AlterField( + model_name='homeoption', + name='twitter_url', + field=models.URLField(blank=True, null=True), + ), + migrations.AlterField( + model_name='mailcontact', + name='address', + field=models.EmailField(default='contact@example.org', help_text='Contact email address', max_length=254), + ), + migrations.AlterField( + model_name='mailcontact', + name='commentary', + field=models.CharField(blank=True, help_text='Description of the associated email address.', max_length=256, null=True), + ), + migrations.AlterField( + model_name='optionalmachine', + name='create_machine', + field=models.BooleanField(default=True), + ), + migrations.AlterField( + model_name='optionalmachine', + name='ipv6_mode', + field=models.CharField(choices=[('SLAAC', 'Autoconfiguration by RA'), ('DHCPV6', 'IP addresses assigning by DHCPv6'), ('DISABLED', 'Disabled')], default='DISABLED', max_length=32), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='radius_general_policy', + field=models.CharField(choices=[('MACHINE', "On the IP range's VLAN of the machine"), ('DEFINED', "Preset in 'VLAN for machines accepted by RADIUS'")], default='DEFINED', max_length=32), + ), + migrations.AlterField( + model_name='optionaluser', + name='all_can_create_adherent', + field=models.BooleanField(default=False, help_text='Users can create a member'), + ), + migrations.AlterField( + model_name='optionaluser', + name='all_can_create_club', + field=models.BooleanField(default=False, help_text='Users can create a club'), + ), + migrations.AlterField( + model_name='optionaluser', + name='max_email_address', + field=models.IntegerField(default=15, help_text='Maximum number of local email addresses for a standard user'), + ), + migrations.AlterField( + model_name='optionaluser', + name='self_adhesion', + field=models.BooleanField(default=False, help_text='A new user can create their account on Re2o'), + ), + migrations.AlterField( + model_name='optionaluser', + name='self_change_shell', + field=models.BooleanField(default=False, help_text='Users can edit their shell'), + ), + ] diff --git a/preferences/models.py b/preferences/models.py index 9226bd4a..3199dd6c 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -63,21 +63,20 @@ class PreferencesModel(models.Model): class OptionalUser(AclMixin, PreferencesModel): """Options pour l'user : obligation ou nom du telephone, activation ou non du solde, autorisation du negatif, fingerprint etc""" - PRETTY_NAME = "Options utilisateur" is_tel_mandatory = models.BooleanField(default=True) gpg_fingerprint = models.BooleanField(default=True) all_can_create_club = models.BooleanField( default=False, - help_text="Les users peuvent créer un club" + help_text=_("Users can create a club") ) all_can_create_adherent = models.BooleanField( default=False, - help_text="Les users peuvent créer d'autres adhérents", + help_text=_("Users can create a member"), ) self_adhesion = models.BooleanField( default=False, - help_text="Un nouvel utilisateur peut se créer son compte sur re2o" + help_text=_("A new user can create their account on Re2o") ) shell_default = models.OneToOneField( 'users.ListShell', @@ -85,31 +84,37 @@ class OptionalUser(AclMixin, PreferencesModel): blank=True, null=True ) + self_change_shell = models.BooleanField( + default=False, + help_text=_("Users can edit their shell") + ) local_email_accounts_enabled = models.BooleanField( default=False, - help_text="Enable local email accounts for users" + help_text=_("Enable local email accounts for users") ) local_email_domain = models.CharField( - max_length = 32, - default = "@example.org", - help_text="Domain to use for local email accounts", + max_length=32, + default="@example.org", + help_text=_("Domain to use for local email accounts") ) max_email_address = models.IntegerField( - default = 15, - help_text = "Maximum number of local email address for a standard user" + default=15, + help_text=_("Maximum number of local email addresses for a standard" + " user") ) class Meta: permissions = ( - ("view_optionaluser", "Peut voir les options de l'user"), + ("view_optionaluser", _("Can view the user options")), ) + verbose_name = _("user options") def clean(self): """Clean model: Check the mail_extension """ if self.local_email_domain[0] != "@": - raise ValidationError("Mail domain must begin with @") + raise ValidationError(_("Email domain must begin with @")) @receiver(post_save, sender=OptionalUser) @@ -122,15 +127,14 @@ def optionaluser_post_save(**kwargs): class OptionalMachine(AclMixin, PreferencesModel): """Options pour les machines : maximum de machines ou d'alias par user sans droit, activation de l'ipv6""" - PRETTY_NAME = "Options machines" SLAAC = 'SLAAC' DHCPV6 = 'DHCPV6' DISABLED = 'DISABLED' CHOICE_IPV6 = ( - (SLAAC, 'Autoconfiguration par RA'), - (DHCPV6, 'Attribution des ip par dhcpv6'), - (DISABLED, 'Désactivé'), + (SLAAC, _("Autoconfiguration by RA")), + (DHCPV6, _("IP addresses assigning by DHCPv6")), + (DISABLED, _("Disabled")), ) password_machine = models.BooleanField(default=False) @@ -142,8 +146,7 @@ class OptionalMachine(AclMixin, PreferencesModel): default='DISABLED' ) create_machine = models.BooleanField( - default=True, - help_text="Permet à l'user de créer une machine" + default=True ) @cached_property @@ -153,8 +156,9 @@ class OptionalMachine(AclMixin, PreferencesModel): class Meta: permissions = ( - ("view_optionalmachine", "Peut voir les options de machine"), + ("view_optionalmachine", _("Can view the machine options")), ) + verbose_name = _("machine options") @receiver(post_save, sender=OptionalMachine) @@ -170,13 +174,11 @@ def optionalmachine_post_save(**kwargs): class OptionalTopologie(AclMixin, PreferencesModel): """Reglages pour la topologie : mode d'accès radius, vlan où placer les machines en accept ou reject""" - PRETTY_NAME = "Options topologie" MACHINE = 'MACHINE' DEFINED = 'DEFINED' CHOICE_RADIUS = ( - (MACHINE, 'Sur le vlan de la plage ip machine'), - (DEFINED, 'Prédéfini dans "Vlan où placer les machines\ - après acceptation RADIUS"'), + (MACHINE, _("On the IP range's VLAN of the machine")), + (DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")), ) radius_general_policy = models.CharField( @@ -201,8 +203,9 @@ class OptionalTopologie(AclMixin, PreferencesModel): class Meta: permissions = ( - ("view_optionaltopologie", "Peut voir les options de topologie"), + ("view_optionaltopologie", _("Can view the topology options")), ) + verbose_name = _("topology options") @receiver(post_save, sender=OptionalTopologie) @@ -215,12 +218,18 @@ def optionaltopologie_post_save(**kwargs): class GeneralOption(AclMixin, PreferencesModel): """Options générales : nombre de resultats par page, nom du site, temps où les liens sont valides""" - PRETTY_NAME = "Options générales" - general_message = models.TextField( + general_message_fr = models.TextField( default="", blank=True, - help_text="Message général affiché sur le site (maintenance, etc" + help_text=_("General message displayed on the French version of the" + " website (e.g. in case of maintenance)") + ) + general_message_en = models.TextField( + default="", + blank=True, + help_text=_("General message displayed on the English version of the" + " website (e.g. in case of maintenance)") ) search_display_page = models.IntegerField(default=15) pagination_number = models.IntegerField(default=25) @@ -241,8 +250,9 @@ class GeneralOption(AclMixin, PreferencesModel): class Meta: permissions = ( - ("view_generaloption", "Peut voir les options générales"), + ("view_generaloption", _("Can view the general options")), ) + verbose_name = _("general options") @receiver(post_save, sender=GeneralOption) @@ -262,8 +272,10 @@ class Service(AclMixin, models.Model): class Meta: permissions = ( - ("view_service", "Peut voir les options de service"), + ("view_service", _("Can view the service options")), ) + verbose_name = _("service") + verbose_name_plural =_("services") def __str__(self): return str(self.name) @@ -273,14 +285,14 @@ class MailContact(AclMixin, models.Model): address = models.EmailField( default = "contact@example.org", - help_text = _("Contact email adress") + help_text = _("Contact email address") ) commentary = models.CharField( blank = True, null = True, help_text = _( - "Description of the associated email adress."), + "Description of the associated email address."), max_length = 256 ) @@ -290,8 +302,10 @@ class MailContact(AclMixin, models.Model): class Meta: permissions = ( - ("view_mailcontact", _("Can see contact email")), + ("view_mailcontact", _("Can view a contact email address object")), ) + verbose_name = _("contact email address") + verbose_name_plural = _("contact email addresses") def __str__(self): return(self.address) @@ -299,18 +313,17 @@ class MailContact(AclMixin, models.Model): class AssoOption(AclMixin, PreferencesModel): """Options générales de l'asso : siret, addresse, nom, etc""" - PRETTY_NAME = "Options de l'association" name = models.CharField( - default="Association réseau école machin", + default=_("Networking organisation school Something"), max_length=256 ) siret = models.CharField(default="00000000000000", max_length=32) - adresse1 = models.CharField(default="1 Rue de exemple", max_length=128) - adresse2 = models.CharField(default="94230 Cachan", max_length=128) + adresse1 = models.CharField(default=_("Threadneedle Street"), max_length=128) + adresse2 = models.CharField(default=_("London EC2R 8AH"), max_length=128) contact = models.EmailField(default="contact@example.org") telephone = models.CharField(max_length=15, default="0000000000") - pseudo = models.CharField(default="Asso", max_length=32) + pseudo = models.CharField(default=_("Organisation"), max_length=32) utilisateur_asso = models.OneToOneField( 'users.User', on_delete=models.PROTECT, @@ -324,8 +337,9 @@ class AssoOption(AclMixin, PreferencesModel): class Meta: permissions = ( - ("view_assooption", "Peut voir les options de l'asso"), + ("view_assooption", _("Can view the organisation options")), ) + verbose_name = _("organisation options") @receiver(post_save, sender=AssoOption) @@ -337,29 +351,26 @@ def assooption_post_save(**kwargs): class HomeOption(AclMixin, PreferencesModel): """Settings of the home page (facebook/twitter etc)""" - PRETTY_NAME = "Options de la page d'accueil" facebook_url = models.URLField( null=True, - blank=True, - help_text="Url du compte facebook" + blank=True ) twitter_url = models.URLField( null=True, - blank=True, - help_text="Url du compte twitter" + blank=True ) twitter_account_name = models.CharField( max_length=32, null=True, - blank=True, - help_text="Nom du compte à afficher" + blank=True ) class Meta: permissions = ( - ("view_homeoption", "Peut voir les options de l'accueil"), + ("view_homeoption", _("Can view the homepage options")), ) + verbose_name = _("homepage options") @receiver(post_save, sender=HomeOption) @@ -371,12 +382,14 @@ def homeoption_post_save(**kwargs): class MailMessageOption(AclMixin, models.Model): """Reglages, mail de bienvenue et autre""" - PRETTY_NAME = "Options de corps de mail" welcome_mail_fr = models.TextField(default="") welcome_mail_en = models.TextField(default="") class Meta: permissions = ( - ("view_mailmessageoption", "Peut voir les options de mail"), + ("view_mailmessageoption", _("Can view the email message" + " options")), ) + verbose_name = _("email message options") + diff --git a/preferences/templates/preferences/aff_mailcontact.html b/preferences/templates/preferences/aff_mailcontact.html index a87e03bb..98ee8cdc 100644 --- a/preferences/templates/preferences/aff_mailcontact.html +++ b/preferences/templates/preferences/aff_mailcontact.html @@ -24,24 +24,26 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load i18n %} {% load acl %} {% load logs_extra %} - - - - - - - - - {% for mailcontact in mailcontact_list %} + +
{% trans "Adress" %}{% trans "Remark" %}
+ + + + + + + + {% for mailcontact in mailcontact_list %} - {% endfor %} -
{% trans "Address" %}{% trans "Comment" %}
{{ mailcontact.address }} {{ mailcontact.commentary }} - {% can_edit mailcontact %} - {% include 'buttons/edit.html' with href='preferences:edit-mailcontact' id=mailcontact.id %} - {% acl_end %} - {% history_button mailcontact %} + {% can_edit mailcontact %} + {% include 'buttons/edit.html' with href='preferences:edit-mailcontact' id=mailcontact.id %} + {% acl_end %} + {% history_button mailcontact %}
+ {% endfor %} + + diff --git a/preferences/templates/preferences/aff_service.html b/preferences/templates/preferences/aff_service.html index 89cfc641..c08e14e0 100644 --- a/preferences/templates/preferences/aff_service.html +++ b/preferences/templates/preferences/aff_service.html @@ -23,30 +23,31 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load acl %} {% load logs_extra %} - - - - - - - - - - - - {% for service in service_list %} +{% load i18n %} + +
NomUrlDescriptionImage
+ + + + + + + + + + {% for service in service_list %} - {% endfor %} -
{% trans "Name" %}{% trans "URL" %}{% trans "Description" %}{% trans "Image" %}
{{ service.name }} {{ service.url }} {{ service.description }} {{ service.image }} - {% can_edit service%} - {% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %} - {% acl_end %} - {% history_button service %} + {% can_edit service%} + {% include 'buttons/edit.html' with href='preferences:edit-service' id=service.id %} + {% acl_end %} + {% history_button service %}
+ {% endfor %} + diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 2e34db5a..6a499969 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -28,218 +28,215 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load design %} {% load i18n %} -{% block title %}Création et modification des préférences{% endblock %} +{% block title %}{% trans "Preferences" %}{% endblock %} {% block content %} -

Préférences utilisateur

- - Editer - - -
Généralités
- - - - - - - - - - - - - - - - - - - -
Téléphone obligatoirement requis{{ useroptions.is_tel_mandatory|tick }}Auto inscription{{ useroptions.self_adhesion|tick }}
Champ gpg fingerprint{{ useroptions.gpg_fingerprint|tick }}Shell par défaut des utilisateurs{{ useroptions.shell_default }}
Creations d'adhérents par tous{{ useroptions.all_can_create_adherent|tick }}Creations de clubs par tous{{ useroptions.all_can_create_club|tick }}
- -
Comptes mails
- - - - - - - - - - - -
Gestion des comptes mails{{ useroptions.local_email_accounts_enabled | tick }}Extension mail interne{{ useroptions.local_email_domain }}
Nombre d'alias mail max{{ useroptions.max_email_address }}
- - -

Préférences machines

- - Editer - - - - - - - - - - - - - - - - - - -
Mot de passe par machine{{ machineoptions.password_machine|tick }}Machines/interfaces autorisées par utilisateurs{{ machineoptions.max_lambdauser_interfaces }}
Alias dns autorisé par utilisateur{{ machineoptions.max_lambdauser_aliases }}Support de l'ipv6{{ machineoptions.ipv6_mode }}
Creation de machines{{ machineoptions.create_machine|tick }}
- - -

Préférences topologie

- - Editer - - - - - - - - - - - - - - -
Politique générale de placement de vlan{{ topologieoptions.radius_general_policy }} - Ce réglage défini la politique vlan après acceptation radius : - soit sur le vlan de la plage d'ip de la machine, soit sur un - vlan prédéfini dans "Vlan où placer les machines après acceptation - RADIUS" -
Vlan où placer les machines après acceptation RADIUS{{ topologieoptions.vlan_decision_ok }}Vlan où placer les machines après rejet RADIUS{{ topologieoptions.vlan_decision_nok }}
- - -

Préférences generales

- - Editer - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nom du site web{{ generaloptions.site_name }}Adresse mail d'expedition automatique{{ generaloptions.email_from }}
Affichage de résultats dans le champ de recherche{{ generaloptions.search_display_page }}Nombre d'items affichés en liste (taille normale){{ generaloptions.pagination_number }}
Nombre d'items affichés en liste (taille élevée){{ generaloptions.pagination_large_number }}Temps avant expiration du lien de reinitialisation de mot de passe (en heures){{ generaloptions.req_expire_hrs }}
Message global affiché sur le site{{ generaloptions.general_message }}Résumé des CGU{{ generaloptions.GTU_sum_up }}
CGU{{generaloptions.GTU}} -
- - -

Données de l'association

- - Editer - - - - - - - - - - - - - - - - - - - - - - - - - - -
Nom{{ assooptions.name }}SIRET{{ assooptions.siret }}
Adresse{{ assooptions.adresse1 }}
- {{ assooptions.adresse2 }}
Contact mail{{ assooptions.contact }}
Telephone{{ assooptions.telephone }}Pseudo d'usage{{ assooptions.pseudo }}
Objet utilisateur de l'association{{ assooptions.utilisateur_asso }}Description de l'association{{ assooptions.description | safe }}
- - -

Messages personalisé dans les mails

- - Editer - - - - - - - - - - -
Mail de bienvenue (Français){{ mailmessageoptions.welcome_mail_fr | safe }}
Mail de bienvenue (Anglais){{ mailmessageoptions.welcome_mail_en | safe }}
- - -

Liste des services et préférences page d'accueil

-{% can_create preferences.Service%} - - Ajouter un service - -{% acl_end %} - - Supprimer un ou plusieurs service - -{% include "preferences/aff_service.html" with service_list=service_list %} - - - Editer - - -

{% trans "Contact email adresses list" %}

- {% can_create preferences.MailContact%} - {% trans "Add an adress" %} - {% acl_end %} - {% trans "Delete one or multiple adresses" %} - {% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %} -

- - - - - - - - - - - - -
Url du compte twitter{{ homeoptions.twitter_url }}Nom utilisé pour afficher le compte{{ homeoptions.twitter_account_name }}
Url du compte facebook{{ homeoptions.facebook_url }}
+

{% trans "User preferences" %}

+ + + {% trans "Edit" %} + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Telephone number required" %}{{ useroptions.is_tel_mandatory|tick }}{% trans "Self registration" %}{{ useroptions.self_adhesion|tick }}
{% trans "Default shell for users" %}{{ useroptions.shell_default }}{% trans "Users can edit their shell" %}{{ useroptions.self_change_shell|tick }}
{% trans "Creation of members by everyone" %}{{ useroptions.all_can_create_adherent|tick }}{% trans "Creation of clubs by everyone" %}{{ useroptions.all_can_create_club|tick }}
{% trans "GPG fingerprint field" %}{{ useroptions.gpg_fingerprint|tick }}
+
{% trans "Email accounts preferences" %} + + + + + + + + + + + +
{% trans "Local email accounts enabled" %}{{ useroptions.local_email_accounts_enabled|tick }}{% trans "Local email domain" %}{{ useroptions.local_email_domain }}
{% trans "Maximum number of email aliases allowed" %}{{ useroptions.max_email_address }}
+

{% trans "Machines preferences" %}

+ + + {% trans "Edit" %} + +

+

+ + + + + + + + + + + + + + + + + +
{% trans "Password per machine" %}{{ machineoptions.password_machine|tick }}{% trans "Maximum number of interfaces allowed for a standard user" %}{{ machineoptions.max_lambdauser_interfaces }}
{% trans "Maximum number of DNS aliases allowed for a standard user" %}{{ machineoptions.max_lambdauser_aliases }}{% trans "IPv6 support" %}{{ machineoptions.ipv6_mode }}
{% trans "Creation of machines" %}{{ machineoptions.create_machine|tick }}
+

{% trans "Topology preferences" %}

+ + + {% trans "Edit" %} + +

+

+ + + + + + + + + + + + + +
{% trans "General policy for VLAN setting" %}{{ topologieoptions.radius_general_policy }}{% trans "This setting defines the VLAN policy after acceptance by RADIUS: either on the IP range's VLAN of the machine, or a VLAN preset in 'VLAN for machines accepted by RADIUS'" %}
{% trans "VLAN for machines accepted by RADIUS" %}{{ topologieoptions.vlan_decision_ok }}{% trans "VLAN for machines rejected by RADIUS" %}{{ topologieoptions.vlan_decision_nok }}
+

{% trans "General preferences" %}

+ + + {% trans "Edit" %} + +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Website name" %}{{ generaloptions.site_name }}{% trans "Email address for automatic emailing" %}{{ generaloptions.email_from }}
{% trans "Number of results displayed when searching" %}{{ generaloptions.search_display_page }}{% trans "Number of items per page (standard size)" %}{{ generaloptions.pagination_number }}
{% trans "Number of items per page (large size)" %}{{ generaloptions.pagination_large_number }}{% trans "Time before expiration of the reset password link (in hours)" %}{{ generaloptions.req_expire_hrs }}
{% trans "General message displayed on the website" %}{{ generaloptions.general_message }}{% trans "Summary of the General Terms of Use" %}{{ generaloptions.GTU_sum_up }}
{% trans "General Terms of Use" %}{{ generaloptions.GTU }} +
+

{% trans "Information about the organisation" %}

+ + + {% trans "Edit" %} + +

+

+ + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Name" %}{{ assooptions.name }}{% trans "SIRET number" %}{{ assooptions.siret }}
{% trans "Address" %}{{ assooptions.adresse1 }}
+ {{ assooptions.adresse2 }} +
{% trans "Contact email address" %}{{ assooptions.contact }}
{% trans "Telephone number" %}{{ assooptions.telephone }}{% trans "Usual name" %}{{ assooptions.pseudo }}
{% trans "User object of the organisation" %}{{ assooptions.utilisateur_asso }}{% trans "Description of the organisation" %}{{ assooptions.description|safe }}
+

{% trans "Custom email message" %}

+ + + {% trans "Edit" %} + +

+

+ + + + + + + + + +
{% trans "Welcome email (in French)" %}{{ mailmessageoptions.welcome_mail_fr|safe }}
{% trans "Welcome email (in English)" %}{{ mailmessageoptions.welcome_mail_en|safe }}
+

{% trans "List of services and homepage preferences" %}

+ {% can_create preferences.Service%} + {% trans " Add a service" %} + {% acl_end %} + {% trans " Delete one or several services" %} + {% include "preferences/aff_service.html" with service_list=service_list %} + + + {% trans "Edit" %} + +

{% trans "List of contact email addresses" %}

+ {% can_create preferences.MailContact %} + {% trans "Add an address" %} + {% acl_end %} + {% trans "Delete one or several addresses" %} + {% include "preferences/aff_mailcontact.html" with mailcontact_list=mailcontact_list %} +

+

+ + + + + + + + + + + +
{% trans "Twitter account URL" %}{{ homeoptions.twitter_url }}{% trans "Twitter account name" %}{{ homeoptions.twitter_account_name }}
{% trans "Facebook account URL" %}{{ homeoptions.facebook_url }}
{% endblock %} + diff --git a/preferences/templates/preferences/edit_preferences.html b/preferences/templates/preferences/edit_preferences.html index 055ac7e8..356f2362 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -25,20 +25,23 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load massive_bootstrap_form %} +{% load i18n %} -{% block title %}Création et modification des préférences{% endblock %} +{% block title %}{% trans "Preferences" %}{% endblock %} {% block content %} {% bootstrap_form_errors options %} -

Edition des préférences

+

{% trans "Editing of preferences" %}

-{% csrf_token %} -{% massive_bootstrap_form options 'utilisateur_asso' %} -{% bootstrap_button "Modifier" button_type="submit" icon="star" %} + {% csrf_token %} + {% massive_bootstrap_form options 'utilisateur_asso' %} + {% trans "Edit" as tr_edit %} + {% bootstrap_button tr_edit button_type="submit" icon="star" %} -
-
-
+
+
+
{% endblock %} + diff --git a/preferences/templates/preferences/preferences.html b/preferences/templates/preferences/preferences.html index e8972d8d..e4ad4ba2 100644 --- a/preferences/templates/preferences/preferences.html +++ b/preferences/templates/preferences/preferences.html @@ -24,8 +24,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Création et modification des preferences{% endblock %} +{% block title %}{% trans "Preferences" %}{% endblock %} {% block content %} {% if preferenceform %} @@ -44,3 +45,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% endblock %} + diff --git a/preferences/templatetags/__init__.py b/preferences/templatetags/__init__.py index 86d112b2..c0a03b85 100644 --- a/preferences/templatetags/__init__.py +++ b/preferences/templatetags/__init__.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- #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. diff --git a/preferences/views.py b/preferences/views.py index 3c0c4879..559cdfef 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -95,14 +95,14 @@ def edit_options(request, section): model = getattr(models, section, None) form_instance = getattr(forms, 'Edit' + section + 'Form', None) if not (model or form_instance): - messages.error(request, "Objet inconnu") + messages.error(request, _("Unknown object")) return redirect(reverse('preferences:display-options')) options_instance, _created = model.objects.get_or_create() can, msg = options_instance.can_edit(request.user) if not can: - messages.error(request, msg or "Vous ne pouvez pas éditer cette\ - option.") + messages.error(request, msg or _("You don't have the right to edit" + " this option.")) return redirect(reverse('index')) options = form_instance( request.POST or None, @@ -114,11 +114,11 @@ def edit_options(request, section): options.save() reversion.set_user(request.user) reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( + "Field(s) edited: %s" % ', '.join( field for field in options.changed_data ) ) - messages.success(request, "Préférences modifiées") + messages.success(request, _("The preferences were edited.")) return redirect(reverse('preferences:display-options')) return form( {'options': options}, @@ -136,11 +136,11 @@ def add_service(request): with transaction.atomic(), reversion.create_revision(): service.save() reversion.set_user(request.user) - reversion.set_comment("Création") - messages.success(request, "Ce service a été ajouté") + reversion.set_comment("Creation") + messages.success(request, _("The service was added.")) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': service, 'action_name': 'Ajouter'}, + {'preferenceform': service, 'action_name': _("Add a service")}, 'preferences/preferences.html', request ) @@ -160,14 +160,14 @@ def edit_service(request, service_instance, **_kwargs): service.save() reversion.set_user(request.user) reversion.set_comment( - "Champs modifié(s) : %s" % ', '.join( + "Field(s) edited: %s" % ', '.join( field for field in service.changed_data ) ) - messages.success(request, "Service modifié") + messages.success(request, _("The service was edited.")) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': service, 'action_name': 'Editer'}, + {'preferenceform': service, 'action_name': _("Edit")}, 'preferences/preferences.html', request ) @@ -185,13 +185,13 @@ def del_service(request, instances): with transaction.atomic(), reversion.create_revision(): services_del.delete() reversion.set_user(request.user) - messages.success(request, "Le service a été supprimé") + messages.success(request, _("The service was deleted.")) except ProtectedError: - messages.error(request, "Erreur le service\ - suivant %s ne peut être supprimé" % services_del) + messages.error(request, _("Error: the service %s can't be" + " deleted.") % services_del) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': services, 'action_name': 'Supprimer'}, + {'preferenceform': services, 'action_name': _("Delete")}, 'preferences/preferences.html', request ) @@ -207,10 +207,11 @@ def add_mailcontact(request): ) if mailcontact.is_valid(): mailcontact.save() - messages.success(request, _("The adress was created.")) + messages.success(request, _("The contact email address was created.")) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': mailcontact, 'action_name': 'Ajouter'}, + {'preferenceform': mailcontact, + 'action_name': _("Add a contact email address")}, 'preferences/preferences.html', request ) @@ -227,10 +228,10 @@ def edit_mailcontact(request, mailcontact_instance, **_kwargs): ) if mailcontact.is_valid(): mailcontact.save() - messages.success(request, _("Email adress updated.")) + messages.success(request, _("The contact email address was edited.")) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': mailcontact, 'action_name': _('Edit')}, + {'preferenceform': mailcontact, 'action_name': _("Edit")}, 'preferences/preferences.html', request ) @@ -248,10 +249,12 @@ def del_mailcontact(request, instances): mailcontacts_dels = mailcontacts.cleaned_data['mailcontacts'] for mailcontacts_del in mailcontacts_dels: mailcontacts_del.delete() - messages.success(request, _("The email adress was deleted.")) + messages.success(request, + _("The contact email adress was deleted.")) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': mailcontacts, 'action_name': _('Delete')}, + {'preferenceform': mailcontacts, 'action_name': _("Delete")}, 'preferences/preferences.html', request ) + diff --git a/re2o/acl.py b/re2o/acl.py index bd304443..116d273b 100644 --- a/re2o/acl.py +++ b/re2o/acl.py @@ -34,6 +34,7 @@ from django.db.models import Model from django.contrib import messages from django.shortcuts import redirect from django.urls import reverse +from django.utils.translation import ugettext as _ def acl_base_decorator(method_name, *targets, on_instance=True): @@ -138,7 +139,7 @@ ModelC) target = target.get_instance(*args, **kwargs) instances.append(target) except target.DoesNotExist: - yield False, u"Entrée inexistante" + yield False, _("Nonexistent entry.") return if hasattr(target, method_name): can_fct = getattr(target, method_name) @@ -155,7 +156,8 @@ ModelC) if error_messages: for msg in error_messages: messages.error( - request, msg or "Vous ne pouvez pas accéder à ce menu") + request, msg or _("You don't have the right to access" + " this menu.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} @@ -219,7 +221,8 @@ def can_delete_set(model): instances = model.objects.filter(id__in=instances_id) if not instances: messages.error( - request, "Vous ne pouvez pas accéder à ce menu") + request, _("You don't have the right to access this menu.") + ) return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} @@ -268,10 +271,11 @@ def can_edit_history(view): return view(request, *args, **kwargs) messages.error( request, - "Vous ne pouvez pas éditer l'historique." + _("You don't have the right to edit the history.") ) return redirect(reverse( 'users:profil', kwargs={'userid': str(request.user.id)} )) return wrapper + diff --git a/re2o/context_processors.py b/re2o/context_processors.py index ceb03be2..6beac564 100644 --- a/re2o/context_processors.py +++ b/re2o/context_processors.py @@ -26,17 +26,24 @@ from __future__ import unicode_literals import datetime from django.contrib import messages - +from django.http import HttpRequest from preferences.models import GeneralOption, OptionalMachine +from django.utils.translation import get_language def context_user(request): """Fonction de context lorsqu'un user est logué (ou non), renvoie les infos sur l'user, la liste de ses droits, ses machines""" user = request.user - global_message = GeneralOption.get_cached_value('general_message') + if get_language()=='fr': + global_message = GeneralOption.get_cached_value('general_message_fr') + else: + global_message = GeneralOption.get_cached_value('general_message_en') if global_message: - messages.warning(request, global_message) + if isinstance(request, HttpRequest): + messages.warning(request, global_message) + else: + messages.warning(request._request, global_message) if user.is_authenticated(): interfaces = user.user_interfaces() else: diff --git a/re2o/contributors.py b/re2o/contributors.py index 951acc47..ac663d9c 100644 --- a/re2o/contributors.py +++ b/re2o/contributors.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- """re2o.contributors A list of the proud contributors to Re2o """ @@ -26,5 +27,7 @@ CONTRIBUTORS = [ 'Hugo "Shaka" Hervieux', '"Mikachu"', 'Thomas "Nymous" Gaudin', - '"Esum"' + 'Benjamin "Esum" Graillot', + 'Gabriel "Boudy" Le Bouder', + 'Charlie "Le membre" Jacomme', ] diff --git a/re2o/locale/fr/LC_MESSAGES/django.mo b/re2o/locale/fr/LC_MESSAGES/django.mo index c0bdc974..72cde111 100644 Binary files a/re2o/locale/fr/LC_MESSAGES/django.mo and b/re2o/locale/fr/LC_MESSAGES/django.mo differ diff --git a/re2o/locale/fr/LC_MESSAGES/django.po b/re2o/locale/fr/LC_MESSAGES/django.po index cd293d53..f54fdcc1 100644 --- a/re2o/locale/fr/LC_MESSAGES/django.po +++ b/re2o/locale/fr/LC_MESSAGES/django.po @@ -21,153 +21,233 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-06-23 18:26+0200\n" +"POT-Creation-Date: 2018-08-23 15:03+0200\n" "PO-Revision-Date: 2018-03-31 16:09+0002\n" -"Last-Translator: Maël Kervella \n" +"Last-Translator: Laouen Fernet \n" "Language-Team: \n" "Language: fr_FR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: settings.py:140 +#: acl.py:142 +msgid "Nonexistent entry." +msgstr "Entrée inexistante." + +#: acl.py:159 acl.py:224 +msgid "You don't have the right to access this menu." +msgstr "Vous n'avez pas le droit d'accéder à ce menu." + +#: acl.py:274 +msgid "You don't have the right to edit the history." +msgstr "Vous n'avez pas le droit de modifier l'historique." + +#: mixins.py:111 +#, python-format +msgid "You don't have the right to create a %s object." +msgstr "Vous n'avez pas le droit de créer un objet %s." + +#: mixins.py:125 +#, python-format +msgid "You don't have the right to edit a %s object." +msgstr "Vous n'avez pas le droit de modifier un objet %s." + +#: mixins.py:139 +#, python-format +msgid "You don't have the right to delete a %s object." +msgstr "Vous n'avez pas le droit de supprimer un objet %s." + +#: mixins.py:153 +#, python-format +msgid "You don't have the right to view every %s object." +msgstr "Vous n'avez pas le droit de voir tous les objets %s." + +#: mixins.py:167 +#, python-format +msgid "You don't have the right to view a %s object." +msgstr "Vous n'avez pas le droit de voir un objet %s." + +#: settings.py:155 msgid "English" msgstr "Anglais" -#: settings.py:141 +#: settings.py:156 msgid "French" msgstr "Français" #: templates/re2o/about.html:29 templates/re2o/about.html:35 msgid "About Re2o" -msgstr "A propos de Re2o" +msgstr "À propos de Re2o" #: templates/re2o/about.html:32 #, python-format msgid "About %(AssoName)s" -msgstr "A propos de %(AssoName)s" +msgstr "À propos de %(AssoName)s" #: templates/re2o/about.html:36 msgid "" -"\n" -" Re2o is an administration tool initiated by\n" -" Rezo Supelec Metz and a few\n" -" members of other FedeRez " -"associations\n" -" around the summer 2016.
\n" -" It is intended to be a tool independant from any network " -"infrastructure\n" -" so it can be setup in \"a few steps\". This tool is entirely free " -"and\n" -" available under a GNU Public License v2 (GPLv2) license on\n" -" FedeRez gitlab.
\n" -" Re2o's mainteners are proud volunteers mainly from French " -"engineering\n" -" schools (but not limited to) who have given a lot of their time to " -"make\n" -" this project possible. So please be kind with them.
\n" -" If you want to get involved in the development process, we will be " -"glad to\n" -" welcome you so do not hesitate to contact us and come help us build " -"the\n" -" future of Re2o.\n" -" " +"Re2o is an administration tool initiated by
Rezo Supelec Metz and a few members of other FedeRez associations around the summer 2016.
It is " +"intended to be a tool independant from any network infrastructure so it can " +"be setup in \"a few steps\". This tool is entirely free and available under " +"a GNU Public License v2 (GPLv2) license on FedeRez gitlab.
Re2o's mainteners are " +"volunteers mainly from French schools.
If you want to get involved in " +"the development process, we will be glad to welcome you so do not hesitate " +"to contact us and come help us build the future of Re2o." msgstr "" -"\n" -" Re2o est un outil d'administration initié par\n" -" Rezo Supelec Metz et quelques\n" -" membres d'autres assocations de FedeRez\n" -" autour de l'été 2016.
\n" -" Il se veut être un outil idépendant de toute infrastructure réseau\n" -" pour pouvoir être installé en \"quelques étapes\". Cet outil est " -"entièrement gratuit et\n" -" est disponible sous license GNU Public License v2 (GPLv2) sur le\n" -" gitlab de " -"FedeRez.
\n" -" Les mainteneurs de Re2o sont de fiers bénévoles venant " -"principalement d'écoles d'ingénieurs françaises\n" -" (mais pas seulement) qui ont donné beaucoup de leur temps pour faire " -"en sorte que\n" -" ce projet soit possible. Donc s'il vous plait soyez gentils avez eux." -"
\n" -" Si vous voulez prendre part au développement, nous serons heureux " -"de\n" -" vous accueillir donc n'hésitez pas à nous contacter et à venir nous " -"aider à construire le\n" -" futur de Re2o.\n" -" " +"Re2o est un outil d'administration initié par Rézo Supélec Metz et quelques membres d'autres assocations de FedeRez autour de l'été 2016.
Il se veut " +"être un outil indépendant de toute infrastructure réseau pour pouvoir être " +"installé en \"quelques étapes\". Cet outil est entièrement gratuit et est " +"disponible sous license GNU Public License v2 (GPLv2) sur legitlab de FedeRez.
\n" +"Les mainteneurs de Re2o sont de fiers bénévoles venant principalement " +"d'écoles d'ingénieurs françaises (mais pas seulement) qui ont donné beaucoup " +"de leur temps pour faire en sorte que ce projet soit possible. Donc s'il " +"vous plait soyez gentils avez eux.
Si vous voulez prendre part au " +"développement, nous serons heureux de vous accueillir donc n'hésitez pas à " +"nous contacter et à venir nous aider à construire le futur de Re2o." -#: templates/re2o/about.html:57 +#: templates/re2o/about.html:55 msgid "Contributors list" msgstr "Liste des contributeurs" -#: templates/re2o/about.html:66 -msgid "Version informations" +#: templates/re2o/about.html:64 +msgid "Version information" msgstr "Informations de versions" -#: templates/re2o/about.html:68 +#: templates/re2o/about.html:66 #, python-format -msgid "" -"\n" -" Remote URL: %(git_info_remote)s\n" -" " -msgstr "" -"\n" -" URL distante : %(git_info_remote)s\n" -" " +msgid "Remote URL: %(git_info_remote)s" +msgstr "URL distante : %(git_info_remote)s" -#: templates/re2o/about.html:71 +#: templates/re2o/about.html:69 #, python-format -msgid "" -"\n" -" Branch: %(git_info_branch)s\n" -" " -msgstr "" -"\n" -" Branche : %(git_info_branch)s\n" -" " +msgid "Branch: %(git_info_branch)s" +msgstr "Branche : %(git_info_branch)s" -#: templates/re2o/about.html:74 +#: templates/re2o/about.html:72 #, python-format -msgid "" -"\n" -" Commit: %(git_info_commit)s\n" -" " -msgstr "" -"\n" -" Commit : %(git_info_commit)s\n" -" " +msgid "Commit: %(git_info_commit)s" +msgstr "Commit : %(git_info_commit)s" -#: templates/re2o/about.html:77 +#: templates/re2o/about.html:75 #, python-format -msgid "" -"\n" -" Commit date: %(git_info_commit_date)s\n" -" " -msgstr "" -"\n" -" Date du commit : %(git_info_commit_date)s\n" -" " +msgid "Commit date: %(git_info_commit_date)s" +msgstr "Date du commit : %(git_info_commit_date)s" -#: templates/re2o/about.html:82 +#: templates/re2o/about.html:80 msgid "Dependencies" msgstr "Dépendances" +#: templates/re2o/aff_history.html:30 +msgid "Next" +msgstr "Suivant" + +#: templates/re2o/aff_history.html:37 +msgid "Previous" +msgstr "Précédent" + +#: templates/re2o/aff_history.html:45 +msgid "Date" +msgstr "Date" + +#: templates/re2o/aff_history.html:46 +msgid "Performed by" +msgstr "Effectuée par" + +#: templates/re2o/aff_history.html:47 +msgid "Comment" +msgstr "Commentaire" + +#: templates/re2o/contact.html:29 +msgid "Contact" +msgstr "Contact" + +#: templates/re2o/contact.html:32 +#, python-format +msgid "Contact the organisation %(asso_name)s" +msgstr "Contacter l'association %(asso_name)s" + +#: templates/re2o/history.html:29 +msgid "History" +msgstr "Historique" + +#: templates/re2o/history.html:32 +#, python-format +msgid "History of %(object)s" +msgstr "Historique de %(object)s" + #: templates/re2o/index.html:30 msgid "Home" msgstr "Accueil" -#: templates/re2o/index.html:33 +#: templates/re2o/index.html:34 #, python-format -msgid "Welcome to %(name_website)s !" -msgstr "Bienvenue sur %(name_website)s !" +msgid "Welcome to %(name_website)s" +msgstr "Bienvenue sur %(name_website)s" -#: templates/re2o/index.html:47 +#: templates/re2o/index.html:43 templates/re2o/index.html:45 +msgid "Registration" +msgstr "Inscription" + +#: templates/re2o/index.html:44 +msgid "" +"If you don't have an account yet and you want to access the Internet and the " +"organisation's services, create your own personal account." +msgstr "" +"Si vous n'avez pas encore de compte et que vous voulez accéder à Internet et " +"aux services de l'association, créez votre espace personnel." + +#: templates/re2o/index.html:55 templates/re2o/index.html:57 +msgid "Logging in" +msgstr "Identification" + +#: templates/re2o/index.html:56 +msgid "" +"If you already have an account, log in. You can manage your subscription to " +"the organisation, your machines and all your services." +msgstr "" +"Si vous avez déjà un compte, identifiez-vous. Vous pouvez gérer votre " +"cotisation à l'association, vos machines et tous vos services." + +#: templates/re2o/index.html:68 +msgid "My profile" +msgstr "Mon profil" + +#: templates/re2o/index.html:69 +msgid "" +"To manage your subscription, your machines and all your services, access " +"your profile." +msgstr "" +"Pour gérer votre cotisation, vos machines et tous vos services, accéder à " +"votre profil." + +#: templates/re2o/index.html:70 +msgid "Access my profile" +msgstr "Accéder à mon profil" + +#: templates/re2o/index.html:79 +msgid "Services of the organisation" +msgstr "Serices de l'association" + +#: templates/re2o/index.html:93 msgid "Go there" msgstr "Accéder" -#: views.py:205 +#: templates/re2o/sidebar.html:47 +#, python-format +msgid "Tweets from @%(twitter_account_name)s" +msgstr "Tweets de @%(twitter_account_name)s" + +#: templates/re2o/sidebar.html:50 +#, python-format +msgid "Follow @%(twitter_account_name)s" +msgstr "Suivre @%(twitter_account_name)s" + +#: views.py:87 msgid "Unable to get the information" msgstr "Impossible d'obtenir l'information" diff --git a/re2o/login.py b/re2o/login.py index b867e836..471c2e02 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -28,14 +28,15 @@ Module in charge of handling the login process and verifications """ -import hashlib import binascii +import crypt +import hashlib import os -from base64 import encodestring -from base64 import decodestring +from base64 import encodestring, decodestring, b64encode, b64decode from collections import OrderedDict - from django.contrib.auth import hashers +from django.contrib.auth.backends import ModelBackend +from hmac import compare_digest as constant_time_compare ALGO_NAME = "{SSHA}" @@ -64,17 +65,127 @@ def checkPassword(challenge_password, password): salt = challenge_bytes[DIGEST_LEN:] hr = hashlib.sha1(password.encode()) hr.update(salt) - valid_password = True - # La comparaison est volontairement en temps constant - # (pour éviter les timing-attacks) - for i, j in zip(digest, hr.digest()): - valid_password &= i == j - return valid_password + return constant_time_compare(digest, hr.digest()) +def hash_password_salt(hashed_password): + """ Extract the salt from a given hashed password """ + if hashed_password.upper().startswith('{CRYPT}'): + hashed_password = hashed_password[7:] + if hashed_password.startswith('$'): + return '$'.join(hashed_password.split('$')[:-1]) + else: + return hashed_password[:2] + elif hashed_password.upper().startswith('{SSHA}'): + try: + digest = b64decode(hashed_password[6:]) + except TypeError as error: + raise ValueError("b64 error for `hashed_password` : %s" % error) + if len(digest) < 20: + raise ValueError("`hashed_password` too short") + return digest[20:] + elif hashed_password.upper().startswith('{SMD5}'): + try: + digest = b64decode(hashed_password[7:]) + except TypeError as error: + raise ValueError("b64 error for `hashed_password` : %s" % error) + if len(digest) < 16: + raise ValueError("`hashed_password` too short") + return digest[16:] + else: + raise ValueError("`hashed_password` should start with '{SSHA}' or '{CRYPT}' or '{SMD5}'") + + + +class CryptPasswordHasher(hashers.BasePasswordHasher): + """ + Crypt password hashing to allow for LDAP auth compatibility + We do not encode, this should bot be used ! + The actual implementation may depend on the OS. + """ + + algorithm = "{crypt}" + + def encode(self, password, salt): + pass + + def verify(self, password, encoded): + """ + Check password against encoded using CRYPT algorithm + """ + assert encoded.startswith(self.algorithm) + salt = hash_password_salt(challenge_password) + return constant_time_compare(crypt.crypt(password.encode(), salt), + challenge.encode()) + + def safe_summary(self, encoded): + """ + Provides a safe summary of the password + """ + assert encoded.startswith(self.algorithm) + hash_str = encoded[7:] + hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode() + return OrderedDict([ + ('algorithm', self.algorithm), + ('iterations', 0), + ('salt', hashers.mask_hash(hash_str[2*DIGEST_LEN:], show=2)), + ('hash', hashers.mask_hash(hash_str[:2*DIGEST_LEN])), + ]) + + def harden_runtime(self, password, encoded): + """ + Method implemented to shut up BasePasswordHasher warning + + As we are not using multiple iterations the method is pretty useless + """ + pass + +class MD5PasswordHasher(hashers.BasePasswordHasher): + """ + Salted MD5 password hashing to allow for LDAP auth compatibility + We do not encode, this should bot be used ! + """ + + algorithm = "{SMD5}" + + def encode(self, password, salt): + pass + + def verify(self, password, encoded): + """ + Check password against encoded using SMD5 algorithm + """ + assert encoded.startswith(self.algorithm) + salt = hash_password_salt(encoded) + return constant_time_compare( + b64encode(hashlib.md5(password.encode() + salt).digest() + salt), + encoded.encode()) + + def safe_summary(self, encoded): + """ + Provides a safe summary of the password + """ + assert encoded.startswith(self.algorithm) + hash_str = encoded[7:] + hash_str = binascii.hexlify(decodestring(hash_str.encode())).decode() + return OrderedDict([ + ('algorithm', self.algorithm), + ('iterations', 0), + ('salt', hashers.mask_hash(hash_str[2*DIGEST_LEN:], show=2)), + ('hash', hashers.mask_hash(hash_str[:2*DIGEST_LEN])), + ]) + + def harden_runtime(self, password, encoded): + """ + Method implemented to shut up BasePasswordHasher warning + + As we are not using multiple iterations the method is pretty useless + """ + pass + class SSHAPasswordHasher(hashers.BasePasswordHasher): """ - SSHA password hashing to allow for LDAP auth compatibility + Salted SHA-1 password hashing to allow for LDAP auth compatibility """ algorithm = ALGO_NAME @@ -116,3 +227,19 @@ class SSHAPasswordHasher(hashers.BasePasswordHasher): As we are not using multiple iterations the method is pretty useless """ pass + + +class RecryptBackend(ModelBackend): + def authenticate(self, username=None, password=None): + # we obtain from the classical auth backend the user + user = super(RecryptBackend, self).authenticate(username, password) + if user: + if not(user.pwd_ntlm): + # if we dont have NT hash, we create it + user.pwd_ntlm = hashNT(password) + user.save() + if not("SSHA" in user.password): + # if the hash is too old, we update it + user.password = makeSecret(password) + user.save() + return user diff --git a/re2o/management/commands/gen_contrib.py b/re2o/management/commands/gen_contrib.py index 6003b30f..9951a383 100644 --- a/re2o/management/commands/gen_contrib.py +++ b/re2o/management/commands/gen_contrib.py @@ -41,7 +41,7 @@ class Command(BaseCommand): self.stdout.write(self.style.SUCCESS("Exportation Sucessfull")) with open("re2o/contributors.py", "w") as contrib_file: contrib_file.write("\"\"\"re2o.contributors\n") - contrib_file.write("A list of the proud contributors to Re2o\n") + contrib_file.write("A list of the contributors to Re2o\n") contrib_file.write("\"\"\"\n") contrib_file.write("\n") contrib_file.write("CONTRIBUTORS = " + str(contributeurs)) diff --git a/re2o/mixins.py b/re2o/mixins.py index 2ee049cc..f3858428 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -25,6 +25,7 @@ A set of mixins used all over the project to avoid duplicating code from reversion import revisions as reversion from django.db import transaction +from django.utils.translation import ugettext as _ class RevMixin(object): @@ -35,14 +36,14 @@ class RevMixin(object): """ Creates a version of this object and save it to database """ if self.pk is None: with transaction.atomic(), reversion.create_revision(): - reversion.set_comment("Création") + reversion.set_comment("Creation") return super(RevMixin, self).save(*args, **kwargs) return super(RevMixin, self).save(*args, **kwargs) def delete(self, *args, **kwargs): """ Creates a version of this object and delete it from database """ with transaction.atomic(), reversion.create_revision(): - reversion.set_comment("Suppresion") + reversion.set_comment("Deletion") return super(RevMixin, self).delete(*args, **kwargs) @@ -58,7 +59,7 @@ class FormRevMixin(object): ) elif self.changed_data: reversion.set_comment( - "Champs modifié(s) : %s" + "Field(s) altered : %s" % ', '.join(field for field in self.changed_data) ) return super(FormRevMixin, self).save(*args, **kwargs) @@ -107,7 +108,8 @@ class AclMixin(object): user_request.has_perm( cls.get_modulename() + '.add_' + cls.get_classname() ), - u"Vous n'avez pas le droit de créer un " + cls.get_classname() + (_("You don't have the right to create a %s object.") + % cls.get_classname()) ) def can_edit(self, user_request, *_args, **_kwargs): @@ -120,7 +122,8 @@ class AclMixin(object): user_request.has_perm( self.get_modulename() + '.change_' + self.get_classname() ), - u"Vous n'avez pas le droit d'éditer des " + self.get_classname() + (_("You don't have the right to edit a %s object.") + % self.get_classname()) ) def can_delete(self, user_request, *_args, **_kwargs): @@ -133,7 +136,8 @@ class AclMixin(object): user_request.has_perm( self.get_modulename() + '.delete_' + self.get_classname() ), - u"Vous n'avez pas le droit d'éditer des " + self.get_classname() + (_("You don't have the right to delete a %s object.") + % self.get_classname()) ) @classmethod @@ -146,7 +150,8 @@ class AclMixin(object): user_request.has_perm( cls.get_modulename() + '.view_' + cls.get_classname() ), - u"Vous n'avez pas le droit de voir des " + cls.get_classname() + (_("You don't have the right to view every %s object.") + % cls.get_classname()) ) def can_view(self, user_request, *_args, **_kwargs): @@ -159,5 +164,7 @@ class AclMixin(object): user_request.has_perm( self.get_modulename() + '.view_' + self.get_classname() ), - u"Vous n'avez pas le droit de voir des " + self.get_classname() + (_("You don't have the right to view a %s object.") + % self.get_classname()) ) + diff --git a/re2o/script_utils.py b/re2o/script_utils.py index e1420e6d..92c00fab 100644 --- a/re2o/script_utils.py +++ b/re2o/script_utils.py @@ -50,10 +50,10 @@ def get_user(pseudo): """Cherche un utilisateur re2o à partir de son pseudo""" user = User.objects.filter(pseudo=pseudo) if len(user) == 0: - raise CommandError("Utilisateur invalide") + raise CommandError("Invalid user.") if len(user) > 1: - raise CommandError("Plusieurs utilisateurs correspondant à ce " - "pseudo. Ceci NE DEVRAIT PAS arriver") + raise CommandError("Several users match this username. This SHOULD" + " NOT happen.") return user[0] @@ -81,19 +81,19 @@ def form_cli(Form, user, action, *args, **kwargs): form = Form(data, user=user, *args, **kwargs) if not form.is_valid(): - sys.stderr.write("Erreurs : \n") + sys.stderr.write("Errors: \n") for err in form.errors: # Oui, oui, on gère du HTML là où d'autres ont eu la # lumineuse idée de le mettre sys.stderr.write( "\t%s : %s\n" % (err, strip_tags(form.errors[err])) ) - raise CommandError("Formulaire invalide") + raise CommandError("Invalid form.") with transaction.atomic(), reversion.create_revision(): form.save() reversion.set_user(user) reversion.set_comment(action) - sys.stdout.write("%s : effectué. La modification peut prendre " - "quelques minutes pour s'appliquer.\n" % action) + sys.stdout.write("%s : done. The edit may take several minutes to" + " apply.\n" % action) diff --git a/re2o/settings.py b/re2o/settings.py index 71bd266f..9dd52d1f 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -46,6 +46,8 @@ BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Auth definition PASSWORD_HASHERS = ( 're2o.login.SSHAPasswordHasher', + 're2o.login.MD5PasswordHasher', + 're2o.login.CryptPasswordHasher', 'django.contrib.auth.hashers.PBKDF2PasswordHasher', ) AUTH_USER_MODEL = 'users.User' # The class to use for authentication @@ -94,6 +96,9 @@ MIDDLEWARE_CLASSES = ( 'django.middleware.security.SecurityMiddleware', 'reversion.middleware.RevisionMiddleware', ) + +AUTHENTICATION_BACKENDS = ['re2o.login.RecryptBackend'] + # Include debug_toolbar middleware if activated if 'debug_toolbar' in INSTALLED_APPS: # Include this middleware at the beggining @@ -177,7 +182,7 @@ STATIC_URL = '/static/' # Directory where the media files served by the server are stored MEDIA_ROOT = os.path.join(BASE_DIR, 'media').replace('\\', '/') # The URL to access the static files -MEDIA_URL = '/media/' +MEDIA_URL = os.path.join(BASE_DIR,'/media/') # Models to use for graphs GRAPH_MODELS = { diff --git a/re2o/templates/re2o/about.html b/re2o/templates/re2o/about.html index 8e88f5bb..f7afc69c 100644 --- a/re2o/templates/re2o/about.html +++ b/re2o/templates/re2o/about.html @@ -33,7 +33,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ description | safe }}

{% trans "About Re2o" %}

-

{% blocktrans %} +

{% blocktrans trimmed %} Re2o is an administration tool initiated by Rezo Supelec Metz and a few members of other FedeRez associations @@ -42,9 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc., so it can be setup in "a few steps". This tool is entirely free and available under a GNU Public License v2 (GPLv2) license on FedeRez gitlab.
- Re2o's mainteners are proud volunteers mainly from French engineering - schools (but not limited to) who have given a lot of their time to make - this project possible. So please be kind with them.
+ Re2o's mainteners are volunteers mainly from French schools.
If you want to get involved in the development process, we will be glad to welcome you so do not hesitate to contact us and come help us build the future of Re2o. @@ -63,18 +61,18 @@ with this program; if not, write to the Free Software Foundation, Inc.,

-

{% trans "Version informations" %}

+

{% trans "Version information" %}

    -
  • {% blocktrans %} +
  • {% blocktrans trimmed %} Remote URL: {{ git_info_remote }} {% endblocktrans %}
  • -
  • {% blocktrans %} +
  • {% blocktrans trimmed %} Branch: {{ git_info_branch }} {% endblocktrans %}
  • -
  • {% blocktrans %} +
  • {% blocktrans trimmed %} Commit: {{ git_info_commit }} {% endblocktrans %}
  • -
  • {% blocktrans %} +
  • {% blocktrans trimmed %} Commit date: {{ git_info_commit_date }} {% endblocktrans %}
diff --git a/re2o/templates/re2o/aff_history.html b/re2o/templates/re2o/aff_history.html index d7be350c..7079d7de 100644 --- a/re2o/templates/re2o/aff_history.html +++ b/re2o/templates/re2o/aff_history.html @@ -22,17 +22,19 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} +{% load i18n %} + {% if reversions.paginator %} {% endif %} @@ -40,9 +42,9 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - + + + {% for rev in reversions %} @@ -53,3 +55,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
DateEffectué parCommentaire{% trans "Date" %}{% trans "Performed by" %}{% trans "Comment" %}
+ diff --git a/re2o/templates/re2o/contact.html b/re2o/templates/re2o/contact.html index f2de696d..f26e002b 100644 --- a/re2o/templates/re2o/contact.html +++ b/re2o/templates/re2o/contact.html @@ -29,7 +29,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Contact" %}{% endblock %} {% block content %} -

{% blocktrans %}Contacter l'association {{asso_name}}{% endblocktrans %}

+

{% blocktrans %}Contact the organisation {{asso_name}}{% endblocktrans %}


{% for contact in contacts %} @@ -45,8 +45,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} - - {% endblock %} diff --git a/re2o/templates/re2o/history.html b/re2o/templates/re2o/history.html index e9d654d9..945a355e 100644 --- a/re2o/templates/re2o/history.html +++ b/re2o/templates/re2o/history.html @@ -24,11 +24,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Historique{% endblock %} +{% block title %}{% trans "History" %}{% endblock %} {% block content %} -

Historique de {{ object }}

+

{% blocktrans %}History of {{ object }}{% endblocktrans %}

{% include "re2o/aff_history.html" with reversions=reversions %}

diff --git a/re2o/templates/re2o/index.html b/re2o/templates/re2o/index.html index 5b505c0f..f7adacbc 100644 --- a/re2o/templates/re2o/index.html +++ b/re2o/templates/re2o/index.html @@ -30,8 +30,54 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Home" %}{% endblock %} {% block content %} -

{% blocktrans %}Welcome to {{ name_website }} !{% endblocktrans %}

+
+

{% blocktrans %}Welcome to {{ name_website }}{% endblocktrans %}

+
+{% if not request.user.is_authenticated %} +
+{% if var_sa %} +
+
+
+
+

{% trans "Registration" %}

+

{% trans "If you don't have an account yet and you want to access the Internet and the organisation's services, create your own personal account." %}

+

{% trans "Registration" %}

+
+
+
+
+{% endif %} +
+
+
+
+

{% trans "Logging in" %}

+

{% trans "If you already have an account, log in. You can manage your subscription to the organisation, your machines and all your services." %}

+

{% trans "Logging in" %}

+
+
+
+
+{% else %} +
+
+
+
+
+

{% trans "My profile" %}

+

{% trans "To manage your subscription, your machines and all your services, access your profile." %}

+

{% trans "Access my profile" %}

+
+
+
+
+{% endif %} +
+
+

{% trans "Services of the organisation" %}

+
{% for service_list in services_urls %}
diff --git a/re2o/templates/re2o/sidebar.html b/re2o/templates/re2o/sidebar.html index ad64caae..c9202d14 100644 --- a/re2o/templates/re2o/sidebar.html +++ b/re2o/templates/re2o/sidebar.html @@ -23,6 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} +{% load i18n %} {% block sidebar %} @@ -43,10 +44,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endif %} {% if twitter_url %} - + - + {% endif %} diff --git a/re2o/templatetags/acl.py b/re2o/templatetags/acl.py index 9a439f88..6fd00e19 100644 --- a/re2o/templatetags/acl.py +++ b/re2o/templatetags/acl.py @@ -79,6 +79,7 @@ from django.contrib.contenttypes.models import ContentType register = template.Library() + def get_model(model_name): """Retrieve the model object from its name""" splitted = model_name.split('.') @@ -145,7 +146,7 @@ def get_callback(tag_name, obj=None): if tag_name == 'can_view_app': return acl_fct( lambda x: ( - not any(not sys.modules[o].can_view(x) for o in obj), + not any(not sys.modules[o].can_view(x)[0] for o in obj), None ), False @@ -153,7 +154,7 @@ def get_callback(tag_name, obj=None): if tag_name == 'cannot_view_app': return acl_fct( lambda x: ( - not any(not sys.modules[o].can_view(x) for o in obj), + not any(not sys.modules[o].can_view(x)[0] for o in obj), None ), True @@ -170,12 +171,12 @@ def get_callback(tag_name, obj=None): ) if tag_name == 'can_view_any_app': return acl_fct( - lambda x: (any(sys.modules[o].can_view(x) for o in obj), None), + lambda x: (any(sys.modules[o].can_view(x)[0] for o in obj), None), False ) if tag_name == 'cannot_view_any_app': return acl_fct( - lambda x: (any(sys.modules[o].can_view(x) for o in obj), None), + lambda x: (any(sys.modules[o].can_view(x)[0] for o in obj), None), True ) @@ -226,7 +227,9 @@ def acl_history_filter(parser, token): def acl_app_filter(parser, token): """Templatetag for acl checking on applications.""" try: - tag_name, *app_name = token.split_contents() + contents = token.split_contents() + tag_name = contents[0] + app_name = contents[1:] except ValueError: raise template.TemplateSyntaxError( "%r tag require 1 argument: an application" diff --git a/re2o/templatetags/design.py b/re2o/templatetags/design.py index 87a0e0f8..c64e9b40 100644 --- a/re2o/templatetags/design.py +++ b/re2o/templatetags/design.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- #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. diff --git a/re2o/templatetags/self_adhesion.py b/re2o/templatetags/self_adhesion.py index 3b463e68..09fe5ede 100644 --- a/re2o/templatetags/self_adhesion.py +++ b/re2o/templatetags/self_adhesion.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # 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. diff --git a/re2o/urls.py b/re2o/urls.py index d1e11d52..39f51ec3 100644 --- a/re2o/urls.py +++ b/re2o/urls.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # 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. diff --git a/re2o/utils.py b/re2o/utils.py index 75304369..6f7870f0 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -250,6 +250,14 @@ class SortTable: 'cotis_id': ['id'], 'default': ['-date'] } + COTISATIONS_CUSTOM = { + 'invoice_date': ['date'], + 'invoice_id': ['id'], + 'invoice_recipient': ['recipient'], + 'invoice_address': ['address'], + 'invoice_payment': ['payment'], + 'default': ['-date'] + } COTISATIONS_CONTROL = { 'control_name': ['user__adherent__name'], 'control_surname': ['user__surname'], diff --git a/re2o/views.py b/re2o/views.py index fb00b98e..15becb35 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # 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. @@ -37,7 +38,6 @@ from django.views.decorators.cache import cache_page from preferences.models import ( Service, MailContact, - GeneralOption, AssoOption, HomeOption ) @@ -70,7 +70,6 @@ def index(request): }, 're2o/index.html', request) -@cache_page(7 * 24 * 60 * 60) def about_page(request): """ The view for the about page. Fetch some info about the configuration of the project. If it can't @@ -132,3 +131,4 @@ def handler500(request): def handler404(request): """The handler view for a 404 error""" return render(request, 'errors/404.html') + diff --git a/search/forms.py b/search/forms.py index 8cdb1cc1..6065e799 100644 --- a/search/forms.py +++ b/search/forms.py @@ -26,23 +26,24 @@ from __future__ import unicode_literals from django import forms from django.forms import Form +from django.utils.translation import ugettext_lazy as _ from re2o.utils import get_input_formats_help_text CHOICES_USER = ( - ('0', 'Actifs'), - ('1', 'Désactivés'), - ('2', 'Archivés'), + ('0', _("Active")), + ('1', _("Disabled")), + ('2', _("Archived")), ) CHOICES_AFF = ( - ('0', 'Utilisateurs'), - ('1', 'Machines'), - ('2', 'Factures'), - ('3', 'Bannissements'), - ('4', 'Accès à titre gracieux'), - ('5', 'Chambres'), - ('6', 'Ports'), - ('7', 'Switchs'), + ('0', _("Users")), + ('1', _("Machines")), + ('2', _("Invoices")), + ('3', _("Bans")), + ('4', _("Whitelists")), + ('5', _("Rooms")), + ('6', _("Ports")), + ('7', _("Switches")), ) @@ -55,11 +56,11 @@ def initial_choices(choice_set): class SearchForm(Form): """The form for a simple search""" q = forms.CharField( - label='Recherche', + label=_("Search"), help_text=( - 'Utilisez « » et «,» pour spécifier différents mots, «"query"» ' - 'pour une recherche exacte et «\\» pour échapper un caractère.' - ), + _("Use « » and «,» to specify distinct words, «\"query\"» for" + " an exact search and «\\» to escape a character.") + ), max_length=100 ) @@ -67,23 +68,23 @@ class SearchForm(Form): class SearchFormPlus(Form): """The form for an advanced search (with filters)""" q = forms.CharField( - label='Recherche', + label=_("Search"), help_text=( - 'Utilisez « » et «,» pour spécifier différents mots, «"query"» ' - 'pour une recherche exacte et «\\» pour échapper un caractère.' + _("Use « » and «,» to specify distinct words, «\"query\"» for" + " an exact search and «\\» to escape a character.") ), max_length=100, required=False ) u = forms.MultipleChoiceField( - label="Filtre utilisateurs", + label=_("Users filter"), required=False, widget=forms.CheckboxSelectMultiple, choices=CHOICES_USER, initial=initial_choices(CHOICES_USER) ) a = forms.MultipleChoiceField( - label="Filtre affichage", + label=_("Display filter"), required=False, widget=forms.CheckboxSelectMultiple, choices=CHOICES_AFF, @@ -91,11 +92,11 @@ class SearchFormPlus(Form): ) s = forms.DateField( required=False, - label="Date de début", + label=_("Start date"), ) e = forms.DateField( required=False, - label="Date de fin" + label=_("End date") ) def __init__(self, *args, **kwargs): @@ -106,3 +107,4 @@ class SearchFormPlus(Form): self.fields['e'].help_text = get_input_formats_help_text( self.fields['e'].input_formats ) + diff --git a/search/locale/fr/LC_MESSAGES/django.mo b/search/locale/fr/LC_MESSAGES/django.mo new file mode 100644 index 00000000..94a44104 Binary files /dev/null and b/search/locale/fr/LC_MESSAGES/django.mo differ diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..dd0b63a3 --- /dev/null +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,163 @@ +# 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 © 2018 Maël Kervella +# +# 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. +msgid "" +msgstr "" +"Project-Id-Version: 2.5\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2018-08-15 18:15+0200\n" +"PO-Revision-Date: 2018-06-24 20:10+0200\n" +"Last-Translator: Laouen Fernet \n" +"Language-Team: \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: forms.py:33 +msgid "Active" +msgstr "Actifs" + +#: forms.py:34 +msgid "Disabled" +msgstr "Désactivés" + +#: forms.py:35 +msgid "Archived" +msgstr "Archivés" + +#: forms.py:39 +msgid "Users" +msgstr "Utilisateurs" + +#: forms.py:40 +msgid "Machines" +msgstr "Machines" + +#: forms.py:41 +msgid "Invoices" +msgstr "Factures" + +#: forms.py:42 +msgid "Bans" +msgstr "Bannissements" + +#: forms.py:43 +msgid "Whitelists" +msgstr "Accès gracieux" + +#: forms.py:44 +msgid "Rooms" +msgstr "Chambres" + +#: forms.py:45 +msgid "Ports" +msgstr "Ports" + +#: forms.py:46 +msgid "Switches" +msgstr "Commutateurs réseau" + +#: forms.py:59 forms.py:71 templates/search/search.html:29 +#: templates/search/search.html:48 +msgid "Search" +msgstr "Rechercher" + +#: forms.py:61 forms.py:73 +msgid "" +"Use « » and «,» to specify distinct words, «\"query\"» for an exact search " +"and «\\» to escape a character." +msgstr "" +"Utilisez « » et «,» pour spécifier différents mots, «\"query\"» pour une " +"recherche exacte et «\\» pour échapper un caractère." + +#: forms.py:80 +msgid "Users filter" +msgstr "Filtre utilisateurs" + +#: forms.py:87 +msgid "Display filter" +msgstr "Filtre affichage" + +#: forms.py:95 +msgid "Start date" +msgstr "Date de début" + +#: forms.py:99 +msgid "End date" +msgstr "Date de fin" + +#: templates/search/index.html:29 +msgid "Search results" +msgstr "Résultats de la recherche" + +#: templates/search/index.html:33 +msgid "Results among users:" +msgstr "Résultats parmi les utilisateurs :" + +#: templates/search/index.html:37 +msgid "Results among clubs:" +msgstr "Résultats parmi les clubs :" + +#: templates/search/index.html:41 +msgid "Results among machines:" +msgstr "Résultats parmi les machines :" + +#: templates/search/index.html:45 +msgid "Results among invoices:" +msgstr "Résultats parmi les factures :" + +#: templates/search/index.html:49 +msgid "Results among whitelists:" +msgstr "Résultats parmi les accès à titre gracieux :" + +#: templates/search/index.html:53 +msgid "Results among bans:" +msgstr "Résultats parmi les bannissements :" + +#: templates/search/index.html:57 +msgid "Results among rooms:" +msgstr "Résultats parmi les chambres :" + +#: templates/search/index.html:61 +msgid "Results among ports" +msgstr "Résultats parmi les ports :" + +#: templates/search/index.html:65 +msgid "Results among switches" +msgstr "Résultats parmi les commutateurs réseau :" + +#: templates/search/index.html:69 +msgid "No result" +msgstr "Pas de résultat" + +#: templates/search/index.html:71 +#, python-format +msgid "(Only the first %(max_result)s results are displayed in each category)" +msgstr "" +"(Seulement les %(max_result)s premiers résultats sont affichés dans chaque " +"catégorie)" + +#: templates/search/sidebar.html:31 +msgid "Simple search" +msgstr "Recherche simple" + +#: templates/search/sidebar.html:35 +msgid "Advanced search" +msgstr "Recherche avancée" diff --git a/search/templates/search/index.html b/search/templates/search/index.html index 2fad0c89..23b57cce 100644 --- a/search/templates/search/index.html +++ b/search/templates/search/index.html @@ -24,52 +24,54 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Résultats de la recherche{% endblock %} +{% block title %}{% trans "Search results" %}{% endblock %} {% block content %} {% if users %} -

Résultats dans les utilisateurs

+

{% trans "Results among users:" %}

{% include "users/aff_users.html" with users_list=users %} {% endif%} {% if clubs %} -

Résultats dans les clubs

+

{% trans "Results among clubs:" %}

{% include "users/aff_clubs.html" with clubs_list=clubs %} {% endif%} {% if machines %} -

Résultats dans les machines :

+

{% trans "Results among machines:" %}

{% include "machines/aff_machines.html" with machines_list=machines %} {% endif %} {% if factures %} -

Résultats dans les factures :

+

{% trans "Results among invoices:" %}

{% include "cotisations/aff_cotisations.html" with facture_list=factures %} {% endif %} {% if whitelists %} -

Résultats dans les accès à titre gracieux :

+

{% trans "Results among whitelists:" %}

{% include "users/aff_whitelists.html" with white_list=whitelists %} {% endif %} {% if bans %} -

Résultats dans les banissements :

+

{% trans "Results among bans:" %}

{% include "users/aff_bans.html" with ban_list=bans %} {% endif %} {% if rooms %} -

Résultats dans les chambres :

+

{% trans "Results among rooms:" %}

{% include "topologie/aff_chambres.html" with room_list=rooms %} {% endif %} {% if ports %} -

Résultats dans les ports :

+

{% trans "Results among ports" %}

{% include "topologie/aff_port.html" with port_list=ports %} {% endif %} {% if switches %} -

Résultats dans les switchs :

+

{% trans "Results among switches" %}

{% include "topologie/aff_switch.html" with switch_list=switches %} {% endif %} {% if not users and not machines and not factures and not whitelists and not bans and not rooms and not ports and not switches %} -

Aucun résultat

+

{% trans "No result" %}

{% else %} -
(Seulement les {{ max_result }} premiers résultats sont affichés dans chaque catégorie)
+
{% blocktrans %}(Only the first {{ max_result }} results are displayed in each category){% endblocktrans %}
{% endif %}


{% endblock %} + diff --git a/search/templates/search/search.html b/search/templates/search/search.html index 7ae5d56d..42012339 100644 --- a/search/templates/search/search.html +++ b/search/templates/search/search.html @@ -24,8 +24,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} +{% load i18n %} -{% block title %}Recherche{% endblock %} +{% block title %}{% trans "Search" %}{% endblock %} {% block content %} {% bootstrap_form_errors search_form %} @@ -44,7 +45,8 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if search_form.e %} {% bootstrap_field search_form.e %} {% endif %} - {% bootstrap_button "Search" button_type="submit" icon="search" %} + {% trans "Search" as tr_search %} + {% bootstrap_button tr_search button_type="submit" icon="search" %}

@@ -52,3 +54,4 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% endblock %} + diff --git a/search/templates/search/sidebar.html b/search/templates/search/sidebar.html index 8e29d379..a445ef41 100644 --- a/search/templates/search/sidebar.html +++ b/search/templates/search/sidebar.html @@ -23,14 +23,16 @@ with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. {% endcomment %} +{% load i18n %} {% block sidebar %} - Recherche simple + {% trans "Simple search" %} - Recherche avancée + {% trans "Advanced search" %} {% endblock %} + diff --git a/search/views.py b/search/views.py index 871515fa..93aa1d0e 100644 --- a/search/views.py +++ b/search/views.py @@ -33,6 +33,7 @@ from django.contrib.auth.decorators import login_required from django.db.models import Q from users.models import User, Adherent, Club, Ban, Whitelist from machines.models import Machine +from cotisations.models import Cotisation from topologie.models import Port, Switch, Room from cotisations.models import Facture from preferences.models import GeneralOption @@ -44,6 +45,7 @@ from search.forms import ( initial_choices ) from re2o.utils import SortTable +from re2o.acl import can_view_all def is_int(variable): @@ -262,9 +264,9 @@ def search_single_word(word, filters, user, ) | Q( related__switch__interface__domain__name__icontains=word ) | Q( - radius__icontains=word + custom_profile__name__icontains=word ) | Q( - vlan_force__name__icontains=word + custom_profile__profil_default__icontains=word ) | Q( details__icontains=word ) @@ -405,6 +407,7 @@ def get_results(query, request, params): @login_required +@can_view_all(User, Machine, Cotisation) def search(request): """ La page de recherche standard """ search_form = SearchForm(request.GET or None) @@ -422,6 +425,7 @@ def search(request): @login_required +@can_view_all(User, Machine, Cotisation) def searchp(request): """ La page de recherche avancée """ search_form = SearchFormPlus(request.GET or None) diff --git a/static/css/base.css b/static/css/base.css index 2dc17770..5748f538 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -129,3 +129,21 @@ td.long_text{ th.long_text{ width: 60%; } + +/* change color of panel heading on hover */ + +.panel > .profil:hover { + background-image: none; + background-color: #e6e6e6; + color: black; +} + +/* add padding under title in profile */ +.title-dashboard{ + padding-bottom: 30px; +} + +/* center the profil boxes */ +.dashboard{ + text-align: center; +} diff --git a/static/js/email_address.js b/static/js/email_address.js new file mode 100644 index 00000000..10c1f544 --- /dev/null +++ b/static/js/email_address.js @@ -0,0 +1,17 @@ +/** To enable the redirection has no meaning if the local email adress is not + * enabled. Thus this function enable the checkbox if needed and changes its + * state. + */ +function enable_redirection_chkbox() { + var redirect = document.getElementById('id_User-local_email_redirect'); + var enabled = document.getElementById('id_User-local_email_enabled').checked; + if(!enabled) + { + redirect.checked = false; + } + redirect.disabled = !enabled; +} + +var enabled_chkbox = document.getElementById('id_User-local_email_enabled'); +enabled_chkbox.onclick = enable_redirection_chkbox; +enable_redirection_chkbox(); diff --git a/templates/base.html b/templates/base.html index 8a21a612..aeb840da 100644 --- a/templates/base.html +++ b/templates/base.html @@ -39,8 +39,8 @@ with this program; if not, write to the Free Software Foundation, Inc., - - + + {# Load CSS and JavaScript #} {% bootstrap_css %} @@ -76,28 +76,19 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if request_user.is_authenticated %}
-

{% blocktrans count interfaces|length as nb %}{{ nb }} machine active{% plural %}{{ nb }} machines active{% endblocktrans %}

+

{% blocktrans count interfaces|length as nb %}{{ nb }} active machine{% plural %}{{ nb }} active machines{% endblocktrans %}

@@ -252,9 +254,10 @@ with this program; if not, write to the Free Software Foundation, Inc., {# Read the documentation for more information #} + diff --git a/templates/buttons/add.html b/templates/buttons/add.html index 17058b89..efbbe142 100644 --- a/templates/buttons/add.html +++ b/templates/buttons/add.html @@ -21,6 +21,11 @@ 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 i18n %} + +{% trans "Add" as tr_add %} + + diff --git a/templates/buttons/edit.html b/templates/buttons/edit.html index ced8f906..87ebea8b 100644 --- a/templates/buttons/edit.html +++ b/templates/buttons/edit.html @@ -21,6 +21,11 @@ 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 i18n %} + +{% trans "Edit" as tr_edit %} + + diff --git a/templates/buttons/history.html b/templates/buttons/history.html index fa7835da..cabcd6b2 100644 --- a/templates/buttons/history.html +++ b/templates/buttons/history.html @@ -21,6 +21,7 @@ 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 i18n %} {% if text %}{% trans 'History' %}{% endif %} diff --git a/templates/buttons/multiple_checkbox_alt.html b/templates/buttons/multiple_checkbox_alt.html index e6fd7d58..2809d3a2 100644 --- a/templates/buttons/multiple_checkbox_alt.html +++ b/templates/buttons/multiple_checkbox_alt.html @@ -36,3 +36,4 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{{ field.help_text }}
+ diff --git a/templates/buttons/setlang.html b/templates/buttons/setlang.html index 3d435ef3..e6a61da8 100644 --- a/templates/buttons/setlang.html +++ b/templates/buttons/setlang.html @@ -43,8 +43,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if language.code == LANGUAGE_CODE %} {% endif %} - {{ language.name_local | title }} ({{ language.code }}) + {{ language.name_local|title }} ({{ language.code }}) {% endfor %}
+ diff --git a/templates/buttons/sort.html b/templates/buttons/sort.html index fde7e546..3de5b6cd 100644 --- a/templates/buttons/sort.html +++ b/templates/buttons/sort.html @@ -24,27 +24,32 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load url_insert_param %} +{% load i18n %} + {% spaceless %} {% endspaceless %} + diff --git a/templates/buttons/suppr.html b/templates/buttons/suppr.html index a03ea42a..4910db03 100644 --- a/templates/buttons/suppr.html +++ b/templates/buttons/suppr.html @@ -21,6 +21,11 @@ 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 i18n %} + +{% trans "Delete" as tr_delete %} + + diff --git a/templates/errors/404.html b/templates/errors/404.html index fdf5ec77..6edaee64 100644 --- a/templates/errors/404.html +++ b/templates/errors/404.html @@ -32,11 +32,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - + - 404, Page not Found + {% trans "404 error: page not found" %} +{% endif %}
{% if showCGU %} -

En cliquant sur Créer ou modifier, l'utilisateur s'engage à respecter les règles d'utilisation du réseau.

-

Résumé des règles d'utilisations

+

{% trans "By clicking 'Create or edit', the user commits to respect the " %}{% trans "General Terms of Use" %}.

+

{% trans "Summary of the General Terms of Use" %}

{{ GTU_sum_up }}

{% endif %}


{% endblock %} + diff --git a/users/urls.py b/users/urls.py index f5114600..5515dd29 100644 --- a/users/urls.py +++ b/users/urls.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # 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. diff --git a/users/views.py b/users/views.py index a4f4ea83..af311ceb 100644 --- a/users/views.py +++ b/users/views.py @@ -1,3 +1,4 @@ +# -*- mode: python; coding: utf-8 -*- # 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. @@ -45,6 +46,7 @@ from django.db import transaction from django.http import HttpResponse from django.http import HttpResponseRedirect from django.views.decorators.csrf import csrf_exempt +from django.utils.translation import ugettext as _ from rest_framework.renderers import JSONRenderer from reversion import revisions as reversion @@ -117,8 +119,8 @@ def new_user(request): if user.is_valid(): user = user.save() user.reset_passwd_mail(request) - messages.success(request, "L'utilisateur %s a été crée, un mail\ - pour l'initialisation du mot de passe a été envoyé" % user.pseudo) + 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)} @@ -129,7 +131,7 @@ def new_user(request): 'GTU_sum_up': GTU_sum_up, 'GTU': GTU, 'showCGU': True, - 'action_name': 'Créer un utilisateur' + 'action_name': _("Create a user") }, 'users/user.html', request @@ -146,14 +148,14 @@ def new_club(request): club = club.save(commit=False) club.save() club.reset_passwd_mail(request) - messages.success(request, "L'utilisateur %s a été crée, un mail\ - pour l'initialisation du mot de passe a été envoyé" % club.pseudo) + messages.success(request, _("The club %s was created, an email to set" + " the password was sent.") % club.pseudo) return redirect(reverse( 'users:profil', kwargs={'userid': str(club.id)} )) return form( - {'userform': club, 'showCGU': False, 'action_name': 'Créer un club'}, + {'userform': club, 'showCGU': False, 'action_name': _("Create a club")}, 'users/user.html', request ) @@ -171,7 +173,7 @@ def edit_club_admin_members(request, club_instance, **_kwargs): if club.is_valid(): if club.changed_data: club.save() - messages.success(request, "Le club a bien été modifié") + messages.success(request, _("The club was edited.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(club_instance.id)} @@ -180,7 +182,7 @@ def edit_club_admin_members(request, club_instance, **_kwargs): { 'userform': club, 'showCGU': False, - 'action_name': 'Editer les admin et membres' + 'action_name': _("Edit the admins and members") }, 'users/user.html', request @@ -208,13 +210,13 @@ def edit_info(request, user, userid): if user_form.is_valid(): if user_form.changed_data: user_form.save() - messages.success(request, "L'user a bien été modifié") + messages.success(request, _("The user was edited.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(userid)} )) return form( - {'userform': user_form, 'action_name': "Editer l'utilisateur"}, + {'userform': user_form, 'action_name': _("Edit the user")}, 'users/user.html', request ) @@ -228,13 +230,13 @@ def state(request, user, userid): if state_form.is_valid(): if state_form.changed_data: state_form.save() - messages.success(request, "Etat changé avec succès") + messages.success(request, _("The state was edited.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(userid)} )) return form( - {'userform': state_form, 'action_name': "Editer l'état"}, + {'userform': state_form, 'action_name': _("Edit the state")}, 'users/user.html', request ) @@ -249,13 +251,13 @@ def groups(request, user, userid): if group_form.is_valid(): if group_form.changed_data: group_form.save() - messages.success(request, "Groupes changés avec succès") + messages.success(request, _("The groups were edited.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(userid)} )) return form( - {'userform': group_form, 'action_name': 'Editer les groupes'}, + {'userform': group_form, 'action_name': _("Edit the groups")}, 'users/user.html', request ) @@ -271,13 +273,13 @@ def password(request, user, userid): if u_form.is_valid(): if u_form.changed_data: u_form.save() - messages.success(request, "Le mot de passe a changé") + messages.success(request, _("The password was changed.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(userid)} )) return form( - {'userform': u_form, 'action_name': 'Changer le mot de passe'}, + {'userform': u_form, 'action_name': _("Change the password")}, 'users/user.html', request ) @@ -289,7 +291,7 @@ def del_group(request, user, listrightid, **_kwargs): """ View used to delete a group """ user.groups.remove(ListRight.objects.get(id=listrightid)) user.save() - messages.success(request, "Droit supprimé à %s" % user) + messages.success(request, _("%s was removed from the group.") % user) return HttpResponseRedirect(request.META.get('HTTP_REFERER')) @@ -299,7 +301,7 @@ def del_superuser(request, user, **_kwargs): """Remove the superuser right of an user.""" user.is_superuser = False user.save() - messages.success(request, "%s n'est plus superuser" % user) + messages.success(request, _("%s is no longer superuser.") % user) return HttpResponseRedirect(request.META.get('HTTP_REFERER')) @@ -312,11 +314,11 @@ def new_serviceuser(request): user.save() messages.success( request, - "L'utilisateur a été crée" + _("The service user was created.") ) return redirect(reverse('users:index-serviceusers')) return form( - {'userform': user, 'action_name': 'Créer un serviceuser'}, + {'userform': user, 'action_name': _("Create a service user")}, 'users/user.html', request ) @@ -333,10 +335,10 @@ def edit_serviceuser(request, serviceuser, **_kwargs): if serviceuser.is_valid(): if serviceuser.changed_data: serviceuser.save() - messages.success(request, "L'user a bien été modifié") + messages.success(request, _("The service user was edited.")) return redirect(reverse('users:index-serviceusers')) return form( - {'userform': serviceuser, 'action_name': 'Editer un serviceuser'}, + {'userform': serviceuser, 'action_name': _("Edit a service user")}, 'users/user.html', request ) @@ -348,10 +350,10 @@ def del_serviceuser(request, serviceuser, **_kwargs): """Suppression d'un ou plusieurs serviceusers""" if request.method == "POST": serviceuser.delete() - messages.success(request, "L'user a été détruit") + messages.success(request, _("The service user was deleted.")) return redirect(reverse('users:index-serviceusers')) return form( - {'objet': serviceuser, 'objet_name': 'serviceuser'}, + {'objet': serviceuser, 'objet_name': 'service user'}, 'users/delete.html', request ) @@ -368,7 +370,7 @@ def add_ban(request, user, userid): ban = BanForm(request.POST or None, instance=ban_instance) if ban.is_valid(): ban.save() - messages.success(request, "Bannissement ajouté") + messages.success(request, _("The ban was added.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(userid)} @@ -376,10 +378,10 @@ def add_ban(request, user, userid): if user.is_ban(): messages.error( request, - "Attention, cet utilisateur a deja un bannissement actif" + _("Warning: this user already has an active ban.") ) return form( - {'userform': ban, 'action_name': 'Ajouter un ban'}, + {'userform': ban, 'action_name': _("Add a ban")}, 'users/user.html', request ) @@ -395,10 +397,10 @@ def edit_ban(request, ban_instance, **_kwargs): if ban.is_valid(): if ban.changed_data: ban.save() - messages.success(request, "Bannissement modifié") + messages.success(request, _("The ban was edited.")) return redirect(reverse('users:index')) return form( - {'userform': ban, 'action_name': 'Editer un ban'}, + {'userform': ban, 'action_name': _("Edit a ban")}, 'users/user.html', request ) @@ -410,7 +412,7 @@ def del_ban(request, ban, **_kwargs): """ Supprime un banissement""" if request.method == "POST": ban.delete() - messages.success(request, "Le banissement a été supprimé") + messages.success(request, _("The ban was deleted.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(ban.user.id)} @@ -437,7 +439,7 @@ def add_whitelist(request, user, userid): ) if whitelist.is_valid(): whitelist.save() - messages.success(request, "Accès à titre gracieux accordé") + messages.success(request, _("The whitelist was added.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(userid)} @@ -445,10 +447,10 @@ def add_whitelist(request, user, userid): if user.is_whitelisted(): messages.error( request, - "Attention, cet utilisateur a deja un accès gracieux actif" + _("Warning: this user already has an active whitelist.") ) return form( - {'userform': whitelist, 'action_name': 'Ajouter une whitelist'}, + {'userform': whitelist, 'action_name': _("Add a whitelist")}, 'users/user.html', request ) @@ -468,10 +470,10 @@ def edit_whitelist(request, whitelist_instance, **_kwargs): if whitelist.is_valid(): if whitelist.changed_data: whitelist.save() - messages.success(request, "Whitelist modifiée") + messages.success(request, _("The whitelist was edited.")) return redirect(reverse('users:index')) return form( - {'userform': whitelist, 'action_name': 'Editer une whitelist'}, + {'userform': whitelist, 'action_name': _("Edit a whitelist")}, 'users/user.html', request ) @@ -483,7 +485,7 @@ def del_whitelist(request, whitelist, **_kwargs): """ Supprime un acces gracieux""" if request.method == "POST": whitelist.delete() - messages.success(request, "L'accés gracieux a été supprimé") + messages.success(request, _("The whitelist was deleted.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(whitelist.user.id)} @@ -507,7 +509,7 @@ def add_emailaddress(request, user, userid): ) if emailaddress.is_valid(): emailaddress.save() - messages.success(request, "Local email account created") + messages.success(request, _("The local email account was created.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(userid)} @@ -515,7 +517,7 @@ def add_emailaddress(request, user, userid): return form( {'userform': emailaddress, 'showCGU': False, - 'action_name': 'Add a local email account'}, + 'action_name': _("Add a local email account")}, 'users/user.html', request ) @@ -532,7 +534,7 @@ def edit_emailaddress(request, emailaddress_instance, **_kwargs): if emailaddress.is_valid(): if emailaddress.changed_data: emailaddress.save() - messages.success(request, "Local email account modified") + messages.success(request, _("The local email account was edited.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(emailaddress_instance.user.id)} @@ -540,7 +542,7 @@ def edit_emailaddress(request, emailaddress_instance, **_kwargs): return form( {'userform': emailaddress, 'showCGU': False, - 'action_name': 'Edit a local email account'}, + 'action_name': _("Edit a local email account")}, 'users/user.html', request ) @@ -552,7 +554,7 @@ def del_emailaddress(request, emailaddress, **_kwargs): """Delete a local email account""" if request.method == "POST": emailaddress.delete() - messages.success(request, "Local email account deleted") + messages.success(request, _("The local email account was deleted.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(emailaddress.user.id)} @@ -576,7 +578,7 @@ def edit_email_settings(request, user_instance, **_kwargs): if email_settings.is_valid(): if email_settings.changed_data: email_settings.save() - messages.success(request, "Email settings updated") + messages.success(request, _("The email settings were edited.")) return redirect(reverse( 'users:profil', kwargs={'userid': str(user_instance.id)} @@ -584,7 +586,8 @@ def edit_email_settings(request, user_instance, **_kwargs): return form( {'userform': email_settings, 'showCGU': False, - 'action_name': 'Edit the email settings'}, + 'load_js_file': '/static/js/email_address.js', + 'action_name': _("Edit the email settings")}, 'users/user.html', request ) @@ -598,10 +601,10 @@ def add_school(request): school = SchoolForm(request.POST or None) if school.is_valid(): school.save() - messages.success(request, "L'établissement a été ajouté") + messages.success(request, _("The school was added.")) return redirect(reverse('users:index-school')) return form( - {'userform': school, 'action_name': 'Ajouter'}, + {'userform': school, 'action_name': _("Add a school")}, 'users/user.html', request ) @@ -616,10 +619,10 @@ def edit_school(request, school_instance, **_kwargs): if school.is_valid(): if school.changed_data: school.save() - messages.success(request, "Établissement modifié") + messages.success(request, _("The school was edited.")) return redirect(reverse('users:index-school')) return form( - {'userform': school, 'action_name': 'Editer'}, + {'userform': school, 'action_name': _("Edit a school")}, 'users/user.html', request ) @@ -638,15 +641,15 @@ def del_school(request, instances): for school_del in school_dels: try: school_del.delete() - messages.success(request, "L'établissement a été supprimé") + messages.success(request, _("The school was deleted.")) except ProtectedError: messages.error( request, - "L'établissement %s est affecté à au moins un user, \ - vous ne pouvez pas le supprimer" % school_del) + _("The school %s is assigned to at least one user," + " impossible to delete it.") % school_del) return redirect(reverse('users:index-school')) return form( - {'userform': school, 'action_name': 'Supprimer'}, + {'userform': school, 'action_name': _("Delete")}, 'users/user.html', request ) @@ -659,10 +662,10 @@ def add_shell(request): shell = ShellForm(request.POST or None) if shell.is_valid(): shell.save() - messages.success(request, "Le shell a été ajouté") + messages.success(request, _("The shell was added.")) return redirect(reverse('users:index-shell')) return form( - {'userform': shell, 'action_name': 'Ajouter'}, + {'userform': shell, 'action_name': _("Add a shell")}, 'users/user.html', request ) @@ -676,10 +679,10 @@ def edit_shell(request, shell_instance, **_kwargs): if shell.is_valid(): if shell.changed_data: shell.save() - messages.success(request, "Le shell a été modifié") + messages.success(request, _("The shell was edited.")) return redirect(reverse('users:index-shell')) return form( - {'userform': shell, 'action_name': 'Editer'}, + {'userform': shell, 'action_name': _("Edit a shell")}, 'users/user.html', request ) @@ -691,7 +694,7 @@ def del_shell(request, shell, **_kwargs): """Destruction d'un shell""" if request.method == "POST": shell.delete() - messages.success(request, "Le shell a été détruit") + messages.success(request, _("The shell was deleted.")) return redirect(reverse('users:index-shell')) return form( {'objet': shell, 'objet_name': 'shell'}, @@ -708,10 +711,10 @@ def add_listright(request): listright = NewListRightForm(request.POST or None) if listright.is_valid(): listright.save() - messages.success(request, "Le droit/groupe a été ajouté") + messages.success(request, _("The group of rights was added.")) return redirect(reverse('users:index-listright')) return form( - {'userform': listright, 'action_name': 'Ajouter'}, + {'userform': listright, 'action_name': _("Add a group of rights")}, 'users/user.html', request ) @@ -729,10 +732,10 @@ def edit_listright(request, listright_instance, **_kwargs): if listright.is_valid(): if listright.changed_data: listright.save() - messages.success(request, "Droit modifié") + messages.success(request, _("The group of rights was edited.")) return redirect(reverse('users:index-listright')) return form( - {'userform': listright, 'action_name': 'Editer'}, + {'userform': listright, 'action_name': _("Edit a group of rights")}, 'users/user.html', request ) @@ -749,15 +752,16 @@ def del_listright(request, instances): for listright_del in listright_dels: try: listright_del.delete() - messages.success(request, "Le droit/groupe a été supprimé") + messages.success(request, _("The group of rights was" + " deleted.")) except ProtectedError: messages.error( request, - "Le groupe %s est affecté à au moins un user, \ - vous ne pouvez pas le supprimer" % listright_del) + _("The group of rights %s is assigned to at least one" + " user, impossible to delete it.") % listright_del) return redirect(reverse('users:index-listright')) return form( - {'userform': listright, 'action_name': 'Supprimer'}, + {'userform': listright, 'action_name': _("Delete")}, 'users/user.html', request ) @@ -781,8 +785,8 @@ def mass_archive(request): with transaction.atomic(), reversion.create_revision(): user.archive() user.save() - reversion.set_comment("Archivage") - messages.success(request, "%s users ont été archivés" % len( + reversion.set_comment(_("Archiving")) + messages.success(request, _("%s users were archived.") % len( to_archive_list )) return redirect(reverse('users:index')) @@ -1016,7 +1020,7 @@ def profil(request, users, **_kwargs): 'emailaddress_list': users.email_address, 'local_email_accounts_enabled': ( OptionalUser.objects.first().local_email_accounts_enabled - ) + ) } ) @@ -1031,18 +1035,17 @@ def reset_password(request): email=userform.cleaned_data['email'] ) except User.DoesNotExist: - messages.error(request, "Cet utilisateur n'existe pas") + messages.error(request, _("The user doesn't exist.")) return form( - {'userform': userform, 'action_name': 'Réinitialiser'}, + {'userform': userform, 'action_name': _("Reset")}, 'users/user.html', request ) user.reset_passwd_mail(request) - messages.success(request, "Un mail pour l'initialisation du mot\ - de passe a été envoyé") + messages.success(request, _("An email to reset the password was sent.")) redirect(reverse('index')) return form( - {'userform': userform, 'action_name': 'Réinitialiser'}, + {'userform': userform, 'action_name': _("Reset")}, 'users/user.html', request ) @@ -1056,7 +1059,7 @@ def process(request, token): if req.type == Request.PASSWD: return process_passwd(request, req) else: - messages.error(request, "Entrée incorrecte, contactez un admin") + messages.error(request, _("Error: please contact an admin.")) redirect(reverse('index')) @@ -1068,12 +1071,12 @@ def process_passwd(request, req): if u_form.is_valid(): with transaction.atomic(), reversion.create_revision(): u_form.save() - reversion.set_comment("Réinitialisation du mot de passe") + reversion.set_comment(_("Password reset")) req.delete() - messages.success(request, "Le mot de passe a changé") + messages.success(request, _("The password was changed.")) return redirect(reverse('index')) return form( - {'userform': u_form, 'action_name': 'Changer le mot de passe'}, + {'userform': u_form, 'action_name': _("Change the password")}, 'users/user.html', request ) @@ -1108,7 +1111,7 @@ def ml_std_members(request, ml_name): members = all_has_access().values('email').distinct() # Unknown mailing else: - messages.error(request, "Cette mailing n'existe pas") + messages.error(request, _("The mailing list doesn't exist.")) return redirect(reverse('index')) seria = MailingMemberSerializer(members, many=True) return JSONResponse(seria.data) @@ -1132,7 +1135,7 @@ def ml_club_admins(request, ml_name): try: club = Club.objects.get(mailing=True, pseudo=ml_name) except Club.DoesNotExist: - messages.error(request, "Cette mailing n'existe pas") + messages.error(request, _("The mailing list doesn't exist.")) return redirect(reverse('index')) members = club.administrators.all().values('email').distinct() seria = MailingMemberSerializer(members, many=True) @@ -1147,7 +1150,7 @@ def ml_club_members(request, ml_name): try: club = Club.objects.get(mailing=True, pseudo=ml_name) except Club.DoesNotExist: - messages.error(request, "Cette mailing n'existe pas") + messages.error(request, _("The mailing list doesn't exist.")) return redirect(reverse('index')) members = ( club.administrators.all().values('email').distinct() | @@ -1155,3 +1158,4 @@ def ml_club_members(request, ml_name): ) seria = MailingMemberSerializer(members, many=True) return JSONResponse(seria.data) +