diff --git a/.gitignore b/.gitignore index c65c2cc3..c978acac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,48 @@ -settings_local.py +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class *.swp -*.pyc + +# Translations +*.mo +*.pot + +# Django stuff +*.log +local_settings.py +db.sqlite3 + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# PyCharm project settings +.idea/ + +# Django statics +static_files/ +static/logo/ + +# re2o specific +settings_local.py re2o.png -__pycache__/* -static_files/* -static/logo/* -media/* +media/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fd4e0177..40ca06d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -150,3 +150,31 @@ On some database engines (postgreSQL) you also need to update the id sequences: ```bash python3 manage.py sqlsequencereset cotisations | python3 manage.py dbshell ``` + +## MR 296: Frontend changes + +Install fonts-font-awesome + +```bash +apt-get -y install fonts-font-awesome +``` + +Collec new statics + +```bash +python3 manage.py collectstatic +``` + +## MR 391: Document templates and subscription vouchers + +Re2o can now use templates for generated invoices. To load default templates run + +```bash +./install update +``` + +Be carefull, you need the proper rights to edit a DocumentTemplate. + +Re2o now sends subscription voucher when an invoice is controlled. It uses one +of the templates. You also need to set the name of the president of your association +to be set in your settings. diff --git a/README.md b/README.md index d3de9163..f4e38aa9 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Re2o -Gnu public license v2.0 +GNU public license v2.0 ## Avant propos -Re2o est un logiciel d'administration développé initiallement au rezometz. Il +Re2o est un logiciel d'administration développé initialement au rezometz. Il se veut agnostique au réseau considéré, de manière à être installable en quelques clics. @@ -31,15 +31,15 @@ Pour cela : ## Fonctionnement général -Re2o est séparé entre les models, qui sont visible sur le schéma des -dépendances. Il s'agit en réalité des tables sql, et les fields etant les +Re2o est séparé entre les models, qui sont visibles sur le schéma des +dépendances. Il s'agit en réalité des tables sql, et les fields étant les colonnes. Ceci dit il n'est jamais nécessaire de toucher directement au sql, django procédant automatiquement à tout cela. On crée donc différents models (user, right pour les droits des users, interfaces, IpList pour l'ensemble des adresses ip, etc) -Du coté des forms, il s'agit des formulaire d'édition des models. Il +Du coté des forms, il s'agit des formulaires d'édition des models. Il s'agit de ModelForms django, qui héritent des models très simplement, voir la documentation django models forms. @@ -56,12 +56,20 @@ d'accéder à ces vues, utilisé par re2o-tools. # Requète en base de donnée -Pour avoir un shell, il suffit de lancer '''python3 manage.py shell''' -Pour charger des objets, example avec User, faire : -''' from users.models import User''' -Pour charger les objets django, il suffit de faire User.objects.all() -pour tous les users par exemple. -Il est ensuite aisé de faire des requètes, par exemple -User.objects.filter(pseudo='test') -Des exemples et la documentation complète sur les requètes django sont +Pour avoir un shell, lancer : +```.bash +python3 manage.py shell +``` + +Pour charger des objets (exemple avec User), faire : +```.python +from users.models import User +``` + +Pour charger les objets django, il suffit de faire `User.objects.all()` +pour tous les users par exemple. +Il est ensuite aisé de faire des requêtes, par exemple +`User.objects.filter(pseudo='test')` + +Des exemples et la documentation complète sur les requêtes django sont disponible sur le site officiel. diff --git a/api/acl.py b/api/acl.py index 4d634beb..0c336281 100644 --- a/api/acl.py +++ b/api/acl.py @@ -26,9 +26,9 @@ done. """ from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.contrib.auth.models import Permission -from django.utils.translation import ugettext_lazy as _ +from django.contrib.contenttypes.models import ContentType +from django.utils.translation import ugettext as _ def _create_api_permission(): @@ -71,4 +71,5 @@ def can_view(user): 'codename': settings.API_PERMISSION_CODENAME } can = user.has_perm('%(app_label)s.%(codename)s' % kwargs) - return can, None if can else _("You cannot see this application.") + return can, None if can else _("You don't have the right to see this" + " application.") diff --git a/api/authentication.py b/api/authentication.py index 05dc626b..d426db24 100644 --- a/api/authentication.py +++ b/api/authentication.py @@ -26,12 +26,14 @@ import datetime from django.conf import settings from django.utils.translation import ugettext_lazy as _ -from rest_framework.authentication import TokenAuthentication from rest_framework import exceptions +from rest_framework.authentication import TokenAuthentication + class ExpiringTokenAuthentication(TokenAuthentication): """Authenticate a user if the provided token is valid and not expired. """ + def authenticate_credentials(self, key): """See base class. Add the verification the token is not expired. """ @@ -44,6 +46,6 @@ class ExpiringTokenAuthentication(TokenAuthentication): ) utc_now = datetime.datetime.now(datetime.timezone.utc) if token.created < utc_now - token_duration: - raise exceptions.AuthenticationFailed(_('Token has expired')) + raise exceptions.AuthenticationFailed(_("The token has expired.")) - return (token.user, token) + return token.user, token diff --git a/api/locale/fr/LC_MESSAGES/django.po b/api/locale/fr/LC_MESSAGES/django.po new file mode 100644 index 00000000..f2d6755e --- /dev/null +++ b/api/locale/fr/LC_MESSAGES/django.po @@ -0,0 +1,40 @@ +# 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: 2019-01-08 23:06+0100\n" +"PO-Revision-Date: 2019-01-07 01:37+0100\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" + +#: acl.py:74 +msgid "You don't have the right to see this application." +msgstr "Vous n'avez pas le droit de voir cette application." + +#: authentication.py:49 +msgid "The token has expired." +msgstr "Le jeton a expiré." diff --git a/api/permissions.py b/api/permissions.py index 0b666ebd..7d778c69 100644 --- a/api/permissions.py +++ b/api/permissions.py @@ -24,8 +24,6 @@ from rest_framework import permissions, exceptions -from re2o.acl import can_create, can_edit, can_delete, can_view_all - from . import acl @@ -57,14 +55,14 @@ def _get_param_in_view(view, param_name): AssertionError: None of the getter function or the attribute are defined in the view. """ - assert hasattr(view, 'get_'+param_name) \ - or getattr(view, param_name, None) is not None, ( + assert hasattr(view, 'get_' + param_name) \ + or getattr(view, param_name, None) is not None, ( 'cannot apply {} on a view that does not set ' '`.{}` or have a `.get_{}()` method.' ).format(self.__class__.__name__, param_name, param_name) - if hasattr(view, 'get_'+param_name): - param = getattr(view, 'get_'+param_name)() + if hasattr(view, 'get_' + param_name): + param = getattr(view, 'get_' + param_name)() assert param is not None, ( '{}.get_{}() returned None' ).format(view.__class__.__name__, param_name) @@ -80,7 +78,8 @@ class ACLPermission(permissions.BasePermission): See the wiki for the syntax of this attribute. """ - def get_required_permissions(self, method, view): + @staticmethod + def get_required_permissions(method, view): """Build the list of permissions required for the request to be accepted. @@ -153,15 +152,15 @@ class AutodetectACLPermission(permissions.BasePermission): 'OPTIONS': [can_see_api, lambda model: model.can_view_all], 'HEAD': [can_see_api, lambda model: model.can_view_all], 'POST': [can_see_api, lambda model: model.can_create], - 'PUT': [], # No restrictions, apply to objects - 'PATCH': [], # No restrictions, apply to objects + 'PUT': [], # No restrictions, apply to objects + 'PATCH': [], # No restrictions, apply to objects 'DELETE': [], # No restrictions, apply to objects } perms_obj_map = { 'GET': [can_see_api, lambda obj: obj.can_view], 'OPTIONS': [can_see_api, lambda obj: obj.can_view], 'HEAD': [can_see_api, lambda obj: obj.can_view], - 'POST': [], # No restrictions, apply to models + 'POST': [], # No restrictions, apply to models 'PUT': [can_see_api, lambda obj: obj.can_edit], 'PATCH': [can_see_api, lambda obj: obj.can_edit], 'DELETE': [can_see_api, lambda obj: obj.can_delete], @@ -209,7 +208,8 @@ class AutodetectACLPermission(permissions.BasePermission): return [perm(obj) for perm in self.perms_obj_map[method]] - def _queryset(self, view): + @staticmethod + def _queryset(view): return _get_param_in_view(view, 'queryset') def has_permission(self, request, view): @@ -282,4 +282,3 @@ class AutodetectACLPermission(permissions.BasePermission): return False return True - diff --git a/api/routers.py b/api/routers.py index 2d245382..7c9e4a0f 100644 --- a/api/routers.py +++ b/api/routers.py @@ -24,12 +24,12 @@ from collections import OrderedDict -from django.conf.urls import url, include +from django.conf.urls import url from django.core.urlresolvers import NoReverseMatch from rest_framework import views -from rest_framework.routers import DefaultRouter from rest_framework.response import Response from rest_framework.reverse import reverse +from rest_framework.routers import DefaultRouter from rest_framework.schemas import SchemaGenerator from rest_framework.settings import api_settings @@ -64,7 +64,8 @@ class AllViewsRouter(DefaultRouter): name = self.get_default_name(pattern) self.view_registry.append((pattern, view, name)) - def get_default_name(self, pattern): + @staticmethod + def get_default_name(pattern): """Returns the name to use for the route if none was specified. Args: @@ -113,7 +114,8 @@ class AllViewsRouter(DefaultRouter): _ignore_model_permissions = True renderer_classes = view_renderers - def get(self, request, *args, **kwargs): + @staticmethod + def get(request, *args, **kwargs): if request.accepted_renderer.media_type in schema_media_types: # Return a schema response. schema = schema_generator.get_schema(request) diff --git a/api/serializers.py b/api/serializers.py index 65a82eb6..8c22ed21 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -30,7 +30,6 @@ import preferences.models as preferences import topologie.models as topologie import users.models as users - # The namespace used for the API. It must match the namespace used in the # urlpatterns to include the API URLs. API_NAMESPACE = 'api' @@ -40,6 +39,7 @@ class NamespacedHRField(serializers.HyperlinkedRelatedField): """A `rest_framework.serializers.HyperlinkedRelatedField` subclass to automatically prefix view names with the API namespace. """ + def __init__(self, view_name=None, **kwargs): if view_name is not None: view_name = '%s:%s' % (API_NAMESPACE, view_name) @@ -50,6 +50,7 @@ class NamespacedHIField(serializers.HyperlinkedIdentityField): """A `rest_framework.serializers.HyperlinkedIdentityField` subclass to automatically prefix view names with teh API namespace. """ + def __init__(self, view_name=None, **kwargs): if view_name is not None: view_name = '%s:%s' % (API_NAMESPACE, view_name) @@ -70,24 +71,33 @@ class NamespacedHMSerializer(serializers.HyperlinkedModelSerializer): class FactureSerializer(NamespacedHMSerializer): """Serialize `cotisations.models.Facture` objects. """ + class Meta: model = cotisations.Facture fields = ('user', 'paiement', 'banque', 'cheque', 'date', 'valid', 'control', 'prix_total', 'name', 'api_url') +class BaseInvoiceSerializer(NamespacedHMSerializer): + class Meta: + model = cotisations.BaseInvoice + fields = ('__all__') + class VenteSerializer(NamespacedHMSerializer): """Serialize `cotisations.models.Vente` objects. """ + class Meta: model = cotisations.Vente - fields = ('facture', 'number', 'name', 'prix', 'duration', + fields = ('facture', + 'number', 'name', 'prix', 'duration', 'type_cotisation', 'prix_total', 'api_url') class ArticleSerializer(NamespacedHMSerializer): """Serialize `cotisations.models.Article` objects. """ + class Meta: model = cotisations.Article fields = ('name', 'prix', 'duration', 'type_user', @@ -97,6 +107,7 @@ class ArticleSerializer(NamespacedHMSerializer): class BanqueSerializer(NamespacedHMSerializer): """Serialize `cotisations.models.Banque` objects. """ + class Meta: model = cotisations.Banque fields = ('name', 'api_url') @@ -105,14 +116,16 @@ class BanqueSerializer(NamespacedHMSerializer): class PaiementSerializer(NamespacedHMSerializer): """Serialize `cotisations.models.Paiement` objects. """ + class Meta: model = cotisations.Paiement - fields = ('moyen', 'type_paiement', 'api_url') + fields = ('moyen', 'api_url') class CotisationSerializer(NamespacedHMSerializer): """Serialize `cotisations.models.Cotisation` objects. """ + class Meta: model = cotisations.Cotisation fields = ('vente', 'type_cotisation', 'date_start', 'date_end', @@ -125,6 +138,7 @@ class CotisationSerializer(NamespacedHMSerializer): class MachineSerializer(NamespacedHMSerializer): """Serialize `machines.models.Machine` objects. """ + class Meta: model = machines.Machine fields = ('user', 'name', 'active', 'api_url') @@ -133,6 +147,7 @@ class MachineSerializer(NamespacedHMSerializer): class MachineTypeSerializer(NamespacedHMSerializer): """Serialize `machines.models.MachineType` objects. """ + class Meta: model = machines.MachineType fields = ('type', 'ip_type', 'api_url') @@ -141,6 +156,7 @@ class MachineTypeSerializer(NamespacedHMSerializer): class IpTypeSerializer(NamespacedHMSerializer): """Serialize `machines.models.IpType` objects. """ + class Meta: model = machines.IpType fields = ('type', 'extension', 'need_infra', 'domaine_ip_start', @@ -151,14 +167,17 @@ class IpTypeSerializer(NamespacedHMSerializer): class VlanSerializer(NamespacedHMSerializer): """Serialize `machines.models.Vlan` objects. """ + class Meta: model = machines.Vlan - fields = ('vlan_id', 'name', 'comment', 'api_url') + fields = ('vlan_id', 'name', 'comment', 'arp_protect', 'dhcp_snooping', + 'dhcpv6_snooping', 'igmp', 'mld', 'api_url') class NasSerializer(NamespacedHMSerializer): """Serialize `machines.models.Nas` objects. """ + class Meta: model = machines.Nas fields = ('name', 'nas_type', 'machine_type', 'port_access_mode', @@ -168,6 +187,7 @@ class NasSerializer(NamespacedHMSerializer): class SOASerializer(NamespacedHMSerializer): """Serialize `machines.models.SOA` objects. """ + class Meta: model = machines.SOA fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl', @@ -177,6 +197,7 @@ class SOASerializer(NamespacedHMSerializer): class ExtensionSerializer(NamespacedHMSerializer): """Serialize `machines.models.Extension` objects. """ + class Meta: model = machines.Extension fields = ('name', 'need_infra', 'origin', 'origin_v6', 'soa', @@ -186,6 +207,7 @@ class ExtensionSerializer(NamespacedHMSerializer): class MxSerializer(NamespacedHMSerializer): """Serialize `machines.models.Mx` objects. """ + class Meta: model = machines.Mx fields = ('zone', 'priority', 'name', 'api_url') @@ -194,13 +216,16 @@ class MxSerializer(NamespacedHMSerializer): class DNameSerializer(NamespacedHMSerializer): """Serialize `machines.models.DName` objects. """ + class Meta: model = machines.DName fields = ('zone', 'alias', 'api_url') + class NsSerializer(NamespacedHMSerializer): """Serialize `machines.models.Ns` objects. """ + class Meta: model = machines.Ns fields = ('zone', 'ns', 'api_url') @@ -209,6 +234,7 @@ class NsSerializer(NamespacedHMSerializer): class TxtSerializer(NamespacedHMSerializer): """Serialize `machines.models.Txt` objects. """ + class Meta: model = machines.Txt fields = ('zone', 'field1', 'field2', 'api_url') @@ -217,14 +243,17 @@ class TxtSerializer(NamespacedHMSerializer): class SrvSerializer(NamespacedHMSerializer): """Serialize `machines.models.Srv` objects. """ + class Meta: model = machines.Srv fields = ('service', 'protocole', 'extension', 'ttl', 'priority', 'weight', 'port', 'target', 'api_url') + class SshFpSerializer(NamespacedHMSerializer): """Serialize `machines.models.SSHFP` objects. """ + class Meta: model = machines.SshFp field = ('machine', 'pub_key_entry', 'algo', 'comment', 'api_url') @@ -245,6 +274,7 @@ class InterfaceSerializer(NamespacedHMSerializer): class Ipv6ListSerializer(NamespacedHMSerializer): """Serialize `machines.models.Ipv6List` objects. """ + class Meta: model = machines.Ipv6List fields = ('ipv6', 'interface', 'slaac_ip', 'api_url') @@ -253,6 +283,7 @@ class Ipv6ListSerializer(NamespacedHMSerializer): class DomainSerializer(NamespacedHMSerializer): """Serialize `machines.models.Domain` objects. """ + class Meta: model = machines.Domain fields = ('interface_parent', 'name', 'extension', 'cname', @@ -262,6 +293,7 @@ class DomainSerializer(NamespacedHMSerializer): class IpListSerializer(NamespacedHMSerializer): """Serialize `machines.models.IpList` objects. """ + class Meta: model = machines.IpList fields = ('ipv4', 'ip_type', 'need_infra', 'api_url') @@ -270,6 +302,7 @@ class IpListSerializer(NamespacedHMSerializer): class ServiceSerializer(NamespacedHMSerializer): """Serialize `machines.models.Service` objects. """ + class Meta: model = machines.Service fields = ('service_type', 'min_time_regen', 'regular_time_regen', @@ -279,6 +312,7 @@ class ServiceSerializer(NamespacedHMSerializer): class ServiceLinkSerializer(NamespacedHMSerializer): """Serialize `machines.models.Service_link` objects. """ + class Meta: model = machines.Service_link fields = ('service', 'server', 'last_regen', 'asked_regen', @@ -305,11 +339,22 @@ class OuverturePortListSerializer(NamespacedHMSerializer): class OuverturePortSerializer(NamespacedHMSerializer): """Serialize `machines.models.OuverturePort` objects. """ + class Meta: model = machines.OuverturePort fields = ('begin', 'end', 'port_list', 'protocole', 'io', 'api_url') +class RoleSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePort` objects. + """ + servers = InterfaceSerializer(read_only=True, many=True) + + class Meta: + model = machines.Role + fields = ('role_type', 'servers', 'api_url') + + # PREFERENCES @@ -317,17 +362,21 @@ class OptionalUserSerializer(NamespacedHMSerializer): """Serialize `preferences.models.OptionalUser` objects. """ tel_mandatory = serializers.BooleanField(source='is_tel_mandatory') + shell_default = serializers.StringRelatedField() class Meta: model = preferences.OptionalUser - fields = ('tel_mandatory', 'user_solde', 'solde_negatif', 'max_solde', - 'min_online_payment', 'gpg_fingerprint', - 'all_can_create_club', 'self_adhesion', 'shell_default') + fields = ('tel_mandatory', 'gpg_fingerprint', + 'all_can_create_club', 'self_adhesion', 'shell_default', + 'self_change_shell', 'local_email_accounts_enabled', 'local_email_domain', + 'max_email_address', + ) class OptionalMachineSerializer(NamespacedHMSerializer): """Serialize `preferences.models.OptionalMachine` objects. """ + class Meta: model = preferences.OptionalMachine fields = ('password_machine', 'max_lambdauser_interfaces', @@ -338,27 +387,45 @@ class OptionalMachineSerializer(NamespacedHMSerializer): class OptionalTopologieSerializer(NamespacedHMSerializer): """Serialize `preferences.models.OptionalTopologie` objects. """ + switchs_management_interface_ip = serializers.CharField() class Meta: model = preferences.OptionalTopologie - fields = ('radius_general_policy', 'vlan_decision_ok', - 'vlan_decision_nok') + fields = ('switchs_ip_type', 'switchs_web_management', + 'switchs_web_management_ssl', 'switchs_rest_management', + 'switchs_management_utils', 'switchs_management_interface_ip', + 'provision_switchs_enabled', 'switchs_provision', 'switchs_management_sftp_creds') + + +class RadiusOptionSerializer(NamespacedHMSerializer): + """Serialize `preferences.models.RadiusOption` objects + """ + + class Meta: + model = preferences.RadiusOption + fields = ('radius_general_policy', 'unknown_machine', + 'unknown_machine_vlan', 'unknown_port', + 'unknown_port_vlan', 'unknown_room', 'unknown_room_vlan', + 'non_member', 'non_member_vlan', 'banned', 'banned_vlan', + 'vlan_decision_ok') class GeneralOptionSerializer(NamespacedHMSerializer): """Serialize `preferences.models.GeneralOption` objects. """ + class Meta: model = preferences.GeneralOption - fields = ('general_message', 'search_display_page', - 'pagination_number', 'pagination_large_number', - 'req_expire_hrs', 'site_name', 'email_from', 'GTU_sum_up', - 'GTU') - + fields = ('general_message_fr', 'general_message_en', + 'search_display_page', 'pagination_number', + 'pagination_large_number', 'req_expire_hrs', + 'site_name', 'main_site_url', 'email_from', + 'GTU_sum_up', 'GTU') class HomeServiceSerializer(NamespacedHMSerializer): """Serialize `preferences.models.Service` objects. """ + class Meta: model = preferences.Service fields = ('name', 'url', 'description', 'image', 'api_url') @@ -370,16 +437,17 @@ class HomeServiceSerializer(NamespacedHMSerializer): class AssoOptionSerializer(NamespacedHMSerializer): """Serialize `preferences.models.AssoOption` objects. """ + class Meta: model = preferences.AssoOption fields = ('name', 'siret', 'adresse1', 'adresse2', 'contact', - 'telephone', 'pseudo', 'utilisateur_asso', 'payment', - 'payment_id', 'payment_pass', 'description') + 'telephone', 'pseudo', 'utilisateur_asso', 'description') class HomeOptionSerializer(NamespacedHMSerializer): """Serialize `preferences.models.HomeOption` objects. """ + class Meta: model = preferences.HomeOption fields = ('facebook_url', 'twitter_url', 'twitter_account_name') @@ -388,18 +456,19 @@ class HomeOptionSerializer(NamespacedHMSerializer): class MailMessageOptionSerializer(NamespacedHMSerializer): """Serialize `preferences.models.MailMessageOption` objects. """ + class Meta: model = preferences.MailMessageOption fields = ('welcome_mail_fr', 'welcome_mail_en') - # TOPOLOGIE class StackSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Stack` objects """ + class Meta: model = topologie.Stack fields = ('name', 'stack_id', 'details', 'member_id_min', @@ -409,6 +478,7 @@ class StackSerializer(NamespacedHMSerializer): class AccessPointSerializer(NamespacedHMSerializer): """Serialize `topologie.models.AccessPoint` objects """ + class Meta: model = topologie.AccessPoint fields = ('user', 'name', 'active', 'location', 'api_url') @@ -418,6 +488,7 @@ class SwitchSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Switch` objects """ port_amount = serializers.IntegerField(source='number') + class Meta: model = topologie.Switch fields = ('user', 'name', 'active', 'port_amount', 'stack', @@ -427,6 +498,7 @@ class SwitchSerializer(NamespacedHMSerializer): class ServerSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Server` objects """ + class Meta: model = topologie.Server fields = ('user', 'name', 'active', 'api_url') @@ -435,6 +507,7 @@ class ServerSerializer(NamespacedHMSerializer): class ModelSwitchSerializer(NamespacedHMSerializer): """Serialize `topologie.models.ModelSwitch` objects """ + class Meta: model = topologie.ModelSwitch fields = ('reference', 'constructor', 'api_url') @@ -443,6 +516,7 @@ class ModelSwitchSerializer(NamespacedHMSerializer): class ConstructorSwitchSerializer(NamespacedHMSerializer): """Serialize `topologie.models.ConstructorSwitch` objects """ + class Meta: model = topologie.ConstructorSwitch fields = ('name', 'api_url') @@ -451,6 +525,7 @@ class ConstructorSwitchSerializer(NamespacedHMSerializer): class SwitchBaySerializer(NamespacedHMSerializer): """Serialize `topologie.models.SwitchBay` objects """ + class Meta: model = topologie.SwitchBay fields = ('name', 'building', 'info', 'api_url') @@ -459,6 +534,7 @@ class SwitchBaySerializer(NamespacedHMSerializer): class BuildingSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Building` objects """ + class Meta: model = topologie.Building fields = ('name', 'api_url') @@ -467,19 +543,34 @@ class BuildingSerializer(NamespacedHMSerializer): class SwitchPortSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Port` objects """ + + get_port_profile = NamespacedHIField(view_name='portprofile-detail', read_only=True) + class Meta: model = topologie.Port fields = ('switch', 'port', 'room', 'machine_interface', 'related', - 'custom_profile', 'state', 'details', 'api_url') + 'custom_profile', 'state', 'get_port_profile', 'details', 'api_url') extra_kwargs = { 'related': {'view_name': 'switchport-detail'}, 'api_url': {'view_name': 'switchport-detail'}, } +class PortProfileSerializer(NamespacedHMSerializer): + """Serialize `topologie.models.Room` objects + """ + 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', 'dhcpv6_snooping', 'arp_protect', + 'ra_guard', 'loop_protect', 'api_url') + + class RoomSerializer(NamespacedHMSerializer): """Serialize `topologie.models.Room` objects """ + class Meta: model = topologie.Room fields = ('name', 'details', 'api_url') @@ -547,14 +638,14 @@ 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','gid') + '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 - """ + +class BasicUserSerializer(NamespacedHMSerializer): + """Serialize 'users.models.User' minimal infos""" uid = serializers.IntegerField(source='uid_number') gid = serializers.IntegerField(source='gid_number') @@ -562,9 +653,11 @@ class HomeCreationSerializer(NamespacedHMSerializer): model = users.User fields = ('pseudo', 'uid', 'gid') + class ServiceUserSerializer(NamespacedHMSerializer): """Serialize `users.models.ServiceUser` objects. """ + class Meta: model = users.ServiceUser fields = ('pseudo', 'access_group', 'comment', 'api_url') @@ -573,6 +666,7 @@ class ServiceUserSerializer(NamespacedHMSerializer): class SchoolSerializer(NamespacedHMSerializer): """Serialize `users.models.School` objects. """ + class Meta: model = users.School fields = ('name', 'api_url') @@ -581,6 +675,7 @@ class SchoolSerializer(NamespacedHMSerializer): class ListRightSerializer(NamespacedHMSerializer): """Serialize `users.models.ListRight` objects. """ + class Meta: model = users.ListRight fields = ('unix_name', 'gid', 'critical', 'details', 'api_url') @@ -589,6 +684,7 @@ class ListRightSerializer(NamespacedHMSerializer): class ShellSerializer(NamespacedHMSerializer): """Serialize `users.models.ListShell` objects. """ + class Meta: model = users.ListShell fields = ('shell', 'api_url') @@ -622,6 +718,7 @@ 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') @@ -644,6 +741,90 @@ class ServiceRegenSerializer(NamespacedHMSerializer): 'api_url': {'view_name': 'serviceregen-detail'} } +# Switches et ports + +class InterfaceVlanSerializer(NamespacedHMSerializer): + domain = serializers.CharField(read_only=True) + ipv4 = serializers.CharField(read_only=True) + ipv6 = Ipv6ListSerializer(read_only=True, many=True) + vlan_id = serializers.IntegerField(source='type.ip_type.vlan.vlan_id', read_only=True) + + class Meta: + model = machines.Interface + fields = ('ipv4', 'ipv6', 'domain', 'vlan_id') + +class InterfaceRoleSerializer(NamespacedHMSerializer): + interface = InterfaceVlanSerializer(source='machine.interface_set', read_only=True, many=True) + + class Meta: + model = machines.Interface + fields = ('interface',) + + +class RoleSerializer(NamespacedHMSerializer): + """Serialize `machines.models.OuverturePort` objects. + """ + servers = InterfaceRoleSerializer(read_only=True, many=True) + + class Meta: + model = machines.Role + fields = ('role_type', 'servers', 'specific_role') + + +class VlanPortSerializer(NamespacedHMSerializer): + class Meta: + model = machines.Vlan + fields = ('vlan_id', 'name') + + +class ProfilSerializer(NamespacedHMSerializer): + vlan_untagged = VlanSerializer(read_only=True) + vlan_tagged = VlanPortSerializer(read_only=True, many=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') + + +class ModelSwitchSerializer(NamespacedHMSerializer): + constructor = serializers.CharField(read_only=True) + + class Meta: + model = topologie.ModelSwitch + fields = ('reference', 'firmware', 'constructor') + + +class SwitchBaySerializer(NamespacedHMSerializer): + class Meta: + model = topologie.SwitchBay + fields = ('name',) + + +class PortsSerializer(NamespacedHMSerializer): + """Serialize `machines.models.Ipv6List` objects. + """ + get_port_profile = ProfilSerializer(read_only=True) + + + class Meta: + model = topologie.Port + fields = ('state', 'port', 'pretty_name', 'get_port_profile') + + + +class SwitchPortSerializer(serializers.ModelSerializer): + """Serialize the data about the switches""" + ports = PortsSerializer(many=True, read_only=True) + model = ModelSwitchSerializer(read_only=True) + switchbay = SwitchBaySerializer(read_only=True) + + + class Meta: + model = topologie.Switch + fields = ('short_name', 'model', 'switchbay', 'ports', 'ipv4', 'ipv6', + 'interfaces_subnet', 'interfaces6_subnet', 'automatic_provision', 'rest_enabled', + 'web_management_enabled', 'get_radius_key_value', 'get_management_cred_value', + 'list_modules') # LOCAL EMAILS @@ -660,13 +841,14 @@ class LocalEmailUsersSerializer(NamespacedHMSerializer): 'email_address', 'email') -#Firewall +# 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) @@ -677,6 +859,7 @@ class FirewallOuverturePortListSerializer(serializers.ModelSerializer): 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) @@ -684,6 +867,7 @@ class SubnetPortsOpenSerializer(serializers.ModelSerializer): 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) @@ -693,6 +877,7 @@ class InterfacePortsOpenSerializer(serializers.ModelSerializer): model = machines.Interface fields = ('port_lists', 'ipv4', 'ipv6') + # DHCP @@ -717,6 +902,7 @@ class SOARecordSerializer(SOASerializer): """Serialize `machines.models.SOA` objects with the data needed to generate a SOA DNS record. """ + class Meta: model = machines.SOA fields = ('name', 'mail', 'refresh', 'retry', 'expire', 'ttl') @@ -726,6 +912,7 @@ class OriginV4RecordSerializer(IpListSerializer): """Serialize `machines.models.IpList` objects with the data needed to generate an IPv4 Origin DNS record. """ + class Meta(IpListSerializer.Meta): fields = ('ipv4',) @@ -754,6 +941,7 @@ class TXTRecordSerializer(TxtSerializer): """Serialize `machines.models.Txt` objects with the data needed to generate a TXT DNS record. """ + class Meta(TxtSerializer.Meta): fields = ('field1', 'field2') @@ -772,6 +960,7 @@ class SSHFPRecordSerializer(SshFpSerializer): """Serialize `machines.models.SshFp` objects with the data needed to generate a SSHFP DNS record. """ + class Meta(SshFpSerializer.Meta): fields = ('algo_id', 'hash') @@ -823,6 +1012,17 @@ class CNAMERecordSerializer(serializers.ModelSerializer): model = machines.Domain fields = ('alias', 'hostname') +class DNAMERecordSerializer(serializers.ModelSerializer): + """Serialize `machines.models.Domain` objects with the data needed to + generate a DNAME DNS record. + """ + alias = serializers.CharField(read_only=True) + zone = serializers.CharField(read_only=True) + + class Meta: + model = machines.DName + fields = ('alias', 'zone') + class DNSZonesSerializer(serializers.ModelSerializer): """Serialize the data about DNS Zones. @@ -837,13 +1037,33 @@ class DNSZonesSerializer(serializers.ModelSerializer): a_records = ARecordSerializer(many=True, source='get_associated_a_records') aaaa_records = AAAARecordSerializer(many=True, source='get_associated_aaaa_records') cname_records = CNAMERecordSerializer(many=True, source='get_associated_cname_records') + dname_records = DNAMERecordSerializer(many=True, source='get_associated_dname_records') sshfp_records = SSHFPInterfaceSerializer(many=True, source='get_associated_sshfp_records') class Meta: model = machines.Extension fields = ('name', 'soa', 'ns_records', 'originv4', 'originv6', 'mx_records', 'txt_records', 'srv_records', 'a_records', - 'aaaa_records', 'cname_records', 'sshfp_records') + 'aaaa_records', 'cname_records', 'dname_records', 'sshfp_records') +#REMINDER + + +class ReminderUsersSerializer(UserSerializer): + """Serialize the data about a mailing member. + """ + class Meta(UserSerializer.Meta): + fields = ('get_full_name', 'get_mail') + + +class ReminderSerializer(serializers.ModelSerializer): + """ + Serialize the data about a reminder + """ + users_to_remind = ReminderUsersSerializer(many=True) + + class Meta: + model = preferences.Reminder + fields = ('days','message','users_to_remind') class DNSReverseZonesSerializer(serializers.ModelSerializer): @@ -858,22 +1078,24 @@ class DNSReverseZonesSerializer(serializers.ModelSerializer): 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 class MailingMemberSerializer(UserSerializer): """Serialize the data about a mailing member. """ + class Meta(UserSerializer.Meta): fields = ('name', 'pseudo', 'get_mail') + class MailingSerializer(ClubSerializer): """Serialize the data about a mailing. """ diff --git a/api/settings.py b/api/settings.py index 925d503a..0ddf5d69 100644 --- a/api/settings.py +++ b/api/settings.py @@ -48,4 +48,4 @@ API_APPS = ( ) # The expiration time for an authentication token -API_TOKEN_DURATION = 86400 # 24 hours +API_TOKEN_DURATION = 86400 # 24 hours diff --git a/api/tests.py b/api/tests.py index 0931ab8e..5c244152 100644 --- a/api/tests.py +++ b/api/tests.py @@ -21,10 +21,11 @@ """Defines the test suite for the API """ -import json import datetime -from rest_framework.test import APITestCase +import json + from requests import codes +from rest_framework.test import APITestCase import cotisations.models as cotisations import machines.models as machines @@ -33,7 +34,7 @@ import topologie.models as topologie import users.models as users -class APIEndpointsTestCase(APITestCase): +class APIEndpointsTestCase(APITestCase): """Test case to test that all endpoints are reachable with respects to authentication and permission checks. @@ -148,10 +149,10 @@ class APIEndpointsTestCase(APITestCase): '/api/users/club/', # 4th user to be create (stduser, superuser, users_adherent_1, # users_club_1) - '/api/users/club/4/', + '/api/users/club/4/', '/api/users/listright/', -# TODO: Merge !145 -# '/api/users/listright/1/', + # TODO: Merge !145 + # '/api/users/listright/1/', '/api/users/school/', '/api/users/school/1/', '/api/users/serviceuser/', @@ -215,7 +216,7 @@ class APIEndpointsTestCase(APITestCase): '/api/users/user/4242/', '/api/users/whitelist/4242/', ] - + stduser = None superuser = None @@ -363,7 +364,7 @@ class APIEndpointsTestCase(APITestCase): machine=cls.machines_machine_1, # Dep machines.Machine type=cls.machines_machinetype_1, # Dep machines.MachineType details="machines Interface 1", - #port_lists=[cls.machines_ouvertureportlist_1] # Dep machines.OuverturePortList + # port_lists=[cls.machines_ouvertureportlist_1] # Dep machines.OuverturePortList ) cls.machines_domain_1 = machines.Domain.objects.create( interface_parent=cls.machines_interface_1, # Dep machines.Interface @@ -525,14 +526,14 @@ class APIEndpointsTestCase(APITestCase): uid_number=21103, rezo_rez_uid=21103 ) -# Need merge of MR145 to work -# TODO: Merge !145 -# cls.users_listright_1 = users.ListRight.objects.create( -# unix_name="userslistright", -# gid=601, -# critical=False, -# details="userslistright" -# ) + # Need merge of MR145 to work + # TODO: Merge !145 + # cls.users_listright_1 = users.ListRight.objects.create( + # unix_name="userslistright", + # gid=601, + # critical=False, + # details="userslistright" + # ) cls.users_serviceuser_1 = users.ServiceUser.objects.create( password="password", last_login=datetime.datetime.now(datetime.timezone.utc), @@ -663,7 +664,7 @@ class APIEndpointsTestCase(APITestCase): AssertionError: An endpoint did not have a 200 status code. """ self.client.force_authenticate(user=self.superuser) - + urls = self.no_auth_endpoints + self.auth_no_perm_endpoints + \ self.auth_perm_endpoints @@ -676,6 +677,7 @@ class APIEndpointsTestCase(APITestCase): formats=[None, 'json', 'api'], assert_more=assert_more) + class APIPaginationTestCase(APITestCase): """Test case to check that the pagination is used on all endpoints that should use it. @@ -756,7 +758,7 @@ class APIPaginationTestCase(APITestCase): @classmethod def tearDownClass(cls): cls.superuser.delete() - super().tearDownClass() + super(APIPaginationTestCase, self).tearDownClass() def test_pagination(self): """Tests that every endpoint is using the pagination correctly. @@ -776,4 +778,3 @@ class APIPaginationTestCase(APITestCase): assert 'previous' in res_json.keys() assert 'results' in res_json.keys() assert not len('results') > 100 - diff --git a/api/urls.py b/api/urls.py index abc466e1..4a34c1de 100644 --- a/api/urls.py +++ b/api/urls.py @@ -32,7 +32,6 @@ from django.conf.urls import url, include from . import views from .routers import AllViewsRouter - router = AllViewsRouter() # COTISATIONS router.register_viewset(r'cotisations/facture', views.FactureViewSet) @@ -63,10 +62,12 @@ router.register_viewset(r'machines/service', views.ServiceViewSet) router.register_viewset(r'machines/servicelink', views.ServiceLinkViewSet, base_name='servicelink') router.register_viewset(r'machines/ouvertureportlist', views.OuverturePortListViewSet) router.register_viewset(r'machines/ouvertureport', views.OuverturePortViewSet) +router.register_viewset(r'machines/role', views.RoleViewSet) # PREFERENCES router.register_view(r'preferences/optionaluser', views.OptionalUserView), router.register_view(r'preferences/optionalmachine', views.OptionalMachineView), router.register_view(r'preferences/optionaltopologie', views.OptionalTopologieView), +router.register_view(r'preferences/radiusoption', views.RadiusOptionView), router.register_view(r'preferences/generaloption', views.GeneralOptionView), router.register_viewset(r'preferences/service', views.HomeServiceViewSet, base_name='homeservice'), router.register_view(r'preferences/assooption', views.AssoOptionView), @@ -81,12 +82,15 @@ 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(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') +router.register_viewset(r'topologie/switchport', views.SwitchPortViewSet, base_name='switchport') +router.register_viewset(r'topologie/portprofile', views.PortProfileViewSet, base_name='portprofile') 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/user', views.UserViewSet, base_name='user') +router.register_viewset(r'users/homecreation', views.HomeCreationViewSet, base_name='homecreation') +router.register_viewset(r'users/normaluser', views.NormalUserViewSet, base_name='normaluser') +router.register_viewset(r'users/criticaluser', views.CriticalUserViewSet, base_name='criticaluser') router.register_viewset(r'users/club', views.ClubViewSet) router.register_viewset(r'users/adherent', views.AdherentViewSet) router.register_viewset(r'users/serviceuser', views.ServiceUserViewSet) @@ -105,6 +109,11 @@ 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), +# Switches config +router.register_view(r'switchs/ports-config', views.SwitchPortView), +router.register_view(r'switchs/role', views.RoleView), +# Reminder +router.register_view(r'reminder/get-users', views.ReminderView), # DNS router.register_view(r'dns/zones', views.DNSZonesView), router.register_view(r'dns/reverse-zones', views.DNSReverseZonesView), @@ -114,7 +123,6 @@ router.register_view(r'mailing/club', views.ClubMailingView), # TOKEN AUTHENTICATION router.register_view(r'token-auth', views.ObtainExpiringAuthToken) - urlpatterns = [ url(r'^', include(router.urls)), ] diff --git a/api/views.py b/api/views.py index 0f6301bc..3108f9f3 100644 --- a/api/views.py +++ b/api/views.py @@ -29,10 +29,11 @@ the response (JSON or other), the CSRF exempting, ... import datetime from django.conf import settings -from rest_framework.authtoken.views import ObtainAuthToken -from rest_framework.authtoken.models import Token -from rest_framework.response import Response +from django.db.models import Q from rest_framework import viewsets, generics, views +from rest_framework.authtoken.models import Token +from rest_framework.authtoken.views import ObtainAuthToken +from rest_framework.response import Response import cotisations.models as cotisations import machines.models as machines @@ -40,7 +41,6 @@ import preferences.models as preferences import topologie.models as topologie import users.models as users from re2o.utils import all_active_interfaces, all_has_access - from . import serializers from .pagination import PageSizedPagination from .permissions import ACLPermission @@ -55,6 +55,12 @@ class FactureViewSet(viewsets.ReadOnlyModelViewSet): queryset = cotisations.Facture.objects.all() serializer_class = serializers.FactureSerializer +class FactureViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `cotisations.models.Facture` objects. + """ + queryset = cotisations.BaseInvoice.objects.all() + serializer_class = serializers.BaseInvoiceSerializer + class VenteViewSet(viewsets.ReadOnlyModelViewSet): """Exposes list and details of `cotisations.models.Vente` objects. @@ -163,6 +169,7 @@ class TxtViewSet(viewsets.ReadOnlyModelViewSet): queryset = machines.Txt.objects.all() serializer_class = serializers.TxtSerializer + class DNameViewSet(viewsets.ReadOnlyModelViewSet): """Exposes list and details of `machines.models.DName` objects. """ @@ -241,6 +248,13 @@ class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = serializers.OuverturePortSerializer +class RoleViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `machines.models.Machine` objects. + """ + queryset = machines.Role.objects.all() + serializer_class = serializers.RoleSerializer + + # PREFERENCES # Those views differ a bit because there is only one object # to display, so we don't bother with the listing part @@ -248,8 +262,8 @@ class OuverturePortViewSet(viewsets.ReadOnlyModelViewSet): class OptionalUserView(generics.RetrieveAPIView): """Exposes details of `preferences.models.` settings. """ - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [preferences.OptionalUser.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.OptionalUser.can_view_all]} serializer_class = serializers.OptionalUserSerializer def get_object(self): @@ -259,8 +273,8 @@ class OptionalUserView(generics.RetrieveAPIView): class OptionalMachineView(generics.RetrieveAPIView): """Exposes details of `preferences.models.OptionalMachine` settings. """ - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [preferences.OptionalMachine.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.OptionalMachine.can_view_all]} serializer_class = serializers.OptionalMachineSerializer def get_object(self): @@ -270,19 +284,30 @@ class OptionalMachineView(generics.RetrieveAPIView): class OptionalTopologieView(generics.RetrieveAPIView): """Exposes details of `preferences.models.OptionalTopologie` settings. """ - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [preferences.OptionalTopologie.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.OptionalTopologie.can_view_all]} serializer_class = serializers.OptionalTopologieSerializer def get_object(self): return preferences.OptionalTopologie.objects.first() +class RadiusOptionView(generics.RetrieveAPIView): + """Exposes details of `preferences.models.OptionalTopologie` settings. + """ + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.RadiusOption.can_view_all]} + serializer_class = serializers.RadiusOptionSerializer + + def get_object(self): + return preferences.RadiusOption.objects.first() + + class GeneralOptionView(generics.RetrieveAPIView): """Exposes details of `preferences.models.GeneralOption` settings. """ - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [preferences.GeneralOption.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.GeneralOption.can_view_all]} serializer_class = serializers.GeneralOptionSerializer def get_object(self): @@ -299,8 +324,8 @@ class HomeServiceViewSet(viewsets.ReadOnlyModelViewSet): class AssoOptionView(generics.RetrieveAPIView): """Exposes details of `preferences.models.AssoOption` settings. """ - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [preferences.AssoOption.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.AssoOption.can_view_all]} serializer_class = serializers.AssoOptionSerializer def get_object(self): @@ -310,8 +335,8 @@ class AssoOptionView(generics.RetrieveAPIView): class HomeOptionView(generics.RetrieveAPIView): """Exposes details of `preferences.models.HomeOption` settings. """ - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [preferences.HomeOption.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.HomeOption.can_view_all]} serializer_class = serializers.HomeOptionSerializer def get_object(self): @@ -321,8 +346,8 @@ class HomeOptionView(generics.RetrieveAPIView): class MailMessageOptionView(generics.RetrieveAPIView): """Exposes details of `preferences.models.MailMessageOption` settings. """ - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [preferences.MailMessageOption.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [preferences.MailMessageOption.can_view_all]} serializer_class = serializers.MailMessageOptionSerializer def get_object(self): @@ -396,6 +421,13 @@ class SwitchPortViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = serializers.SwitchPortSerializer +class PortProfileViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes list and details of `topologie.models.PortProfile` objects. + """ + queryset = topologie.PortProfile.objects.all() + serializer_class = serializers.PortProfileSerializer + + class RoomViewSet(viewsets.ReadOnlyModelViewSet): """Exposes list and details of `topologie.models.Room` objects. """ @@ -409,6 +441,7 @@ class PortProfileViewSet(viewsets.ReadOnlyModelViewSet): queryset = topologie.PortProfile.objects.all() serializer_class = serializers.PortProfileSerializer + # USER @@ -418,11 +451,25 @@ 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 + queryset = users.User.objects.exclude(Q(state=users.User.STATE_DISABLED) | Q(state=users.User.STATE_NOT_YET_ACTIVE)) + serializer_class = serializers.BasicUserSerializer + + +class NormalUserViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes infos of `users.models.Users`without specific rights objects.""" + queryset = users.User.objects.exclude(groups__listright__critical=True).distinct() + serializer_class = serializers.BasicUserSerializer + + +class CriticalUserViewSet(viewsets.ReadOnlyModelViewSet): + """Exposes infos of `users.models.Users`without specific rights objects.""" + queryset = users.User.objects.filter(groups__listright__critical=True).distinct() + serializer_class = serializers.BasicUserSerializer + class ClubViewSet(viewsets.ReadOnlyModelViewSet): """Exposes list and details of `users.models.Club` objects. @@ -488,7 +535,7 @@ class EMailAddressViewSet(viewsets.ReadOnlyModelViewSet): def get_queryset(self): if preferences.OptionalUser.get_cached_value( - 'local_email_accounts_enabled'): + 'local_email_accounts_enabled'): return (users.EMailAddress.objects .filter(user__local_email_enabled=True)) else: @@ -514,6 +561,31 @@ class ServiceRegenViewSet(viewsets.ModelViewSet): queryset = queryset.filter(server__domain__name__iexact=hostname) return queryset +# Config des switches + +class SwitchPortView(generics.ListAPIView): + """Output each port of a switch, to be serialized with + additionnal informations (profiles etc) + """ + queryset = topologie.Switch.objects.all().select_related("switchbay").select_related("model__constructor").prefetch_related("ports__custom_profile__vlan_tagged").prefetch_related("ports__custom_profile__vlan_untagged").prefetch_related("ports__machine_interface__domain__extension").prefetch_related("ports__room") + + serializer_class = serializers.SwitchPortSerializer + +# Rappel fin adhésion + +class ReminderView(generics.ListAPIView): + """Output for users to remind an end of their subscription. + """ + queryset = preferences.Reminder.objects.all() + serializer_class = serializers.ReminderSerializer + + +class RoleView(generics.ListAPIView): + """Output of roles for each server + """ + queryset = machines.Role.objects.all().prefetch_related('servers') + serializer_class = serializers.RoleSerializer + # LOCAL EMAILS @@ -525,7 +597,7 @@ class LocalEmailUsersView(generics.ListAPIView): def get_queryset(self): if preferences.OptionalUser.get_cached_value( - 'local_email_accounts_enabled'): + 'local_email_accounts_enabled'): return (users.User.objects .filter(local_email_enabled=True)) else: @@ -539,20 +611,24 @@ class HostMacIpView(generics.ListAPIView): """Exposes the associations between hostname, mac address and IPv4 in order to build the DHCP lease files. """ - queryset = all_active_interfaces() serializer_class = serializers.HostMacIpSerializer + def get_queryset(self): + return all_active_interfaces() -#Firewall + +# 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 @@ -570,16 +646,15 @@ class DNSZonesView(generics.ListAPIView): .all()) serializer_class = serializers.DNSZonesSerializer + class DNSReverseZonesView(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.IpType.objects.all()) serializer_class = serializers.DNSReverseZonesSerializer - - # MAILING @@ -588,8 +663,8 @@ class StandardMailingView(views.APIView): order to building the corresponding mailing lists. """ pagination_class = PageSizedPagination - permission_classes = (ACLPermission, ) - perms_map = {'GET' : [users.User.can_view_all]} + permission_classes = (ACLPermission,) + perms_map = {'GET': [users.User.can_view_all]} def get(self, request, format=None): adherents_data = serializers.MailingMemberSerializer(all_has_access(), many=True).data @@ -617,6 +692,7 @@ class ObtainExpiringAuthToken(ObtainAuthToken): `rest_framework.auth_token.views.ObtainAuthToken` view except that the expiration time is send along with the token as an addtional information. """ + def post(self, request, *args, **kwargs): serializer = self.serializer_class(data=request.data) serializer.is_valid(raise_exception=True) diff --git a/apt_requirements.txt b/apt_requirements.txt index 46d4c752..0cf83a7c 100644 --- a/apt_requirements.txt +++ b/apt_requirements.txt @@ -14,4 +14,7 @@ libjs-jquery libjs-jquery-ui libjs-jquery-timepicker libjs-bootstrap +fonts-font-awesome graphviz +git +gettext diff --git a/cotisations/admin.py b/cotisations/admin.py index afe4621c..4b47ccc8 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -30,7 +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 +from .models import CustomInvoice, CostEstimate class FactureAdmin(VersionAdmin): @@ -38,6 +38,11 @@ class FactureAdmin(VersionAdmin): pass +class CostEstimateAdmin(VersionAdmin): + """Admin class for cost estimates.""" + pass + + class CustomInvoiceAdmin(VersionAdmin): """Admin class for custom invoices.""" pass @@ -76,3 +81,4 @@ admin.site.register(Paiement, PaiementAdmin) admin.site.register(Vente, VenteAdmin) admin.site.register(Cotisation, CotisationAdmin) admin.site.register(CustomInvoice, CustomInvoiceAdmin) +admin.site.register(CostEstimate, CostEstimateAdmin) diff --git a/cotisations/forms.py b/cotisations/forms.py index 9194597a..3f99382b 100644 --- a/cotisations/forms.py +++ b/cotisations/forms.py @@ -46,7 +46,10 @@ 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, CustomInvoice +from .models import ( + Article, Paiement, Facture, Banque, + CustomInvoice, Vente, CostEstimate, +) from .payment_methods import balance @@ -102,9 +105,46 @@ class SelectArticleForm(FormRevMixin, Form): def __init__(self, *args, **kwargs): user = kwargs.pop('user') - target_user = kwargs.pop('target_user') + target_user = kwargs.pop('target_user', None) super(SelectArticleForm, self).__init__(*args, **kwargs) - self.fields['article'].queryset = Article.find_allowed_articles(user, target_user) + self.fields['article'].queryset = Article.find_allowed_articles( + user, target_user) + + +class DiscountForm(Form): + """ + Form used in oder to create a discount on an invoice. + """ + is_relative = forms.BooleanField( + label=_("Discount is on percentage."), + required=False, + ) + discount = forms.DecimalField( + label=_("Discount"), + max_value=100, + min_value=0, + max_digits=5, + decimal_places=2, + required=False, + ) + + def apply_to_invoice(self, invoice): + invoice_price = invoice.prix_total() + discount = self.cleaned_data['discount'] + is_relative = self.cleaned_data['is_relative'] + if is_relative: + amount = discount/100 * invoice_price + else: + amount = discount + if amount: + name = _("{}% discount") if is_relative else _("{}€ discount") + name = name.format(discount) + Vente.objects.create( + facture=invoice, + name=name, + prix=-amount, + number=1 + ) class CustomInvoiceForm(FormRevMixin, ModelForm): @@ -116,6 +156,15 @@ class CustomInvoiceForm(FormRevMixin, ModelForm): fields = '__all__' +class CostEstimateForm(FormRevMixin, ModelForm): + """ + Form used to create a cost estimate. + """ + class Meta: + model = CostEstimate + exclude = ['paid', 'final_invoice'] + + class ArticleForm(FormRevMixin, ModelForm): """ Form used to create an article. @@ -233,7 +282,7 @@ class RechargeForm(FormRevMixin, Form): """ Form used to refill a user's balance """ - value = forms.FloatField( + value = forms.DecimalField( label=_("Amount"), min_value=0.01, validators=[] @@ -248,7 +297,8 @@ class RechargeForm(FormRevMixin, Form): super(RechargeForm, self).__init__(*args, **kwargs) self.fields['payment'].empty_label = \ _("Select a payment method") - self.fields['payment'].queryset = Paiement.find_allowed_payments(user_source).exclude(is_balance=True) + self.fields['payment'].queryset = Paiement.find_allowed_payments( + user_source).exclude(is_balance=True) def clean(self): """ @@ -260,10 +310,9 @@ class RechargeForm(FormRevMixin, Form): if balance_method.maximum_balance is not None and \ value + self.user.solde > balance_method.maximum_balance: raise forms.ValidationError( - _("Requested amount is too high. Your balance can't exceed \ - %(max_online_balance)s €.") % { + _("Requested amount is too high. Your balance can't exceed" + " %(max_online_balance)s €.") % { 'max_online_balance': balance_method.maximum_balance } ) return self.cleaned_data - diff --git a/cotisations/locale/fr/LC_MESSAGES/django.mo b/cotisations/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index b5a65357..00000000 Binary files a/cotisations/locale/fr/LC_MESSAGES/django.mo and /dev/null differ diff --git a/cotisations/locale/fr/LC_MESSAGES/django.po b/cotisations/locale/fr/LC_MESSAGES/django.po index 129d4d72..2c29dc8d 100644 --- a/cotisations/locale/fr/LC_MESSAGES/django.po +++ b/cotisations/locale/fr/LC_MESSAGES/django.po @@ -21,7 +21,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-18 13:17+0200\n" +"POT-Creation-Date: 2019-01-12 16:50+0100\n" "PO-Revision-Date: 2018-03-31 16:09+0002\n" "Last-Translator: Laouen Fernet \n" "Language: fr_FR\n" @@ -33,79 +33,98 @@ msgstr "" 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:274 +#: forms.py:66 forms.py:299 msgid "Select a payment method" msgstr "Sélectionnez un moyen de paiement" -#: forms.py:66 models.py:510 +#: forms.py:69 models.py:579 msgid "Member" msgstr "Adhérent" -#: forms.py:68 +#: forms.py:71 msgid "Select the proprietary member" msgstr "Sélectionnez l'adhérent propriétaire" -#: forms.py:69 +#: forms.py:72 msgid "Validated invoice" msgstr "Facture validée" -#: forms.py:82 +#: forms.py:85 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 +#: forms.py:97 templates/cotisations/aff_article.html:33 +#: templates/cotisations/facture.html:67 msgid "Article" msgstr "Article" -#: forms.py:100 forms.py:124 templates/cotisations/edit_facture.html:46 +#: forms.py:101 templates/cotisations/edit_facture.html:50 msgid "Quantity" msgstr "Quantité" -#: forms.py:154 +#: forms.py:119 +msgid "Discount is on percentage." +msgstr "La réduction est en pourcentage." + +#: forms.py:123 templates/cotisations/facture.html:78 +msgid "Discount" +msgstr "Réduction" + +#: forms.py:140 +#, python-format +msgid "{}% discount" +msgstr "{}% de réduction" + +#: forms.py:140 +msgid "{}€ discount" +msgstr "{}€ de réduction" + +#: forms.py:179 msgid "Article name" msgstr "Nom de l'article" -#: forms.py:164 templates/cotisations/sidebar.html:50 +#: forms.py:189 templates/cotisations/sidebar.html:55 msgid "Available articles" msgstr "Articles disponibles" -#: forms.py:192 +#: forms.py:217 msgid "Payment method name" msgstr "Nom du moyen de paiement" -#: forms.py:204 +#: forms.py:229 msgid "Available payment methods" msgstr "Moyens de paiement disponibles" -#: forms.py:230 +#: forms.py:255 msgid "Bank name" msgstr "Nom de la banque" -#: forms.py:242 +#: forms.py:267 msgid "Available banks" msgstr "Banques disponibles" -#: forms.py:261 +#: forms.py:286 msgid "Amount" msgstr "Montant" -#: forms.py:267 templates/cotisations/aff_cotisations.html:44 +#: forms.py:292 templates/cotisations/aff_cost_estimate.html:42 +#: 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 +#: forms.py:313 #, python-format msgid "" -"Requested amount is too high. Your balance can't exceed " +"Requested amount is too high. Your balance can't exceed " "%(max_online_balance)s €." msgstr "" -"Le montant demandé trop grand. Votre solde ne peut excéder " +"Le montant demandé est trop grand. Votre solde ne peut excéder " "%(max_online_balance)s €." -#: models.py:60 templates/cotisations/aff_cotisations.html:48 +#: models.py:60 templates/cotisations/aff_cost_estimate.html:46 +#: templates/cotisations/aff_cotisations.html:48 #: templates/cotisations/aff_custom_invoice.html:46 #: templates/cotisations/control.html:70 msgid "Date" @@ -133,9 +152,9 @@ msgstr "Peut voir un objet facture" #: models.py:158 msgid "Can edit all the previous invoices" -msgstr "Peut modifier toutes les factures existantes" +msgstr "Peut modifier toutes les factures précédentes" -#: models.py:160 models.py:305 +#: models.py:160 models.py:373 msgid "invoice" msgstr "facture" @@ -156,128 +175,149 @@ 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écedemment contrôlée ou " +"Vous n'avez pas le droit de modifier une facture précédemment contrôlée ou " "invalidée." #: 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:186 +#: models.py:187 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:189 +#: models.py:191 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 contrôlée ou " +"Vous n'avez pas le droit de supprimer une facture précédemment contrôlée ou " "invalidée." -#: models.py:197 +#: models.py:199 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 des factures d'un autre " "utilisateur." -#: models.py:200 +#: models.py:202 msgid "The invoice has been invalidated." msgstr "La facture a été invalidée." -#: models.py:210 +#: models.py:214 msgid "You don't have the right to edit the \"controlled\" state." msgstr "Vous n'avez pas le droit de modifier le statut \"contrôlé\"." -#: models.py:224 +#: models.py:228 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:226 +#: models.py:230 msgid "There are no article that you can buy." msgstr "Il n'y a pas d'article que vous puissiez acheter." -#: models.py:261 +#: models.py:272 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 +#: models.py:276 templates/cotisations/aff_cost_estimate.html:36 +#: templates/cotisations/aff_custom_invoice.html:36 msgid "Recipient" msgstr "Destinataire" -#: models.py:269 templates/cotisations/aff_paiement.html:33 +#: models.py:280 templates/cotisations/aff_paiement.html:33 msgid "Payment type" msgstr "Type de paiement" -#: models.py:273 +#: models.py:284 msgid "Address" msgstr "Adresse" -#: models.py:276 templates/cotisations/aff_custom_invoice.html:54 +#: models.py:287 templates/cotisations/aff_custom_invoice.html:54 msgid "Paid" msgstr "Payé" -#: models.py:296 models.py:516 models.py:764 +#: models.py:291 +msgid "Remark" +msgstr "Remarque" + +#: models.py:300 +msgid "Can view a cost estimate object" +msgstr "Peut voir un objet devis" + +#: models.py:303 +msgid "Period of validity" +msgstr "Période de validité" + +#: models.py:340 +msgid "You don't have the right to delete a cost estimate." +msgstr "Vous n'avez pas le droit de supprimer un devis." + +#: models.py:343 +msgid "The cost estimate has an invoice and can't be deleted." +msgstr "Le devis a une facture et ne peut pas être supprimé." + +#: models.py:364 models.py:585 models.py:852 msgid "Connection" msgstr "Connexion" -#: models.py:297 models.py:517 models.py:765 +#: models.py:365 models.py:586 models.py:853 msgid "Membership" msgstr "Adhésion" -#: models.py:298 models.py:512 models.py:518 models.py:766 +#: models.py:366 models.py:581 models.py:587 models.py:854 msgid "Both of them" msgstr "Les deux" -#: models.py:310 +#: models.py:378 msgid "amount" msgstr "montant" -#: models.py:315 +#: models.py:383 msgid "article" msgstr "article" -#: models.py:322 +#: models.py:390 msgid "price" msgstr "prix" -#: models.py:327 models.py:535 +#: models.py:395 models.py:604 msgid "duration (in months)" msgstr "durée (en mois)" -#: models.py:335 models.py:549 models.py:780 +#: models.py:403 models.py:618 models.py:868 msgid "subscription type" msgstr "type de cotisation" -#: models.py:340 +#: models.py:408 msgid "Can view a purchase object" msgstr "Peut voir un objet achat" -#: models.py:341 +#: models.py:409 msgid "Can edit all the previous purchases" msgstr "Peut modifier tous les achats précédents" -#: models.py:343 models.py:774 +#: models.py:411 models.py:862 msgid "purchase" msgstr "achat" -#: models.py:344 +#: models.py:412 msgid "purchases" msgstr "achats" -#: models.py:411 models.py:573 +#: models.py:479 models.py:642 msgid "Duration must be specified for a subscription." msgstr "La durée de la cotisation doit être indiquée." -#: models.py:418 +#: models.py:486 msgid "You don't have the right to edit the purchases." msgstr "Vous n'avez pas le droit de modifier les achats." -#: models.py:423 +#: models.py:491 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:427 +#: models.py:495 msgid "" "You don't have the right to edit a purchase already controlled or " "invalidated." @@ -285,150 +325,150 @@ msgstr "" "Vous n'avez pas le droit de modifier un achat précédemment contrôlé ou " "invalidé." -#: models.py:434 +#: models.py:502 msgid "You don't have the right to delete a purchase." msgstr "Vous n'avez pas le droit de supprimer un achat." -#: models.py:436 +#: models.py:504 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:439 +#: models.py:507 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 contrôlé ou " +"Vous n'avez pas le droit de supprimer un achat précédemment contrôlé ou " "invalidé." -#: models.py:447 +#: models.py:515 msgid "You don't have the right to view someone else's purchase history." msgstr "" "Vous n'avez pas le droit de voir l'historique des achats d'un autre " "utilisateur." -#: models.py:511 +#: models.py:580 msgid "Club" msgstr "Club" -#: models.py:523 +#: models.py:592 msgid "designation" msgstr "désignation" -#: models.py:529 +#: models.py:598 msgid "unit price" msgstr "prix unitaire" -#: models.py:541 +#: models.py:610 msgid "type of users concerned" msgstr "type d'utilisateurs concernés" -#: models.py:553 models.py:649 +#: models.py:622 models.py:733 msgid "is available for every user" msgstr "est disponible pour chaque utilisateur" -#: models.py:560 +#: models.py:629 msgid "Can view an article object" msgstr "Peut voir un objet article" -#: models.py:561 +#: models.py:630 msgid "Can buy every article" msgstr "Peut acheter chaque article" -#: models.py:569 +#: models.py:638 msgid "Balance is a reserved article name." msgstr "Solde est un nom d'article réservé." -#: models.py:594 +#: models.py:663 msgid "You can't buy this article." msgstr "Vous ne pouvez pas acheter cet article." -#: models.py:624 +#: models.py:708 msgid "Can view a bank object" msgstr "Peut voir un objet banque" -#: models.py:626 +#: models.py:710 msgid "bank" msgstr "banque" -#: models.py:627 +#: models.py:711 msgid "banks" msgstr "banques" -#: models.py:645 +#: models.py:729 msgid "method" msgstr "moyen" -#: models.py:654 +#: models.py:738 msgid "is user balance" msgstr "est solde utilisateur" -#: models.py:655 +#: models.py:739 msgid "There should be only one balance payment method." msgstr "Il ne devrait y avoir qu'un moyen de paiement solde." -#: models.py:661 +#: models.py:745 msgid "Can view a payment method object" msgstr "Peut voir un objet moyen de paiement" -#: models.py:662 +#: models.py:746 msgid "Can use every payment method" msgstr "Peut utiliser chaque moyen de paiement" -#: models.py:664 +#: models.py:748 msgid "payment method" msgstr "moyen de paiement" -#: models.py:665 +#: models.py:749 msgid "payment methods" msgstr "moyens de paiement" -#: models.py:699 payment_methods/comnpay/views.py:63 +#: models.py:787 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 +#: models.py:797 msgid "The invoice was created." msgstr "La facture a été créée." -#: models.py:730 +#: models.py:818 msgid "You can't use this payment method." msgstr "Vous ne pouvez pas utiliser ce moyen de paiement." -#: models.py:748 +#: models.py:836 msgid "No custom payment method." msgstr "Pas de moyen de paiement personnalisé." -#: models.py:783 +#: models.py:871 msgid "start date" msgstr "date de début" -#: models.py:786 +#: models.py:874 msgid "end date" msgstr "date de fin" -#: models.py:791 +#: models.py:879 msgid "Can view a subscription object" msgstr "Peut voir un objet cotisation" -#: models.py:792 +#: models.py:880 msgid "Can edit the previous subscriptions" msgstr "Peut modifier les cotisations précédentes" -#: models.py:794 +#: models.py:882 msgid "subscription" msgstr "cotisation" -#: models.py:795 +#: models.py:883 msgid "subscriptions" msgstr "cotisations" -#: models.py:799 +#: models.py:887 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 +#: models.py:891 msgid "" "You don't have the right to edit a subscription already controlled or " "invalidated." @@ -436,11 +476,11 @@ msgstr "" "Vous n'avez pas le droit de modifier une cotisation précédemment contrôlée " "ou invalidée." -#: models.py:810 +#: models.py:898 msgid "You don't have the right to delete a subscription." msgstr "Vous n'avez pas le droit de supprimer une cotisation." -#: models.py:813 +#: models.py:901 msgid "" "You don't have the right to delete a subscription already controlled or " "invalidated." @@ -448,7 +488,7 @@ msgstr "" "Vous n'avez pas le droit de supprimer une cotisation précédemment contrôlée " "ou invalidée." -#: models.py:821 +#: models.py:909 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 " @@ -482,11 +522,11 @@ msgstr "Le montant maximal d'argent autorisé pour le solde." 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 +#: payment_methods/balance/models.py:79 payment_methods/balance/models.py:110 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 +#: payment_methods/balance/models.py:97 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." @@ -523,11 +563,11 @@ msgstr "" 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 +#: payment_methods/comnpay/models.py:102 msgid "Pay invoice number " msgstr "Payer la facture numéro " -#: payment_methods/comnpay/models.py:116 +#: payment_methods/comnpay/models.py:114 msgid "" "In order to pay your invoice with ComNpay, the price must be greater than {} " "€." @@ -559,6 +599,30 @@ msgstr "" msgid "no" msgstr "non" +#: payment_methods/note_kfet/forms.py:32 +msgid "pseudo note" +msgstr "pseudo note" + +#: payment_methods/note_kfet/forms.py:35 +msgid "Password" +msgstr "Mot de passe" + +#: payment_methods/note_kfet/models.py:40 +msgid "NoteKfet" +msgstr "NoteKfet" + +#: payment_methods/note_kfet/models.py:50 +msgid "server" +msgstr "serveur" + +#: payment_methods/note_kfet/views.py:60 +msgid "Unknown error." +msgstr "Erreur inconnue." + +#: payment_methods/note_kfet/views.py:88 +msgid "The payment with note was done." +msgstr "Le paiement par note a été effectué." + #: templates/cotisations/aff_article.html:34 msgid "Price" msgstr "Prix" @@ -579,34 +643,47 @@ msgstr "Utilisateurs concernés" 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_banque.html:32 msgid "Bank" msgstr "Banque" -#: templates/cotisations/aff_cotisations.html:38 -msgid "User" -msgstr "Utilisateur" - +#: templates/cotisations/aff_cost_estimate.html:39 #: templates/cotisations/aff_cotisations.html:41 #: templates/cotisations/aff_custom_invoice.html:39 #: templates/cotisations/control.html:63 -#: templates/cotisations/edit_facture.html:45 +#: templates/cotisations/edit_facture.html:49 msgid "Designation" msgstr "Désignation" +#: templates/cotisations/aff_cost_estimate.html:40 #: 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_cost_estimate.html:50 +msgid "Validity" +msgstr "Validité" + +#: templates/cotisations/aff_cost_estimate.html:54 +msgid "Cost estimate ID" +msgstr "ID devis" + +#: templates/cotisations/aff_cost_estimate.html:58 +msgid "Invoice created" +msgstr "Facture créée" + +#: templates/cotisations/aff_cost_estimate.html:91 +#: templates/cotisations/aff_cotisations.html:81 +#: templates/cotisations/aff_custom_invoice.html:79 +msgid "PDF" +msgstr "PDF" + +#: templates/cotisations/aff_cotisations.html:38 +msgid "User" +msgstr "Utilisateur" + #: templates/cotisations/aff_cotisations.html:52 #: templates/cotisations/aff_custom_invoice.html:50 #: templates/cotisations/control.html:56 @@ -617,11 +694,6 @@ msgstr "ID facture" msgid "Controlled invoice" msgstr "Facture contrôlée" -#: templates/cotisations/aff_cotisations.html:81 -#: templates/cotisations/aff_custom_invoice.html:79 -msgid "PDF" -msgstr "PDF" - #: templates/cotisations/aff_cotisations.html:84 msgid "Invalidated invoice" msgstr "Facture invalidée" @@ -666,6 +738,11 @@ msgstr "Validé" msgid "Controlled" msgstr "Contrôlé" +#: templates/cotisations/control.html:107 views.py:642 views.py:729 +#: views.py:809 +msgid "Edit" +msgstr "Modifier" + #: templates/cotisations/delete.html:29 msgid "Deletion of subscriptions" msgstr "Suppression de cotisations" @@ -676,11 +753,12 @@ msgid "" "Warning: are you sure you really want to delete this %(object_name)s object " "( %(objet)s )?" msgstr "" -"\tAttention: voulez-vous vraiment supprimer cet objet %(object_name)s " +"Attention: voulez-vous vraiment supprimer cet objet %(object_name)s " "( %(objet)s ) ?" #: templates/cotisations/delete.html:38 -#: templates/cotisations/edit_facture.html:60 +#: templates/cotisations/edit_facture.html:64 views.py:178 views.py:228 +#: views.py:280 msgid "Confirm" msgstr "Confirmer" @@ -689,18 +767,19 @@ msgstr "Confirmer" msgid "Creation and editing of invoices" msgstr "Création et modification de factures" -#: templates/cotisations/edit_facture.html:38 -msgid "Edit the invoice" +#: templates/cotisations/edit_facture.html:41 +msgid "Edit invoice" msgstr "Modifier la facture" -#: templates/cotisations/edit_facture.html:41 -#: templates/cotisations/facture.html:56 -msgid "Invoice's articles" -msgstr "Articles de la facture" +#: templates/cotisations/edit_facture.html:45 +#: templates/cotisations/facture.html:62 +#: templates/cotisations/index_article.html:30 +msgid "Articles" +msgstr "Articles" #: templates/cotisations/facture.html:37 -msgid "New invoice" -msgstr "Nouvelle facture" +msgid "Buy" +msgstr "Acheter" #: templates/cotisations/facture.html:40 #, python-format @@ -712,11 +791,11 @@ msgstr "Solde maximum autorisé : %(max_balance)s €" 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:76 +msgid "Add an extra article" +msgstr "Ajouter un article supplémentaire" -#: templates/cotisations/facture.html:72 +#: templates/cotisations/facture.html:82 msgid "Total price: 0,00 €" msgstr "Prix total : 0,00 €" @@ -728,12 +807,8 @@ msgstr "Factures" msgid "Subscriptions" msgstr "Cotisations" -#: templates/cotisations/index_article.html:30 -msgid "Articles" -msgstr "Articles" - #: templates/cotisations/index_article.html:33 -msgid "Article types list" +msgid "List of article types" msgstr "Liste des types d'article" #: templates/cotisations/index_article.html:36 @@ -745,12 +820,12 @@ 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:55 +#: templates/cotisations/sidebar.html:60 msgid "Banks" msgstr "Banques" #: templates/cotisations/index_banque.html:33 -msgid "Banks list" +msgid "List of banks" msgstr "Liste des banques" #: templates/cotisations/index_banque.html:36 @@ -761,17 +836,26 @@ msgstr "Ajouter une banque" msgid "Delete one or several banks" msgstr "Supprimer une ou plusieurs banques" +#: templates/cotisations/index_cost_estimate.html:28 +#: templates/cotisations/sidebar.html:50 +msgid "Cost estimates" +msgstr "Devis" + +#: templates/cotisations/index_cost_estimate.html:31 +msgid "List of cost estimates" +msgstr "Liste des devis" + #: 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" +msgid "List of custom invoices" +msgstr "Liste des factures personnalisées" #: templates/cotisations/index_paiement.html:30 -#: templates/cotisations/sidebar.html:60 +#: templates/cotisations/sidebar.html:65 msgid "Payment methods" msgstr "Moyens de paiement" @@ -794,9 +878,9 @@ msgstr "Rechargement de solde" #: templates/cotisations/payment.html:34 #, python-format msgid "Pay %(amount)s €" -msgstr "Recharger de %(amount)s €" +msgstr "Payer %(amount)s €" -#: templates/cotisations/payment.html:42 views.py:870 +#: templates/cotisations/payment.html:42 views.py:1049 msgid "Pay" msgstr "Payer" @@ -808,84 +892,104 @@ msgstr "Créer une facture" msgid "Control the invoices" msgstr "Contrôler les factures" -#: views.py:167 +#: views.py:164 msgid "You need to choose at least one article." msgstr "Vous devez choisir au moins un article." -#: views.py:181 views.py:235 -msgid "Create" -msgstr "Créer" +#: views.py:222 +msgid "The cost estimate was created." +msgstr "Le devis a été créé." -#: views.py:228 +#: views.py:232 views.py:534 +msgid "Cost estimate" +msgstr "Devis" + +#: views.py:274 msgid "The custom invoice was created." msgstr "La facture personnalisée a été créée." -#: views.py:316 views.py:370 +#: views.py:363 views.py:466 msgid "The invoice was edited." msgstr "La facture a été modifiée." -#: views.py:336 views.py:430 +#: views.py:383 views.py:589 msgid "The invoice was deleted." msgstr "La facture a été supprimée." -#: views.py:341 views.py:435 +#: views.py:388 views.py:594 msgid "Invoice" msgstr "Facture" -#: views.py:456 +#: views.py:417 +msgid "The cost estimate was edited." +msgstr "Le devis a été modifié." + +#: views.py:424 +msgid "Edit cost estimate" +msgstr "Modifier le devis" + +#: views.py:436 +msgid "An invoice was successfully created from your cost estimate." +msgstr "Une facture a bien été créée à partir de votre devis." + +#: views.py:529 +msgid "The cost estimate was deleted." +msgstr "Le devis a été supprimé." + +#: views.py:615 msgid "The article was created." msgstr "L'article a été créé." -#: views.py:461 views.py:534 views.py:627 +#: views.py:620 views.py:693 views.py:786 msgid "Add" msgstr "Ajouter" -#: views.py:462 +#: views.py:621 msgid "New article" msgstr "Nouvel article" -#: views.py:478 +#: views.py:637 msgid "The article was edited." msgstr "L'article a été modifié." -#: views.py:484 +#: views.py:643 msgid "Edit article" msgstr "Modifier l'article" -#: views.py:500 +#: views.py:659 msgid "The articles were deleted." msgstr "Les articles ont été supprimés." -#: views.py:505 views.py:605 views.py:685 +#: views.py:664 views.py:764 views.py:844 msgid "Delete" msgstr "Supprimer" -#: views.py:506 +#: views.py:665 msgid "Delete article" msgstr "Supprimer l'article" -#: views.py:528 +#: views.py:687 msgid "The payment method was created." msgstr "Le moyen de paiment a été créé." -#: views.py:535 +#: views.py:694 msgid "New payment method" msgstr "Nouveau moyen de paiement" -#: views.py:564 +#: views.py:723 msgid "The payment method was edited." msgstr "Le moyen de paiment a été modifié." -#: views.py:571 +#: views.py:730 msgid "Edit payment method" msgstr "Modifier le moyen de paiement" -#: views.py:590 +#: views.py:749 #, python-format msgid "The payment method %(method_name)s was deleted." msgstr "Le moyen de paiement %(method_name)s a été supprimé." -#: views.py:597 +#: views.py:756 #, python-format msgid "" "The payment method %(method_name)s can't be deleted " @@ -894,52 +998,51 @@ msgstr "" "Le moyen de paiement %(method_name)s ne peut pas être supprimé car il y a " "des factures qui l'utilisent." -#: views.py:606 +#: views.py:765 msgid "Delete payment method" msgstr "Supprimer le moyen de paiement" -#: views.py:622 +#: views.py:781 msgid "The bank was created." msgstr "La banque a été créée." -#: views.py:628 +#: views.py:787 msgid "New bank" msgstr "Nouvelle banque" -#: views.py:645 +#: views.py:804 msgid "The bank was edited." msgstr "La banque a été modifiée." -#: views.py:651 +#: views.py:810 msgid "Edit bank" msgstr "Modifier la banque" -#: views.py:670 +#: views.py:829 #, python-format msgid "The bank %(bank_name)s was deleted." msgstr "La banque %(bank_name)s a été supprimée." -#: views.py:677 +#: views.py:836 #, python-format msgid "" -"The bank %(bank_name)s can't be deleted because there " -"are invoices using it." +"The bank %(bank_name)s can't be deleted because there are invoices using it." msgstr "" "La banque %(bank_name)s ne peut pas être supprimée car il y a des factures " "qui l'utilisent." -#: views.py:686 +#: views.py:845 msgid "Delete bank" msgstr "Supprimer la banque" -#: views.py:722 +#: views.py:881 msgid "Your changes have been properly taken into account." msgstr "Vos modifications ont correctement été prises en compte." -#: views.py:834 +#: views.py:1016 msgid "You are not allowed to credit your balance." msgstr "Vous n'êtes pas autorisés à créditer votre solde." -#: views.py:869 +#: views.py:1048 msgid "Refill your balance" msgstr "Recharger votre solde" diff --git a/cotisations/migrations/0032_custom_invoice.py b/cotisations/migrations/0032_custom_invoice.py index 68963bf5..39c23e8b 100644 --- a/cotisations/migrations/0032_custom_invoice.py +++ b/cotisations/migrations/0032_custom_invoice.py @@ -30,7 +30,9 @@ def update_rights(apps, schema_editor): create_permissions(app) app.models_module = False - former = Permission.objects.get(codename='change_facture_pdf') + ContentType = apps.get_model("contenttypes", "ContentType") + content_type = ContentType.objects.get_for_model(Permission) + former, created = Permission.objects.get_or_create(codename='change_facture_pdf', content_type=content_type) new_1 = Permission.objects.get(codename='add_custominvoice') new_2 = Permission.objects.get(codename='change_custominvoice') new_3 = Permission.objects.get(codename='view_custominvoice') diff --git a/cotisations/migrations/0034_auto_20180831_1532.py b/cotisations/migrations/0034_auto_20180831_1532.py new file mode 100644 index 00000000..ea698374 --- /dev/null +++ b/cotisations/migrations/0034_auto_20180831_1532.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-08-31 13:32 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0033_auto_20180818_1319'), + ] + + operations = [ + migrations.AlterField( + model_name='facture', + name='valid', + field=models.BooleanField(default=False, verbose_name='validated'), + ), + ] diff --git a/cotisations/migrations/0035_notepayment.py b/cotisations/migrations/0035_notepayment.py new file mode 100644 index 00000000..1d8bcd48 --- /dev/null +++ b/cotisations/migrations/0035_notepayment.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-09-01 11:27 +from __future__ import unicode_literals + +import cotisations.payment_methods.mixins +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0034_auto_20180831_1532'), + ] + + operations = [ + migrations.CreateModel( + name='NotePayment', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('server', models.CharField(max_length=255, verbose_name='server')), + ('port', models.PositiveIntegerField(blank=True, null=True)), + ('id_note', models.PositiveIntegerField(blank=True, null=True)), + ('payment', models.OneToOneField(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='payment_method', to='cotisations.Paiement')), + ], + options={ + 'verbose_name': 'NoteKfet', + }, + bases=(cotisations.payment_methods.mixins.PaymentMethodMixin, models.Model), + ), + ] diff --git a/cotisations/migrations/0036_custominvoice_remark.py b/cotisations/migrations/0036_custominvoice_remark.py new file mode 100644 index 00000000..7719b31d --- /dev/null +++ b/cotisations/migrations/0036_custominvoice_remark.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-29 14:22 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0035_notepayment'), + ] + + operations = [ + migrations.AddField( + model_name='custominvoice', + name='remark', + field=models.TextField(blank=True, null=True, verbose_name='Remark'), + ), + ] diff --git a/cotisations/migrations/0037_costestimate.py b/cotisations/migrations/0037_costestimate.py new file mode 100644 index 00000000..3d97f3f3 --- /dev/null +++ b/cotisations/migrations/0037_costestimate.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-29 21:03 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0036_custominvoice_remark'), + ] + + operations = [ + migrations.CreateModel( + name='CostEstimate', + fields=[ + ('custominvoice_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='cotisations.CustomInvoice')), + ('validity', models.DurationField(verbose_name='Period of validity')), + ('final_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='origin_cost_estimate', to='cotisations.CustomInvoice')), + ], + options={ + 'permissions': (('view_costestimate', 'Can view a cost estimate object'),), + }, + bases=('cotisations.custominvoice',), + ), + ] diff --git a/cotisations/migrations/0038_auto_20181231_1657.py b/cotisations/migrations/0038_auto_20181231_1657.py new file mode 100644 index 00000000..a9415bf0 --- /dev/null +++ b/cotisations/migrations/0038_auto_20181231_1657.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-31 22:57 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('cotisations', '0037_costestimate'), + ] + + operations = [ + migrations.AlterField( + model_name='costestimate', + name='final_invoice', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='origin_cost_estimate', to='cotisations.CustomInvoice'), + ), + migrations.AlterField( + model_name='costestimate', + name='validity', + field=models.DurationField(help_text='DD HH:MM:SS', verbose_name='Period of validity'), + ), + migrations.AlterField( + model_name='custominvoice', + name='paid', + field=models.BooleanField(default=False, verbose_name='Paid'), + ), + ] diff --git a/cotisations/models.py b/cotisations/models.py index fe89aa5d..c6b7cd1c 100644 --- a/cotisations/models.py +++ b/cotisations/models.py @@ -46,11 +46,14 @@ from django.urls import reverse from django.shortcuts import redirect from django.contrib import messages +from preferences.models import CotisationsOption from machines.models import regen from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin -from cotisations.utils import find_payment_method +from cotisations.utils import ( + find_payment_method, send_mail_invoice, send_mail_voucher +) from cotisations.validators import check_no_balance @@ -83,7 +86,7 @@ class BaseInvoice(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): ).aggregate( total=models.Sum( models.F('prix')*models.F('number'), - output_field=models.FloatField() + output_field=models.DecimalField() ) )['total'] or 0 @@ -137,7 +140,7 @@ class Facture(BaseInvoice): ) # TODO : change name to validity for clarity valid = models.BooleanField( - default=True, + default=False, verbose_name=_("validated") ) # TODO : changed name to controlled for clarity @@ -182,22 +185,26 @@ class Facture(BaseInvoice): def can_delete(self, user_request, *args, **kwargs): if not user_request.has_perm('cotisations.delete_facture'): return False, _("You don't have the right to delete an invoice.") - if not self.user.can_edit(user_request, *args, **kwargs)[0]: + elif not user_request.has_perm('cotisations.change_all_facture') and \ + not self.user.can_edit(user_request, *args, **kwargs)[0]: return False, _("You don't have the right to delete this user's " "invoices.") - if self.control or not self.valid: + elif not user_request.has_perm('cotisations.change_all_facture') and \ + (self.control or not self.valid): return False, _("You don't have the right to delete an invoice " "already controlled or invalidated.") else: return True, None 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 view someone else's " - "invoices history.") - elif not self.valid: - return False, _("The invoice has been invalidated.") + if not user_request.has_perm('cotisations.view_facture'): + if self.user != user_request: + 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.") + else: + return True, None else: return True, None @@ -231,6 +238,31 @@ class Facture(BaseInvoice): self.field_permissions = { 'control': self.can_change_control, } + self.__original_valid = self.valid + self.__original_control = self.control + + def get_subscription(self): + """Returns every subscription associated with this invoice.""" + return Cotisation.objects.filter( + vente__in=self.vente_set.filter( + Q(type_cotisation='All') | + Q(type_cotisation='Adhesion') + ) + ) + + def is_subscription(self): + """Returns True if this invoice contains at least one subscribtion.""" + return bool(self.get_subscription()) + + def save(self, *args, **kwargs): + super(Facture, self).save(*args, **kwargs) + if not self.__original_valid and self.valid: + send_mail_invoice(self) + if self.is_subscription() \ + and not self.__original_control \ + and self.control \ + and CotisationsOption.get_cached_value('send_voucher_mail'): + send_mail_voucher(self) def __str__(self): return str(self.user) + ' ' + str(self.date) @@ -242,8 +274,10 @@ def facture_post_save(**kwargs): Synchronise the LDAP user after an invoice has been saved. """ facture = kwargs['instance'] - user = facture.user - user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + if facture.valid: + user = facture.user + user.set_active() + user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @receiver(post_delete, sender=Facture) @@ -273,8 +307,65 @@ class CustomInvoice(BaseInvoice): verbose_name=_("Address") ) paid = models.BooleanField( - verbose_name=_("Paid") + verbose_name=_("Paid"), + default=False ) + remark = models.TextField( + verbose_name=_("Remark"), + blank=True, + null=True + ) + + +class CostEstimate(CustomInvoice): + class Meta: + permissions = ( + ('view_costestimate', _("Can view a cost estimate object")), + ) + validity = models.DurationField( + verbose_name=_("Period of validity"), + help_text="DD HH:MM:SS" + ) + final_invoice = models.ForeignKey( + CustomInvoice, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="origin_cost_estimate", + primary_key=False + ) + + def create_invoice(self): + """Create a CustomInvoice from the CostEstimate.""" + if self.final_invoice is not None: + return self.final_invoice + invoice = CustomInvoice() + invoice.recipient = self.recipient + invoice.payment = self.payment + invoice.address = self.address + invoice.paid = False + invoice.remark = self.remark + invoice.date = timezone.now() + invoice.save() + self.final_invoice = invoice + self.save() + for sale in self.vente_set.all(): + Vente.objects.create( + facture=invoice, + name=sale.name, + prix=sale.prix, + number=sale.number, + ) + return invoice + + def can_delete(self, user_request, *args, **kwargs): + if not user_request.has_perm('cotisations.delete_costestimate'): + return False, _("You don't have the right " + "to delete a cost estimate.") + if self.final_invoice is not None: + return False, _("The cost estimate has an " + "invoice and can't be deleted.") + return True, None # TODO : change Vente to Purchase @@ -471,8 +562,9 @@ def vente_post_save(**kwargs): if purchase.type_cotisation: purchase.create_cotis() purchase.cotisation.save() - user = purchase.facture.user - user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) + user = purchase.facture.facture.user + user.set_active() + user.ldap_sync(base=True, access_refresh=True, mac_refresh=False) # TODO : change vente to purchase @@ -602,7 +694,9 @@ class Article(RevMixin, AclMixin, models.Model): user: The user requesting articles. target_user: The user to sell articles """ - if target_user.is_class_club: + if target_user is None: + objects_pool = cls.objects.filter(Q(type_user='All')) + elif target_user.is_class_club: objects_pool = cls.objects.filter( Q(type_user='All') | Q(type_user='Club') ) @@ -610,6 +704,10 @@ class Article(RevMixin, AclMixin, models.Model): objects_pool = cls.objects.filter( Q(type_user='All') | Q(type_user='Adherent') ) + if target_user is not None and not target_user.is_adherent(): + objects_pool = objects_pool.filter( + Q(type_cotisation='All') | Q(type_cotisation='Adhesion') + ) if user.has_perm('cotisations.buy_every_article'): return objects_pool return objects_pool.filter(available_for_everyone=True) @@ -700,6 +798,10 @@ class Paiement(RevMixin, AclMixin, models.Model): if payment_method is not None and use_payment_method: return payment_method.end_payment(invoice, request) + # So make this invoice valid, trigger send mail + invoice.valid = True + invoice.save() + # In case a cotisation was bought, inform the user, the # cotisation time has been extended too if any(sell.type_cotisation for sell in invoice.vente_set.all()): @@ -856,4 +958,3 @@ def cotisation_post_delete(**_kwargs): """ regen('mac_ip_list') regen('mailing') - diff --git a/cotisations/payment_methods/__init__.py b/cotisations/payment_methods/__init__.py index b78a86fd..1efde30b 100644 --- a/cotisations/payment_methods/__init__.py +++ b/cotisations/payment_methods/__init__.py @@ -127,10 +127,11 @@ method to your model, where `form` is an instance of """ -from . import comnpay, cheque, balance, urls +from . import comnpay, cheque, balance, note_kfet, urls PAYMENT_METHODS = [ comnpay, cheque, balance, + note_kfet ] diff --git a/cotisations/payment_methods/balance/models.py b/cotisations/payment_methods/balance/models.py index 250e6949..221cca3e 100644 --- a/cotisations/payment_methods/balance/models.py +++ b/cotisations/payment_methods/balance/models.py @@ -73,9 +73,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): """ user = invoice.user total_price = invoice.prix_total() - if float(user.solde) - float(total_price) < self.minimum_balance: - invoice.valid = False - invoice.save() + if user.solde - total_price < self.minimum_balance: messages.error( request, _("Your balance is too low for this operation.") @@ -108,7 +106,7 @@ class BalancePayment(PaymentMethodMixin, models.Model): balance. """ return ( - float(user.solde) - float(price) >= self.minimum_balance, + user.solde - price >= self.minimum_balance, _("Your balance is too low for this operation.") ) diff --git a/cotisations/payment_methods/cheque/models.py b/cotisations/payment_methods/cheque/models.py index cd6d2920..8f00ff46 100644 --- a/cotisations/payment_methods/cheque/models.py +++ b/cotisations/payment_methods/cheque/models.py @@ -46,8 +46,6 @@ class ChequePayment(PaymentMethodMixin, models.Model): """Invalidates the invoice then redirect the user towards a view asking for informations to add to the invoice before validating it. """ - invoice.valid = False - invoice.save() return redirect(reverse( 'cotisations:cheque:validate', kwargs={'invoice_pk': invoice.pk} diff --git a/cotisations/payment_methods/comnpay/models.py b/cotisations/payment_methods/comnpay/models.py index af389cf8..7fac089a 100644 --- a/cotisations/payment_methods/comnpay/models.py +++ b/cotisations/payment_methods/comnpay/models.py @@ -81,8 +81,6 @@ class ComnpayPayment(PaymentMethodMixin, models.Model): a facture id, the price and the secret transaction data stored in the preferences. """ - invoice.valid = False - invoice.save() host = request.get_host() p = Transaction( str(self.payment_credential), diff --git a/cotisations/payment_methods/comnpay/views.py b/cotisations/payment_methods/comnpay/views.py index 2383f1e9..12a5747b 100644 --- a/cotisations/payment_methods/comnpay/views.py +++ b/cotisations/payment_methods/comnpay/views.py @@ -62,13 +62,13 @@ def accept_payment(request, factureid): request, _("The subscription of %(member_name)s was extended to" " %(end_date)s.") % { - 'member_name': request.user.pseudo, - 'end_date': request.user.end_adhesion() + 'member_name': invoice.user.pseudo, + 'end_date': invoice.user.end_adhesion() } ) return redirect(reverse( 'users:profil', - kwargs={'userid': request.user.id} + kwargs={'userid': invoice.user.id} )) diff --git a/cotisations/payment_methods/note_kfet/__init__.py b/cotisations/payment_methods/note_kfet/__init__.py new file mode 100644 index 00000000..1f133d11 --- /dev/null +++ b/cotisations/payment_methods/note_kfet/__init__.py @@ -0,0 +1,26 @@ +# -*- 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. +# +# Copyright © 2018 Gabriel Detraz, Pierre-Antoine Comby +# +# 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. +""" +This module contains a method to pay online using comnpay. +""" +from . import models, urls +NAME = "NOTE" +PaymentMethod = models.NotePayment diff --git a/cotisations/payment_methods/note_kfet/forms.py b/cotisations/payment_methods/note_kfet/forms.py new file mode 100644 index 00000000..e52c275c --- /dev/null +++ b/cotisations/payment_methods/note_kfet/forms.py @@ -0,0 +1,38 @@ +# -*- 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. +# +# Copyright © 2018 Pierre-Antoine Comby +# Copyright © 2018 Gabriel Detraz +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django import forms +from django.utils.translation import ugettext_lazy as _ + +from cotisations.utils import find_payment_method + +class NoteCredentialForm(forms.Form): + """A special form to get credential to connect to a NoteKfet2015 server throught his API + object. + """ + login = forms.CharField( + label=_("pseudo note") + ) + password = forms.CharField( + label=_("Password"), + widget=forms.PasswordInput + ) + diff --git a/cotisations/payment_methods/note_kfet/models.py b/cotisations/payment_methods/note_kfet/models.py new file mode 100644 index 00000000..be54bd54 --- /dev/null +++ b/cotisations/payment_methods/note_kfet/models.py @@ -0,0 +1,65 @@ +# -*- 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. +# +# Copyright © 2018 Pierre-Antoine Comby +# Copyright © 2018 Gabriel Detraz +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.db import models +from django.shortcuts import render +from django.urls import reverse +from django.utils.translation import ugettext_lazy as _ +from django.contrib import messages + +from cotisations.models import Paiement +from cotisations.payment_methods.mixins import PaymentMethodMixin + +from django.shortcuts import render, redirect + + +class NotePayment(PaymentMethodMixin, models.Model): + """ + The model allowing you to pay with NoteKfet2015. + """ + + class Meta: + verbose_name = _("NoteKfet") + + payment = models.OneToOneField( + Paiement, + on_delete = models.CASCADE, + related_name = 'payment_method', + editable = False + ) + server = models.CharField( + max_length=255, + verbose_name=_("server") + ) + port = models.PositiveIntegerField( + blank = True, + null = True + ) + id_note = models.PositiveIntegerField( + blank = True, + null = True + ) + + def end_payment(self, invoice, request): + return redirect(reverse( + 'cotisations:note_kfet:note_payment', + kwargs={'factureid': invoice.id} + )) diff --git a/cotisations/payment_methods/note_kfet/note.py b/cotisations/payment_methods/note_kfet/note.py new file mode 100755 index 00000000..8b7614b0 --- /dev/null +++ b/cotisations/payment_methods/note_kfet/note.py @@ -0,0 +1,74 @@ +#!/usr/bin/python3 +# -*- coding:utf-8 -*- + +# Codé par PAC , forké de 20-100 + +""" Module pour dialoguer avec la NoteKfet2015 """ + +import socket +import json +import ssl +import traceback + + +def get_response(socket): + length_str = b'' + char = socket.recv(1) + while char != b'\n': + length_str += char + char = socket.recv(1) + total = int(length_str) + return json.loads(socket.recv(total).decode('utf-8')) + +def connect(server, port): + sock = socket.socket() + try: + # On établit la connexion sur port 4242 + sock.connect((server, port)) + # On passe en SSL + sock = ssl.wrap_socket(sock) + # On fait un hello + sock.send(b'["hello", "manual"]') + retcode = get_response(sock) + except: + # Si on a foiré quelque part, c'est que le serveur est down + return (False, sock, "Serveur indisponible") + return (True, sock, "") + +def login(server, port, username, password, masque = [[], [], True]): + result, sock, err = connect(server, port) + if not result: + return (False, None, err) + try: + commande = ["login", [username, password, "bdd", masque]] + sock.send(json.dumps(commande).encode("utf-8")) + response = get_response(sock) + retcode = response['retcode'] + if retcode == 0: + return (True, sock, "") + elif retcode == 5: + return (False, sock, "Login incorrect") + else: + return (False, sock, "Erreur inconnue " + str(retcode)) + except: + # Si on a foiré quelque part, c'est que le serveur est down + return (False, sock, "Erreur de communication avec le serveur") + + +def don(sock, montant, id_note, facture): + """ + Faire faire un don à l'id_note + """ + try: + sock.send(json.dumps(["dons", [[id_note], round(montant*100), "Facture : id=%s, designation=%s" % (facture.id, facture.name())]]).encode("utf-8")) + response = get_response(sock) + retcode = response['retcode'] + transaction_retcode = response["msg"][0][0] + if 0 < retcode < 100 or 200 <= retcode or 0 < transaction_retcode < 100 or 200 <= transaction_retcode: + return (False, "Transaction échouée. (Solde trop négatif ?)") + elif retcode == 0: + return (True, "") + else: + return (False, "Erreur inconnue " + str(retcode)) + except: + return (False, "Erreur de communication avec le serveur") diff --git a/cotisations/payment_methods/note_kfet/urls.py b/cotisations/payment_methods/note_kfet/urls.py new file mode 100644 index 00000000..7b939136 --- /dev/null +++ b/cotisations/payment_methods/note_kfet/urls.py @@ -0,0 +1,30 @@ +# -*- 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. +# +# Copyright © 2018 Gabriel Detraz +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +from django.conf.urls import url +from . import views + +urlpatterns = [ + url( + r'^note_payment/(?P[0-9]+)$', + views.note_payment, + name='note_payment' + ), +] diff --git a/cotisations/payment_methods/note_kfet/views.py b/cotisations/payment_methods/note_kfet/views.py new file mode 100644 index 00000000..cfdda9b0 --- /dev/null +++ b/cotisations/payment_methods/note_kfet/views.py @@ -0,0 +1,97 @@ +# -*- 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. +# +# Copyright © 2018 Gabriel Detraz +# Copyright © 2018 Pierre-Antoine Comby +# +# 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. +"""Payment + +Here are the views needed by comnpay +""" + +from collections import OrderedDict + +from django.urls import reverse +from django.shortcuts import redirect, get_object_or_404 +from django.contrib.auth.decorators import login_required +from django.contrib import messages +from django.views.decorators.csrf import csrf_exempt +from django.utils.datastructures import MultiValueDictKeyError +from django.utils.translation import ugettext as _ +from django.http import HttpResponse, HttpResponseBadRequest + +from cotisations.models import Facture +from cotisations.utils import find_payment_method +from .models import NotePayment +from re2o.views import form +from re2o.acl import ( + can_create, + can_edit +) +from .note import login, don +from .forms import NoteCredentialForm + +@login_required +@can_edit(Facture) +def note_payment(request, facture, factureid): + """ + Build a request to start the negociation with NoteKfet by using + a facture id, the price and the login/password data stored in + the preferences. + """ + user = facture.user + payment_method = find_payment_method(facture.paiement) + if not payment_method or not isinstance(payment_method, NotePayment): + messages.error(request, _("Unknown error.")) + return redirect(reverse( + 'users:profil', + kwargs={'userid': user.id} + )) + noteform = NoteCredentialForm(request.POST or None) + if noteform.is_valid(): + pseudo = noteform.cleaned_data['login'] + password = noteform.cleaned_data['password'] + result, sock, err = login(payment_method.server, payment_method.port, pseudo, password) + if not result: + messages.error(request, err) + return form( + {'form': noteform, 'amount': facture.prix_total()}, + "cotisations/payment.html", + request + ) + else: + result, err = don(sock, facture.prix_total(), payment_method.id_note, facture) + if not result: + messages.error(request, err) + return form( + {'form': noteform, 'amount': facture.prix_total()}, + "cotisations/payment.html", + request + ) + facture.valid = True + facture.save() + messages.success(request, _("The payment with note was done.")) + return redirect(reverse( + 'users:profil', + kwargs={'userid': user.id} + )) + return form( + {'form': noteform, 'amount': facture.prix_total()}, + "cotisations/payment.html", + request + ) diff --git a/cotisations/payment_methods/urls.py b/cotisations/payment_methods/urls.py index 20e50255..f6bb31dd 100644 --- a/cotisations/payment_methods/urls.py +++ b/cotisations/payment_methods/urls.py @@ -19,9 +19,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. from django.conf.urls import include, url -from . import comnpay, cheque +from . import comnpay, cheque, note_kfet urlpatterns = [ url(r'^comnpay/', include(comnpay.urls, namespace='comnpay')), url(r'^cheque/', include(cheque.urls, namespace='cheque')), + url(r'^note_kfet/', include(note_kfet.urls, namespace='note_kfet')), ] diff --git a/cotisations/templates/cotisations/aff_article.html b/cotisations/templates/cotisations/aff_article.html index b07035da..682d6a05 100644 --- a/cotisations/templates/cotisations/aff_article.html +++ b/cotisations/templates/cotisations/aff_article.html @@ -49,9 +49,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ article.available_for_everyone | tick }} {% can_edit article %} - - - + {% include 'buttons/edit.html' with href='cotisations:edit-article' id=article.id %} {% acl_end %} {% history_button article %} diff --git a/cotisations/templates/cotisations/aff_cost_estimate.html b/cotisations/templates/cotisations/aff_cost_estimate.html new file mode 100644 index 00000000..eb040dce --- /dev/null +++ b/cotisations/templates/cotisations/aff_cost_estimate.html @@ -0,0 +1,101 @@ +{% 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 cost_estimate_list.paginator %} + {% include 'pagination.html' with list=cost_estimate_list%} + {% endif %} + + + + + + + + + + + + + + + + + {% for estimate in cost_estimate_list %} + + + + + + + + + + + + {% endfor %} +
+ {% trans "Recipient" as tr_recip %} + {% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_recip %} + {% 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 "Validity" as tr_validity %} + {% include 'buttons/sort.html' with prefix='invoice' col='validity' text=tr_validity %} + + {% trans "Cost estimate ID" as tr_estimate_id %} + {% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_estimate_id %} + + {% trans "Invoice created" as tr_invoice_created%} + {% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_created %} +
{{ estimate.recipient }}{{ estimate.name }}{{ estimate.prix_total }}{{ estimate.payment }}{{ estimate.date }}{{ estimate.validity }}{{ estimate.id }} + {% if estimate.final_invoice %} + + {% else %} + + {% endif %} + + {% can_edit estimate %} + {% include 'buttons/edit.html' with href='cotisations:edit-cost-estimate' id=estimate.id %} + {% acl_end %} + {% history_button estimate %} + {% include 'buttons/suppr.html' with href='cotisations:del-cost-estimate' id=estimate.id %} + + + + + {% trans "PDF" %} + +
+ + {% if custom_invoice_list.paginator %} + {% include 'pagination.html' with list=custom_invoice_list %} + {% endif %} +
diff --git a/cotisations/templates/cotisations/aff_cotisations.html b/cotisations/templates/cotisations/aff_cotisations.html index 30de85dc..e27ae8c7 100644 --- a/cotisations/templates/cotisations/aff_cotisations.html +++ b/cotisations/templates/cotisations/aff_cotisations.html @@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if facture_list.paginator %} -{% include 'pagination.html' with list=facture_list %} + {% include 'pagination.html' with list=facture_list %} {% endif %} @@ -48,7 +48,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Date" as tr_date %} {% include 'buttons/sort.html' with prefix='cotis' col='date' text=tr_date %} - @@ -63,33 +63,38 @@ with this program; if not, write to the Free Software Foundation, Inc., - + {% endfor %}
+ {% trans "Invoice ID" as tr_invoice_id %} {% include 'buttons/sort.html' with prefix='cotis' col='id' text=tr_invoice_id %} {{ facture.prix_total }} {{ facture.paiement }} {{ facture.date }}{{ facture.id }}{{ facture.id }} {% can_edit facture %} - {% include 'buttons/edit.html' with href='cotisations:edit-facture' id=facture.id %} + {% 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 %} + {% include 'buttons/suppr.html' with href='cotisations:del-facture' id=facture.id %} {% acl_end %} - {% history_button facture %} + {% history_button facture %} {% if facture.valid %} - {% trans "PDF" %} + {% trans "PDF" %} {% else %} {% trans "Invalidated invoice" %} {% endif %} + {% if facture.control and facture.is_subscription %} + + {% trans "Voucher" %} + + {% endif %}
-{% if facture_list.paginator %} -{% include 'pagination.html' with list=facture_list %} -{% endif %} + {% if facture_list.paginator %} + {% 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 index 41984c2c..c1c5a396 100644 --- a/cotisations/templates/cotisations/aff_custom_invoice.html +++ b/cotisations/templates/cotisations/aff_custom_invoice.html @@ -26,7 +26,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if custom_invoice_list.paginator %} - {% include 'pagination.html' with list=custom_invoice_list %} + {% include 'pagination.html' with list=custom_invoice_list %} {% endif %} @@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., @@ -51,7 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% include 'buttons/sort.html' with prefix='invoice' col='id' text=tr_invoice_id %} @@ -76,7 +76,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% acl_end %} {% history_button invoice %} - {% trans "PDF" %} + {% trans "PDF" %} @@ -84,6 +84,6 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% trans "Recipient" as tr_recip %} - {% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_user %} + {% include 'buttons/sort.html' with prefix='invoice' col='user' text=tr_recip %} {% trans "Designation" %} {% trans "Total price" %} - {% trans "Paid" as tr_invoice_paid%} + {% trans "Paid" as tr_invoice_paid %} {% include 'buttons/sort.html' with prefix='invoice' col='paid' text=tr_invoice_paid %}
{% if custom_invoice_list.paginator %} - {% include 'pagination.html' with list=custom_invoice_list %} + {% 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 633eb456..afb78b48 100644 --- a/cotisations/templates/cotisations/aff_paiement.html +++ b/cotisations/templates/cotisations/aff_paiement.html @@ -45,9 +45,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% can_edit paiement %} - - - + {% include 'buttons/edit.html' with href='cotisations:edit-paiement' id=paiement.id %} {% acl_end %} {% history_button paiement %} diff --git a/cotisations/templates/cotisations/control.html b/cotisations/templates/cotisations/control.html index 6a4a5cca..5a18bd01 100644 --- a/cotisations/templates/cotisations/control.html +++ b/cotisations/templates/cotisations/control.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 @@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% trans "Invoice control and validation" %}

{% if facture_list.paginator %} -{% include 'pagination.html' with list=facture_list %} + {% include 'pagination.html' with list=facture_list %} {% endif %}
@@ -105,11 +105,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %} {% trans "Edit" as tr_edit %} - {% bootstrap_button tr_edit button_type='submit' icon='star' %} + {% bootstrap_button tr_edit button_type='submit' icon='ok' button_class='btn-success' %}
{% endblock %} {% if facture_list.paginator %} -{% include 'pagination.html' with list=facture_list %} + {% include 'pagination.html' with list=facture_list %} {% endif %} diff --git a/cotisations/templates/cotisations/delete.html b/cotisations/templates/cotisations/delete.html index dc06e5a5..e6060d09 100644 --- a/cotisations/templates/cotisations/delete.html +++ b/cotisations/templates/cotisations/delete.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -36,7 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% 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' %} + {% bootstrap_button tr_confirm button_type='submit' icon='trash' button_class='btn-danger' %} {% endblock %} diff --git a/cotisations/templates/cotisations/edit_facture.html b/cotisations/templates/cotisations/edit_facture.html index 9ddcac8c..891f7476 100644 --- a/cotisations/templates/cotisations/edit_facture.html +++ b/cotisations/templates/cotisations/edit_facture.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 @@ -35,10 +35,14 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} -

{% trans "Edit the invoice" %}

+ {% if title %} +

{{title}}

+ {% else %} +

{% trans "Edit invoice" %}

+ {% endif %} {% massive_bootstrap_form factureform 'user' %} {{ venteform.management_form }} -

{% trans "Invoice's articles" %}

+

{% trans "Articles" %}

@@ -58,7 +62,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type='submit' icon='star' %} + {% bootstrap_button tr_confirm button_type='submit' icon='ok' button_class='btn-success' %}
{% endblock %} diff --git a/cotisations/templates/cotisations/email_subscription_accepted b/cotisations/templates/cotisations/email_subscription_accepted new file mode 100644 index 00000000..58027cec --- /dev/null +++ b/cotisations/templates/cotisations/email_subscription_accepted @@ -0,0 +1,22 @@ +Bonjour {{name}} ! + +Nous vous informons que votre cotisation auprès de {{asso_name}} a été acceptée. Vous voilà donc membre de l'association. + +Vous trouverez en pièce jointe un reçu. + +Pour nous faire part de toute remarque, suggestion ou problème vous pouvez nous envoyer un mail à {{asso_email}}. + +À bientôt, +L'équipe de {{asso_name}}. + +--- + +Your subscription to {{asso_name}} has just been accepted. You are now a full member of {{asso_name}}. + +You will find with this email a subscription voucher. + +For any information, suggestion or problem, you can contact us via email at +{{asso_email}}. + +Regards, +The {{asso_name}} team. diff --git a/cotisations/templates/cotisations/facture.html b/cotisations/templates/cotisations/facture.html index 4f905160..65b05199 100644 --- a/cotisations/templates/cotisations/facture.html +++ b/cotisations/templates/cotisations/facture.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 @@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if title %}

{{ title }}

{% else %} -

{% trans "New invoice" %}

+

{% trans "Buy" %}

{% endif %} {% if max_balance %}

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

@@ -44,6 +44,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% blocktrans %}Current balance: {{ balance }} €{% endblocktrans %}

{% endif %} +{% if factureform %} +{% bootstrap_form_errors factureform %} +{% endif %} +{% if discount_form %} +{% bootstrap_form_errors discount_form %} +{% endif %}
{% csrf_token %} @@ -53,7 +59,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% if articlesformset %} -

{% trans "Invoice's articles" %}

+

{% trans "Articles" %}

{{ articlesformset.management_form }} {% for articlesform in articlesformset.forms %} @@ -67,116 +73,132 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endfor %} - + + {% if discount_form %} +

{% trans "Discount" %}

+ {% bootstrap_form discount_form %} + {% endif %}

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

{% endif %} - {% bootstrap_button action_name button_type='submit' icon='star' %} + {% bootstrap_button action_name button_type='submit' icon='ok' button_class='btn-success' %}
{% if articlesformset or payment_method%} {% endif %} diff --git a/cotisations/templates/cotisations/factures.tex b/cotisations/templates/cotisations/factures.tex index 3f2ebedc..2cfd4f46 100644 --- a/cotisations/templates/cotisations/factures.tex +++ b/cotisations/templates/cotisations/factures.tex @@ -43,7 +43,7 @@ \begin{document} - + %---------------------------------------------------------------------------------------- % HEADING SECTION %---------------------------------------------------------------------------------------- @@ -70,13 +70,17 @@ {\bf Siret :} {{siret|safe}} \vspace{2cm} - + \begin{tabular*}{\textwidth}{@{\extracolsep{\fill}} l r} {\bf Pour :} {{recipient_name|safe}} & {\bf Date :} {{DATE}} \\ {\bf Adresse :} {% if address is None %}Aucune adresse renseignée{% else %}{{address}}{% endif %} & \\ {% if fid is not None %} + {% if is_estimate %} + {\bf Devis n\textsuperscript{o} :} {{ fid }} & \\ + {% else %} {\bf Facture n\textsuperscript{o} :} {{ fid }} & \\ {% endif %} + {% endif %} \end{tabular*} \\ @@ -84,39 +88,57 @@ %---------------------------------------------------------------------------------------- % TABLE OF EXPENSES %---------------------------------------------------------------------------------------- - + \begin{tabularx}{\textwidth}{|X|r|r|r|} \hline \textbf{Désignation} & \textbf{Prix Unit.} \euro & \textbf{Quantité} & \textbf{Prix total} \euro\\ \doublehline - + {% for a in article %} {{a.name}} & {{a.price}} \euro & {{a.quantity}} & {{a.total_price}} \euro\\ \hline {% endfor %} - + \end{tabularx} - + \vspace{1cm} \hfill \begin{tabular}{|l|r|} \hline \textbf{Total} & {{total|floatformat:2}} \euro \\ + {% if not is_estimate %} \textbf{Votre règlement} & {% if paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro \\ \doublehline \textbf{À PAYER} & {% if not paid %}{{total|floatformat:2}}{% else %} 00,00 {% endif %} \euro\\ + {% endif %} \hline \end{tabular} - + + \vspace{1cm} + \begin{tabularx}{\textwidth}{r X} + \hline + \textbf{Moyen de paiement} & {{payment_method|default:"Non spécifié"}} \\ + \hline + {% if remark %} + \textbf{Remarque} & {{remark|safe}} \\ + \hline + {% endif %} + {% if end_validity %} + \textbf{Validité} & Jusqu'au {{end_validity}} \\ + \hline + {% endif %} + \end{tabularx} + + \vfill %---------------------------------------------------------------------------------------- % FOOTNOTE %---------------------------------------------------------------------------------------- - + \hrule \smallskip \footnotesize{TVA non applicable, art. 293 B du CGI} diff --git a/cotisations/templates/cotisations/index.html b/cotisations/templates/cotisations/index.html index ca9cde5b..42b8a3bf 100644 --- a/cotisations/templates/cotisations/index.html +++ b/cotisations/templates/cotisations/index.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 diff --git a/cotisations/templates/cotisations/index_article.html b/cotisations/templates/cotisations/index_article.html index 41ffb62e..5f93b5ce 100644 --- a/cotisations/templates/cotisations/index_article.html +++ b/cotisations/templates/cotisations/index_article.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Articles" %}{% endblock %} {% block content %} -

{% trans "Article types list" %}

+

{% trans "List of article types" %}

{% can_create Article %} {% trans "Add an article type" %} diff --git a/cotisations/templates/cotisations/index_banque.html b/cotisations/templates/cotisations/index_banque.html index f4dea1b1..87067222 100644 --- a/cotisations/templates/cotisations/index_banque.html +++ b/cotisations/templates/cotisations/index_banque.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Banks" %}{% endblock %} {% block content %} -

{% trans "Banks list" %}

+

{% trans "List of banks" %}

{% can_create Banque %}
{% trans "Add a bank" %} diff --git a/cotisations/templates/cotisations/index_cost_estimate.html b/cotisations/templates/cotisations/index_cost_estimate.html new file mode 100644 index 00000000..c3d57197 --- /dev/null +++ b/cotisations/templates/cotisations/index_cost_estimate.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 "Cost estimates" %}{% endblock %} + +{% block content %} +

{% trans "List of cost estimates" %}

+{% can_create CostEstimate %} +{% include 'buttons/add.html' with href='cotisations:new-cost-estimate'%} +{% acl_end %} +{% include 'cotisations/aff_cost_estimate.html' %} +{% endblock %} diff --git a/cotisations/templates/cotisations/index_custom_invoice.html b/cotisations/templates/cotisations/index_custom_invoice.html index 67d00126..9b539614 100644 --- a/cotisations/templates/cotisations/index_custom_invoice.html +++ b/cotisations/templates/cotisations/index_custom_invoice.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 @@ -28,9 +28,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Custom invoices" %}{% endblock %} {% block content %} -

{% trans "Custom invoices list" %}

+

{% trans "List of custom invoices" %}

{% can_create CustomInvoice %} -{% include "buttons/add.html" with href='cotisations:new-custom-invoice'%} +{% 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 f4908d02..09b7e033 100644 --- a/cotisations/templates/cotisations/index_paiement.html +++ b/cotisations/templates/cotisations/index_paiement.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 diff --git a/cotisations/templates/cotisations/payment.html b/cotisations/templates/cotisations/payment.html index 997168fd..ceb8db6f 100644 --- a/cotisations/templates/cotisations/payment.html +++ b/cotisations/templates/cotisations/payment.html @@ -1,4 +1,4 @@ -{% extends "cotisations/sidebar.html" %} +{% 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 @@ -40,7 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% bootstrap_form form %} {% endif %} {% trans "Pay" as tr_pay %} - {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' %} + {% bootstrap_button tr_pay button_type='submit' icon='piggy-bank' button_class='btn-success' %} {% endblock %} diff --git a/cotisations/templates/cotisations/sidebar.html b/cotisations/templates/cotisations/sidebar.html index 4f077fad..608f95c2 100644 --- a/cotisations/templates/cotisations/sidebar.html +++ b/cotisations/templates/cotisations/sidebar.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.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 @@ -28,35 +28,40 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block sidebar %} {% can_create CustomInvoice %} -
+ {% trans "Create an invoice" %} - + {% trans "Control the invoices" %} {% acl_end %} {% can_view_all Facture %} - + {% trans "Invoices" %} {% acl_end %} {% can_view_all CustomInvoice %} - + {% trans "Custom invoices" %} {% acl_end %} + {% can_view_all CostEstimate %} + + {% trans "Cost estimates" %} + + {% acl_end %} {% can_view_all Article %} - + {% trans "Available articles" %} {% acl_end %} {% can_view_all Banque %} - + {% trans "Banks" %} {% acl_end %} {% can_view_all Paiement %} - + {% trans "Payment methods" %} {% acl_end %} diff --git a/cotisations/templates/cotisations/voucher.tex b/cotisations/templates/cotisations/voucher.tex new file mode 100644 index 00000000..aeebc187 --- /dev/null +++ b/cotisations/templates/cotisations/voucher.tex @@ -0,0 +1,87 @@ +{% load i18n %} +{% language 'fr' %} + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Invoice Template +% LaTeX Template +% Version 1.0 (3/11/12) +%% This template has been downloaded from: +% http://www.LaTeXTemplates.com +% +% Original author: +% Trey Hunner (http://www.treyhunner.com/) +% +% License: +% CC BY-NC-SA 3.0 (http://creativecommons.org/licenses/by-nc-sa/3.0/) +% +% Important note: +% This template requires the invoice.cls file to be in the same directory as +% the .tex file. The invoice.cls file provides the style used for structuring the +% document. +% +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +%---------------------------------------------------------------------------------------- +% DOCUMENT CONFIGURATION +%---------------------------------------------------------------------------------------- + +\documentclass[12pt]{article} % Use the custom invoice class (invoice.cls) +\usepackage[utf8]{inputenc} +\usepackage[letterpaper,hmargin=0.79in,vmargin=0.79in]{geometry} +\usepackage{longtable} +\usepackage{graphicx} +\usepackage{tabularx} +\usepackage{eurosym} +\usepackage{multicol} + +\pagestyle{empty} % No page numbers + +\linespread{1.5} + +\newcommand{\doublehline}{\noalign{\hrule height 1pt}} +\setlength{\parindent}{0cm} + + +\begin{document} + + %---------------------------------------------------------------------------------------- + % HEADING SECTION + %---------------------------------------------------------------------------------------- + \begin{center} + {\Huge\bf Reçu d'adhésion \\ {{asso_name|safe}} } % Company providing the invoice + \end{center} + + \bigskip + \hrule + \bigskip + + \vfill + + Je sousigné, {{pres_name|safe}}, déclare par la présente avoir reçu le bulletin d'adhésion de: + + \begin{center} + \setlength{\tabcolsep}{10pt} % Make table columns tighter, usefull for postionning + \begin{tabular}{r l r l} + {\bf Prénom :}~ & {{firstname|safe}} & {% if phone %}{\bf Téléphone :}~ & {{phone}}{% else %} & {% endif %} \\ + {\bf Nom :}~ & {{lastname|safe}} & {\bf Mail :}~ & {{email|safe}} \\ + \end{tabular} + \end{center} + \bigskip + + ainsi que sa cotisation. + + Le postulant, déclare reconnaître l'objet de l'association, et en a accepté les statuts ainsi que le règlement intérieur qui sont mis à sa disposition dans les locaux de l'association. L'adhésion du membre sus-nommé est ainsi validée. Ce reçu confirme la qualité de membre du postulant, et ouvre droit à la participation à l'assemblée générale de l'association jusqu'au {{date_end|date:"d F Y"}}. + + \bigskip + + Validé électroniquement par {{pres_name|safe}}, le {{date_begin|date:"d/m/Y"}}. + + \vfill + \hrule + \smallskip + \footnotesize + Les informations recueillies sont nécessaires pour votre adhésion. Conformément à la loi "Informatique et Libertés" du 6 janvier 1978, vous disposez d'un droit d'accès et de rectification aux données personnelles vous concernant. Pour l'exercer, adressez-vous au secrétariat de l'association. + + +\end{document} +{% endlanguage %} diff --git a/cotisations/tex.py b/cotisations/tex.py index 0ee45bdb..1ab964ba 100644 --- a/cotisations/tex.py +++ b/cotisations/tex.py @@ -1,4 +1,4 @@ -# coding: utf-8 +# -*- 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. @@ -31,11 +31,16 @@ from subprocess import Popen, PIPE import os from datetime import datetime +from django.db import models from django.template.loader import get_template from django.template import Context from django.http import HttpResponse from django.conf import settings from django.utils.text import slugify +from django.utils.translation import ugettext_lazy as _ + +from re2o.mixins import AclMixin, RevMixin +from preferences.models import CotisationsOption TEMP_PREFIX = getattr(settings, 'TEX_TEMP_PREFIX', 'render_tex-') @@ -48,15 +53,40 @@ def render_invoice(_request, ctx={}): Render an invoice using some available information such as the current date, the user, the articles, the prices, ... """ + options, _ = CotisationsOption.objects.get_or_create() + is_estimate = ctx.get('is_estimate', False) filename = '_'.join([ - 'invoice', + 'cost_estimate' if is_estimate else 'invoice', slugify(ctx.get('asso_name', "")), slugify(ctx.get('recipient_name', "")), str(ctx.get('DATE', datetime.now()).year), str(ctx.get('DATE', datetime.now()).month), str(ctx.get('DATE', datetime.now()).day), ]) - r = render_tex(_request, 'cotisations/factures.tex', ctx) + templatename = options.invoice_template.template.name.split('/')[-1] + r = render_tex(_request, templatename, ctx) + r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format( + name=filename + ) + return r + + +def render_voucher(_request, ctx={}): + """ + Render a subscribtion voucher. + """ + options, _ = CotisationsOption.objects.get_or_create() + filename = '_'.join([ + 'voucher', + slugify(ctx.get('asso_name', "")), + slugify(ctx.get('firstname', "")), + slugify(ctx.get('lastname', "")), + str(ctx.get('date_begin', datetime.now()).year), + str(ctx.get('date_begin', datetime.now()).month), + str(ctx.get('date_begin', datetime.now()).day), + ]) + templatename = options.voucher_template.template.name.split('/')[-1] + r = render_tex(_request, templatename, ctx) r['Content-Disposition'] = 'attachment; filename="{name}.pdf"'.format( name=filename ) @@ -81,18 +111,33 @@ def create_pdf(template, ctx={}): with tempfile.TemporaryDirectory() as tempdir: for _ in range(2): - process = Popen( - ['pdflatex', '-output-directory', tempdir], - stdin=PIPE, - stdout=PIPE, - ) - process.communicate(rendered_tpl) + with open("/var/www/re2o/out.log", "w") as f: + process = Popen( + ['pdflatex', '-output-directory', tempdir], + stdin=PIPE, + stdout=f,#PIPE, + ) + process.communicate(rendered_tpl) with open(os.path.join(tempdir, 'texput.pdf'), 'rb') as f: pdf = f.read() return pdf +def escape_chars(string): + """Escape the '%' and the '€' signs to avoid messing with LaTeX""" + if not isinstance(string, str): + return string + mapping = ( + ('€', r'\euro'), + ('%', r'\%'), + ) + r = str(string) + for k, v in mapping: + r = r.replace(k, v) + return r + + def render_tex(_request, template, ctx={}): """Creates a PDF from a LaTex templates using pdflatex. diff --git a/cotisations/urls.py b/cotisations/urls.py index edc448fe..0c7256f7 100644 --- a/cotisations/urls.py +++ b/cotisations/urls.py @@ -51,11 +51,46 @@ urlpatterns = [ views.facture_pdf, name='facture-pdf' ), + url( + r'^voucher_pdf/(?P[0-9]+)$', + views.voucher_pdf, + name='voucher-pdf' + ), + url( + r'^new_cost_estimate/$', + views.new_cost_estimate, + name='new-cost-estimate' + ), + url( + r'^index_cost_estimate/$', + views.index_cost_estimate, + name='index-cost-estimate' + ), + url( + r'^cost_estimate_pdf/(?P[0-9]+)$', + views.cost_estimate_pdf, + name='cost-estimate-pdf', + ), url( r'^index_custom_invoice/$', views.index_custom_invoice, name='index-custom-invoice' ), + url( + r'^edit_cost_estimate/(?P[0-9]+)$', + views.edit_cost_estimate, + name='edit-cost-estimate' + ), + url( + r'^cost_estimate_to_invoice/(?P[0-9]+)$', + views.cost_estimate_to_invoice, + name='cost-estimate-to-invoice' + ), + url( + r'^del_cost_estimate/(?P[0-9]+)$', + views.del_cost_estimate, + name='del-cost-estimate' + ), url( r'^new_custom_invoice/$', views.new_custom_invoice, @@ -146,5 +181,5 @@ urlpatterns = [ views.control, name='control' ), - url(r'^$', views.index, name='index'), + url(r'^$', views.index, name='index'), ] + payment_methods.urls.urlpatterns diff --git a/cotisations/utils.py b/cotisations/utils.py index 0211dd40..4715338b 100644 --- a/cotisations/utils.py +++ b/cotisations/utils.py @@ -25,7 +25,7 @@ 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 preferences.models import AssoOption, GeneralOption, CotisationsOption from re2o.settings import LOGO_PATH from re2o import settings @@ -89,7 +89,42 @@ def send_mail_invoice(invoice): 'Votre facture / Your invoice', template.render(ctx), GeneralOption.get_cached_value('email_from'), - [invoice.user.email], + [invoice.user.get_mail], attachments=[('invoice.pdf', pdf, 'application/pdf')] ) mail.send() + + +def send_mail_voucher(invoice): + """Creates a voucher from an invoice and sends it by email to the client""" + ctx = { + 'asso_name': AssoOption.get_cached_value('name'), + 'pres_name': AssoOption.get_cached_value('pres_name'), + 'firstname': invoice.user.name, + 'lastname': invoice.user.surname, + 'email': invoice.user.email, + 'phone': invoice.user.telephone, + 'date_end': invoice.get_subscription().latest('date_end').date_end, + 'date_begin': invoice.get_subscription().earliest('date_start').date_start + } + templatename = CotisationsOption.get_cached_value('voucher_template').template.name.split('/')[-1] + pdf = create_pdf(templatename, ctx) + template = get_template('cotisations/email_subscription_accepted') + + ctx = { + 'name': "{} {}".format( + invoice.user.name, + invoice.user.surname + ), + 'asso_email': AssoOption.get_cached_value('contact'), + 'asso_name': AssoOption.get_cached_value('name') + } + + mail = EmailMessage( + 'Votre reçu / Your voucher', + template.render(ctx), + GeneralOption.get_cached_value('email_from'), + [invoice.user.get_mail], + attachments=[('voucher.pdf', pdf, 'application/pdf')] + ) + mail.send() diff --git a/cotisations/views.py b/cotisations/views.py index a4a35825..b6d5a8a6 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -47,7 +47,10 @@ from users.models import User from re2o.settings import LOGO_PATH from re2o import settings from re2o.views import form -from re2o.utils import SortTable, re2o_paginator +from re2o.base import ( + SortTable, + re2o_paginator, +) from re2o.acl import ( can_create, can_edit, @@ -65,7 +68,8 @@ from .models import ( Paiement, Banque, CustomInvoice, - BaseInvoice + BaseInvoice, + CostEstimate, ) from .forms import ( FactureForm, @@ -77,11 +81,13 @@ from .forms import ( DelBanqueForm, SelectArticleForm, RechargeForm, - CustomInvoiceForm + CustomInvoiceForm, + DiscountForm, + CostEstimateForm, ) -from .tex import render_invoice +from .tex import render_invoice, render_voucher, escape_chars from .payment_methods.forms import payment_method_factory -from .utils import find_payment_method, send_mail_invoice +from .utils import find_payment_method @login_required @@ -148,8 +154,6 @@ 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 @@ -171,13 +175,65 @@ def new_facture(request, user, userid): 'articlesformset': article_formset, 'articlelist': article_list, 'balance': balance, - 'action_name': _('Create'), + 'action_name': _('Confirm'), }, 'cotisations/facture.html', request ) -# TODO : change facture to invoice +@login_required +@can_create(CostEstimate) +def new_cost_estimate(request): + """ + 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. + """ + # The template needs the list of articles (for the JS part) + articles = Article.objects.filter( + Q(type_user='All') | Q(type_user=request.user.class_name) + ) + # Building the invocie form and the article formset + cost_estimate_form = CostEstimateForm(request.POST or None) + + articles_formset = formset_factory(SelectArticleForm)( + request.POST or None, + form_kwargs={'user': request.user} + ) + discount_form = DiscountForm(request.POST or None) + + if cost_estimate_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid(): + cost_estimate_instance = cost_estimate_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=cost_estimate_instance, + name=article.name, + prix=article.prix, + type_cotisation=article.type_cotisation, + duration=article.duration, + number=quantity + ) + discount_form.apply_to_invoice(cost_estimate_instance) + + messages.success( + request, + _("The cost estimate was created.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + + return form({ + 'factureform': cost_estimate_form, + 'action_name': _("Confirm"), + 'articlesformset': articles_formset, + 'articlelist': articles, + 'discount_form': discount_form, + 'title': _("Cost estimate"), + }, 'cotisations/facture.html', request) + + @login_required @can_create(CustomInvoice) def new_custom_invoice(request): @@ -193,12 +249,13 @@ def new_custom_invoice(request): # Building the invocie form and the article formset invoice_form = CustomInvoiceForm(request.POST or None) - article_formset = formset_factory(SelectArticleForm)( + articles_formset = formset_factory(SelectArticleForm)( request.POST or None, - form_kwargs={'user': request.user, 'target_user': user} + form_kwargs={'user': request.user} ) + discount_form = DiscountForm(request.POST or None) - if invoice_form.is_valid() and articles_formset.is_valid(): + if invoice_form.is_valid() and articles_formset.is_valid() and discount_form.is_valid(): new_invoice_instance = invoice_form.save() for art_item in articles_formset: if art_item.cleaned_data: @@ -212,18 +269,19 @@ def new_custom_invoice(request): duration=article.duration, number=quantity ) + discount_form.apply_to_invoice(new_invoice_instance) messages.success( request, _("The custom invoice was created.") ) return redirect(reverse('cotisations:index-custom-invoice')) - return form({ 'factureform': invoice_form, - 'action_name': _("Create"), + 'action_name': _("Confirm"), 'articlesformset': articles_formset, - 'articlelist': articles + 'articlelist': articles, + 'discount_form': discount_form }, 'cotisations/facture.html', request) @@ -266,7 +324,8 @@ def facture_pdf(request, facture, **_kwargs): '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) + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), + 'payment_method': facture.paiement.moyen, }) @@ -331,6 +390,55 @@ def del_facture(request, facture, **_kwargs): }, 'cotisations/delete.html', request) +@login_required +@can_edit(CostEstimate) +def edit_cost_estimate(request, invoice, **kwargs): + # Building the invocie form and the article formset + invoice_form = CostEstimateForm( + 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 cost estimate was edited.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + + return form({ + 'factureform': invoice_form, + 'venteform': purchase_form, + 'title': _("Edit cost estimate") + }, 'cotisations/edit_facture.html', request) + + +@login_required +@can_edit(CostEstimate) +@can_create(CustomInvoice) +def cost_estimate_to_invoice(request, cost_estimate, **_kwargs): + """Create a custom invoice from a cos estimate""" + cost_estimate.create_invoice() + messages.success( + request, + _("An invoice was successfully created from your cost estimate.") + ) + return redirect(reverse('cotisations:index-custom-invoice')) + + @login_required @can_edit(CustomInvoice) def edit_custom_invoice(request, invoice, **kwargs): @@ -367,22 +475,21 @@ def edit_custom_invoice(request, invoice, **kwargs): @login_required -@can_view(CustomInvoice) -def custom_invoice_pdf(request, invoice, **_kwargs): +@can_view(CostEstimate) +def cost_estimate_pdf(request, invoice, **_kwargs): """ - View used to generate a PDF file from an existing invoice in database + View used to generate a PDF file from an existing cost estimate 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, + 'name': escape_chars(purchase.name), 'price': purchase.prix, 'quantity': purchase.number, 'total_price': purchase.prix_total @@ -401,11 +508,74 @@ def custom_invoice_pdf(request, invoice, **_kwargs): '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) + 'tpl_path': os.path.join(settings.BASE_DIR, LOGO_PATH), + 'payment_method': invoice.payment, + 'remark': invoice.remark, + 'end_validity': invoice.date + invoice.validity, + 'is_estimate': True, + }) + + +@login_required +@can_delete(CostEstimate) +def del_cost_estimate(request, estimate, **_kwargs): + """ + View used to delete an existing invocie. + """ + if request.method == "POST": + estimate.delete() + messages.success( + request, + _("The cost estimate was deleted.") + ) + return redirect(reverse('cotisations:index-cost-estimate')) + return form({ + 'objet': estimate, + 'objet_name': _("Cost estimate") + }, 'cotisations/delete.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': escape_chars(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), + 'payment_method': invoice.payment, + 'remark': invoice.remark, }) -# TODO : change facture to invoice @login_required @can_delete(CustomInvoice) def del_custom_invoice(request, invoice, **_kwargs): @@ -550,7 +720,7 @@ def edit_paiement(request, paiement_instance, **_kwargs): if payment_method is not None: payment_method.save() messages.success( - request,_("The payment method was edited.") + request, _("The payment method was edited.") ) return redirect(reverse('cotisations:index-paiement')) return form({ @@ -663,8 +833,8 @@ def del_banque(request, instances): except ProtectedError: messages.error( request, - _("The bank %(bank_name)s can't be deleted \ - because there are invoices using it.") % { + _("The bank %(bank_name)s can't be deleted because there" + " are invoices using it.") % { 'bank_name': bank_del } ) @@ -756,12 +926,36 @@ def index_banque(request): }) +@login_required +@can_view_all(CustomInvoice) +def index_cost_estimate(request): + """View used to display every custom invoice.""" + pagination_number = GeneralOption.get_cached_value('pagination_number') + cost_estimate_list = CostEstimate.objects.prefetch_related('vente_set') + cost_estimate_list = SortTable.sort( + cost_estimate_list, + request.GET.get('col'), + request.GET.get('order'), + SortTable.COTISATIONS_CUSTOM + ) + cost_estimate_list = re2o_paginator( + request, + cost_estimate_list, + pagination_number, + ) + return render(request, 'cotisations/index_cost_estimate.html', { + 'cost_estimate_list': cost_estimate_list + }) + + @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') + cost_estimate_ids = [i for i, in CostEstimate.objects.values_list('id')] + custom_invoice_list = CustomInvoice.objects.prefetch_related( + 'vente_set').exclude(id__in=cost_estimate_ids) custom_invoice_list = SortTable.sort( custom_invoice_list, request.GET.get('col'), @@ -827,7 +1021,8 @@ def credit_solde(request, user, **_kwargs): kwargs={'userid': user.id} )) - refill_form = RechargeForm(request.POST or None, user=user, user_source=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) @@ -839,7 +1034,6 @@ def credit_solde(request, user, **_kwargs): else: price_ok = True if price_ok: - invoice.valid = True invoice.save() Vente.objects.create( facture=invoice, @@ -848,8 +1042,6 @@ def credit_solde(request, user, **_kwargs): number=1 ) - send_mail_invoice(invoice) - return invoice.paiement.end_payment(invoice, request) p = get_object_or_404(Paiement, is_balance=True) return form({ @@ -857,6 +1049,32 @@ def credit_solde(request, user, **_kwargs): 'balance': user.solde, 'title': _("Refill your balance"), 'action_name': _("Pay"), - 'max_balance': p.payment_method.maximum_balance, + 'max_balance': find_payment_method(p).maximum_balance, }, 'cotisations/facture.html', request) + +@login_required +@can_view(Facture) +def voucher_pdf(request, invoice, **_kwargs): + """ + View used to generate a PDF file from a controlled invoice + 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. + """ + if not invoice.control: + messages.error( + request, + _("Could not find a voucher for that invoice.") + ) + return redirect(reverse('cotisations:index')) + return render_voucher(request, { + 'asso_name': AssoOption.get_cached_value('name'), + 'pres_name': AssoOption.get_cached_value('pres_name'), + 'firstname': invoice.user.name, + 'lastname': invoice.user.surname, + 'email': invoice.user.email, + 'phone': invoice.user.telephone, + 'date_end': invoice.get_subscription().latest('date_end').date_end, + 'date_begin': invoice.date + }) diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index afa834b0..54364dc1 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -38,6 +38,7 @@ Inspiré du travail de Daniel Stan au Crans import os import sys import logging +import traceback import radiusd # Module magique freeradius (radiusd.py is dummy) from django.core.wsgi import get_wsgi_application @@ -57,14 +58,9 @@ application = get_wsgi_application() from machines.models import Interface, IpList, Nas, Domain from topologie.models import Port, Switch from users.models import User -from preferences.models import OptionalTopologie +from preferences.models import RadiusOption -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)) @@ -81,7 +77,7 @@ class RadiusdHandler(logging.Handler): rad_sig = radiusd.L_INFO else: rad_sig = radiusd.L_DBG - radiusd.radlog(rad_sig, record.msg) + radiusd.radlog(rad_sig, record.msg.encode('utf-8')) # Initialisation d'un logger (pour logguer unifié) @@ -122,7 +118,8 @@ def radius_event(fun): return fun(data) except Exception as err: logger.error('Failed %r on data %r' % (err, auth_data)) - raise + logger.debug('Function %r, Traceback: %s' % (fun, repr(traceback.format_stack()))) + return radiusd.RLM_MODULE_FAIL return new_f @@ -194,12 +191,12 @@ def post_auth(data): nas_instance = find_nas_from_request(nas) # Toutes les reuquètes non proxifiées if not nas_instance: - logger.info(u"Requète proxifiée, nas inconnu".encode('utf-8')) + logger.info(u"Requete proxifiee, nas inconnu".encode('utf-8')) return radiusd.RLM_MODULE_OK nas_type = Nas.objects.filter(nas_type=nas_instance.type).first() if not nas_type: logger.info( - u"Type de nas non enregistré dans la bdd!".encode('utf-8') + u"Type de nas non enregistre dans la bdd!".encode('utf-8') ) return radiusd.RLM_MODULE_OK @@ -227,27 +224,37 @@ def post_auth(data): # On récupère le numéro du port sur l'output de freeradius. # La ligne suivante fonctionne pour cisco, HP et Juniper port = port.split(".")[0].split('/')[-1][-2:] - out = decide_vlan_and_register_switch(nas_machine, nas_type, port, mac) - sw_name, room, reason, vlan_id = out + out = decide_vlan_switch(nas_machine, nas_type, port, mac) + sw_name, room, reason, vlan_id, decision = out - log_message = '(fil) %s -> %s [%s%s]' % ( - sw_name + u":" + port + u"/" + str(room), - mac, - vlan_id, - (reason and u': ' + reason).encode('utf-8') - ) - logger.info(log_message) + if decision: + log_message = '(fil) %s -> %s [%s%s]' % ( + sw_name + u":" + port + u"/" + str(room), + mac, + vlan_id, + (reason and u': ' + reason).encode('utf-8') + ) + logger.info(log_message) - # Filaire - return ( - radiusd.RLM_MODULE_UPDATED, - ( - ("Tunnel-Type", "VLAN"), - ("Tunnel-Medium-Type", "IEEE-802"), - ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)), - ), - () - ) + # Filaire + return ( + radiusd.RLM_MODULE_UPDATED, + ( + ("Tunnel-Type", "VLAN"), + ("Tunnel-Medium-Type", "IEEE-802"), + ("Tunnel-Private-Group-Id", '%d' % int(vlan_id)), + ), + () + ) + else: + log_message = '(fil) %s -> %s [Reject:%s]' % ( + sw_name + u":" + port + u"/" + str(room), + mac, + (reason and u': ' + reason).encode('utf-8') + ) + logger.info(log_message) + + return radiusd.RLM_MODULE_REJECT else: return radiusd.RLM_MODULE_OK @@ -284,19 +291,19 @@ def check_user_machine_and_register(nas_type, username, mac_address): Renvoie le mot de passe ntlm de l'user si tout est ok Utilise pour les authentifications en 802.1X""" interface = Interface.objects.filter(mac_address=mac_address).first() - user = User.objects.filter(pseudo=username).first() + user = User.objects.filter(pseudo__iexact=username).first() if not user: return (False, u"User inconnu", '') if not user.has_access(): - return (False, u"Adhérent non cotisant", '') + return (False, u"Adherent non cotisant", '') if interface: if interface.machine.user != user: return (False, - u"Machine enregistrée sur le compte d'un autre " + u"Machine enregistree sur le compte d'un autre " "user...", '') elif not interface.is_active: - return (False, u"Machine desactivée", '') + return (False, u"Machine desactivee", '') elif not interface.ipv4: interface.assign_ipv4() return (True, u"Ok, Reassignation de l'ipv4", user.pwd_ntlm) @@ -317,36 +324,51 @@ def check_user_machine_and_register(nas_type, username, mac_address): return (False, u"Machine inconnue", '') -def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, - mac_address): +def decide_vlan_switch(nas_machine, nas_type, port_number, + mac_address): """Fonction de placement vlan pour un switch en radius filaire auth par mac. Plusieurs modes : - - nas inconnu, port inconnu : on place sur le vlan par defaut VLAN_OK - - pas de radius sur le port : VLAN_OK - - bloq : VLAN_NOK - - force : placement sur le vlan indiqué dans la bdd - - mode strict : - - pas de chambre associée : VLAN_NOK - - pas d'utilisateur dans la chambre : VLAN_NOK - - cotisation non à jour : VLAN_NOK - - sinon passe à common (ci-dessous) - - mode common : - - interface connue (macaddress): - - utilisateur proprio non cotisant ou banni : VLAN_NOK - - user à jour : VLAN_OK - - interface inconnue : - - register mac désactivé : VLAN_NOK - - register mac activé : - - dans la chambre associé au port, pas d'user ou non à - jour : VLAN_NOK - - user à jour, autocapture de la mac et VLAN_OK + - tous les modes: + - nas inconnu: VLAN_OK + - port inconnu: Politique définie dans RadiusOption + - pas de radius sur le port: VLAN_OK + - force: placement sur le vlan indiqué dans la bdd + - mode strict: + - pas de chambre associée: Politique définie + dans RadiusOption + - pas d'utilisateur dans la chambre : Rejet + (redirection web si disponible) + - utilisateur de la chambre banni ou désactivé : Rejet + (redirection web si disponible) + - utilisateur de la chambre non cotisant et non whiteslist: + Politique définie dans RadiusOption + + - sinon passe à common (ci-dessous) + - mode common : + - interface connue (macaddress): + - utilisateur proprio non cotisant / machine désactivée: + Politique définie dans RadiusOption + - utilisateur proprio banni : + Politique définie dans RadiusOption + - user à jour : VLAN_OK (réassignation de l'ipv4 au besoin) + - interface inconnue : + - register mac désactivé : Politique définie + dans RadiusOption + - register mac activé: redirection vers webauth + Returns: + tuple avec : + - Nom du switch (str) + - chambre (str) + - raison de la décision (str) + - vlan_id (int) + - decision (bool) """ # Get port from switch and port number extra_log = "" # Si le NAS est inconnu, on place sur le vlan defaut if not nas_machine: - return ('?', u'Chambre inconnue', u'Nas inconnu', VLAN_OK) + return ('?', u'Chambre inconnue', u'Nas inconnu', RadiusOption.get_cached_value('vlan_decision_ok').vlan_id, True) sw_name = str(getattr(nas_machine, 'short_name', str(nas_machine))) @@ -361,7 +383,13 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, # 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) + return ( + sw_name, + "Chambre inconnue", + u'Port inconnu', + getattr(RadiusOption.get_cached_value('unknown_port_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('unknown_port')!= RadiusOption.REJECT + ) # On récupère le profil du port port_profile = port.get_port_profile @@ -372,46 +400,82 @@ def decide_vlan_and_register_switch(nas_machine, nas_type, port_number, DECISION_VLAN = int(port_profile.vlan_untagged.vlan_id) extra_log = u"Force sur vlan " + str(DECISION_VLAN) else: - DECISION_VLAN = VLAN_OK + DECISION_VLAN = RadiusOption.get_cached_value('vlan_decision_ok').vlan_id - # Si le port est désactivé, on rejette sur le vlan de déconnexion + # Si le port est désactivé, on rejette la connexion if not port.state: - return (sw_name, port.room, u'Port desactivé', VLAN_NOK) + return (sw_name, port.room, u'Port desactive', None, False) # 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) + DECISION_VLAN, + True) - # Si le 802.1X est activé sur ce port, cela veut dire que la personne a été accept précédemment + # 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': + if (nas_type.port_access_mode, port_profile.radius_type) == ('802.1X', '802.1X'): room = port.room or "Chambre/local inconnu" - return (sw_name, room, u'Acceptation authentification 802.1X', DECISION_VLAN) + return ( + sw_name, + room, + u'Acceptation authentification 802.1X', + DECISION_VLAN, + True + ) # 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 + # 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) + return ( + sw_name, + "Inconnue", + u'Chambre inconnue', + getattr(RadiusOption.get_cached_value('unknown_room_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('unknown_room')!= RadiusOption.REJECT + ) room_user = User.objects.filter( Q(club__room=port.room) | Q(adherent__room=port.room) ) if not room_user: - return (sw_name, room, u'Chambre non cotisante', VLAN_NOK) + return ( + sw_name, + room, + u'Chambre non cotisante -> Web redirect', + None, + False + ) for user in room_user: - if not user.has_access(): - return (sw_name, room, u'Chambre resident desactive', VLAN_NOK) + if user.is_ban() or user.state != User.STATE_ACTIVE: + return ( + sw_name, + room, + u'Utilisateur banni ou desactive -> Web redirect', + None, + False + ) + elif not (user.is_connected() or user.is_whitelisted()): + return ( + sw_name, + room, + u'Utilisateur non cotisant', + getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT + ) # else: user OK, on passe à la verif MAC - # Si on fait de l'auth par mac, on cherche l'interface via sa mac dans la bdd + # 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 @@ -421,88 +485,67 @@ 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, 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) + # On essaye de register la mac, si l'autocapture a été activée, + # on rejette pour faire une redirection web si possible. + if nas_type.autocapture_mac: + return ( + sw_name, + room, + u'Machine Inconnue -> Web redirect', + None, + False + ) + # Sinon on bascule sur la politique définie dans les options + # radius. 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) - ) - if not room_user: - return (sw_name, - room, - 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, - u'Machine inconnue, il y a au moins 2 users ' - '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: - interface, reason = (room_user - .first() - .autoregister_machine( - mac_address, - nas_type - )) - 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, - DECISION_VLAN) - else: - return (sw_name, - room, - u'Erreur dans le register mac %s' % ( - reason + str(mac_address) - ), - VLAN_NOK) - # L'interface a été trouvée, on vérifie qu'elle est active, sinon on reject + return ( + sw_name, + "", + u'Machine inconnue', + getattr(RadiusOption.get_cached_value('unknown_machine_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('unknown_machine')!= RadiusOption.REJECT + ) + + # 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 interface.machine.user.is_ban(): + return ( + sw_name, + room, + u'Adherent banni', + getattr(RadiusOption.get_cached_value('banned_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('banned')!= RadiusOption.REJECT + ) if not interface.is_active: - return (sw_name, - room, - u'Machine non active / adherent non cotisant', - VLAN_NOK) - ## Si on choisi de placer les machines sur le vlan correspondant à leur type : - if RADIUS_POLICY == 'MACHINE': + return ( + sw_name, + room, + u'Machine non active / adherent non cotisant', + getattr(RadiusOption.get_cached_value('non_member_vlan'), 'vlan_id', None), + RadiusOption.get_cached_value('non_member')!= RadiusOption.REJECT + ) + # Si on choisi de placer les machines sur le vlan + # correspondant à leur type : + if RadiusOption.get_cached_value('radius_general_policy') == 'MACHINE': DECISION_VLAN = interface.type.ip_type.vlan.vlan_id if not interface.ipv4: interface.assign_ipv4() - return (sw_name, - room, - u"Ok, Reassignation de l'ipv4" + extra_log, - DECISION_VLAN) + return ( + sw_name, + room, + u"Ok, Reassignation de l'ipv4" + extra_log, + DECISION_VLAN, + True + ) else: - return (sw_name, - room, - u'Machine OK' + extra_log, - DECISION_VLAN) + return ( + sw_name, + room, + u'Machine OK' + extra_log, + DECISION_VLAN, + True + ) diff --git a/install_re2o.sh b/install_re2o.sh index 6168ec08..d6b0a4ef 100755 --- a/install_re2o.sh +++ b/install_re2o.sh @@ -59,7 +59,7 @@ _ask_value() { install_requirements() { - ### Usage: install_requirements + ### Usage: install_requirements # # This function will install the required packages from APT repository # and Pypi repository. Those packages are all required for Re2o to work @@ -273,7 +273,7 @@ write_settings_file() { django_secret_key="$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(50)]))")" aes_key="$(python -c "import random; print(''.join([random.SystemRandom().choice('abcdefghijklmnopqrstuvwxyz0123456789%=+') for i in range(32)]))")" - + if [ "$db_engine_type" == 1 ]; then sed -i 's/db_engine/django.db.backends.mysql/g' "$SETTINGS_LOCAL_FILE" else @@ -316,6 +316,25 @@ update_django() { echo "Collecting web frontend statics ..." python3 manage.py collectstatic --noinput echo "Collecting web frontend statics: Done" + + echo "Generating locales ..." + python3 manage.py compilemessages + echo "Generating locales: Done" +} + + + +copy_templates_files() { + ### Usage: copy_templates_files + # + # This will copy LaTeX templates in the media root. + + echo "Copying LaTeX templates ..." + mkdir -p media/templates/ + cp cotisations/templates/cotisations/factures.tex media/templates/default_invoice.tex + cp cotisations/templates/cotisations/voucher.tex media/templates/default_voucher.tex + chown -R www-data:www-data media/templates/ + echo "Copying LaTeX templates: Done" } @@ -472,7 +491,7 @@ interactive_guide() { sql_host="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Prompt to enter the remote database name TITLE="SQL database name" INPUTBOX="The name of the remote SQL database" @@ -519,14 +538,14 @@ interactive_guide() { ldap_is_local="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --menu "$MENU" \ $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" - + # Prompt to enter the LDAP domain extension TITLE="Domain extension" INPUTBOX="The local domain extension to use (e.g. 'example.net'). This is used in the LDAP configuration." extension_locale="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Building the DN of the LDAP from the extension IFS='.' read -a extension_locale_array <<< $extension_locale for i in "${extension_locale_array[@]}" @@ -542,7 +561,7 @@ interactive_guide() { ldap_host="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Prompt to choose if TLS should be activated or not for the LDAP TITLE="TLS on LDAP" MENU="Would you like to activate TLS for communicating with the remote LDAP ?" @@ -579,7 +598,7 @@ interactive_guide() { ######################### BACKTITLE="Re2o setup - configuration of the mail server" - + # Prompt to enter the hostname of the mail server TITLE="Mail server hostname" INPUTBOX="The hostname of the mail server to use" @@ -587,7 +606,7 @@ interactive_guide() { --title "$TITLE" --inputbox "$TITLE" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - # Prompt to choose the port of the mail server + # Prompt to choose the port of the mail server TITLE="Mail server port" MENU="Which port (thus which protocol) to use to contact the mail server" OPTIONS=(25 "SMTP" @@ -604,7 +623,7 @@ interactive_guide() { ######################## BACKTITLE="Re2o setup - configuration of the web server" - + # Prompt to choose the web server TITLE="Web server to use" MENU="Which web server to install for accessing Re2o web frontend (automatic setup of nginx is not supported) ?" @@ -613,14 +632,14 @@ interactive_guide() { web_serveur="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --menu "$MENU" \ $HEIGHT $WIDTH $CHOICE_HEIGHT "${OPTIONS[@]}" 2>&1 >/dev/tty)" - + # Prompt to enter the requested URL for the web frontend TITLE="Web URL" INPUTBOX="URL for accessing the web server (e.g. re2o.example.net). Be sure that this URL is accessible and correspond to a DNS entry (if applicable)." url_server="$(dialog --clear --backtitle "$BACKTITLE" \ --title "$TITLE" --inputbox "$INPUTBOX" \ $HEIGHT $WIDTH 2>&1 >/dev/tty)" - + # Prompt to choose if the TLS should be setup or not for the web server TITLE="TLS on web server" MENU="Would you like to activate the TLS (with Let'Encrypt) on the web server ?" @@ -675,7 +694,7 @@ interactive_guide() { update_django create_superuser - + install_webserver "$web_serveur" "$is_tls" "$url_server" @@ -744,9 +763,10 @@ main_function() { echo " * {help} ---------- Display this quick usage documentation" echo " * {setup} --------- Launch the full interactive guide to setup entirely" echo " re2o from scratch" - echo " * {update} -------- Collect frontend statics, install the missing APT" + echo " * {update} -------- Collect frontend statics, install the missing APT and copy LaTeX templates files" echo " and pip packages and apply the migrations to the DB" echo " * {update-django} - Apply Django migration and collect frontend statics" + echo " * {copy-template-files} - Copy LaTeX templates files to media/templates" echo " * {update-packages} Install the missing APT and pip packages" echo " * {update-settings} Interactively rewrite the settings file" echo " * {reset-db} ------ Erase the previous local database, setup a new empty" @@ -778,9 +798,14 @@ main_function() { update ) install_requirements + copy_templates_files update_django ;; + copy-templates-files ) + copy_templates_files + ;; + update-django ) update_django ;; @@ -796,7 +821,7 @@ main_function() { reset-db ) if [ ! -z "$2" ]; then db_password="$2" - case "$3" in + case "$3" in mysql ) db_engine_type=1;; postresql ) diff --git a/logs/locale/fr/LC_MESSAGES/django.mo b/logs/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 030b0cac..00000000 Binary files a/logs/locale/fr/LC_MESSAGES/django.mo and /dev/null differ diff --git a/logs/locale/fr/LC_MESSAGES/django.po b/logs/locale/fr/LC_MESSAGES/django.po index 70c58073..807bda76 100644 --- a/logs/locale/fr/LC_MESSAGES/django.po +++ b/logs/locale/fr/LC_MESSAGES/django.po @@ -21,7 +21,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-15 20:12+0200\n" +"POT-Creation-Date: 2019-01-08 23:16+0100\n" "PO-Revision-Date: 2018-06-23 16:01+0200\n" "Last-Translator: Laouen Fernet \n" "Language-Team: \n" @@ -57,7 +57,7 @@ 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 +#: templates/logs/aff_summary.html:128 templates/logs/aff_summary.html:147 msgid "Cancel" msgstr "Annuler" @@ -113,15 +113,19 @@ 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 à" +msgid "%(username)s has sold %(number)sx %(name)s" +msgstr "%(username)s a vendu %(number)sx %(name)s" #: templates/logs/aff_summary.html:116 +msgid " to" +msgstr " à" + +#: templates/logs/aff_summary.html:119 #, python-format msgid "+%(duration)s months" msgstr "+%(duration)s mois" -#: templates/logs/aff_summary.html:132 +#: templates/logs/aff_summary.html:137 #, python-format msgid "%(username)s has edited an interface of" msgstr "%(username)s a modifié une interface de" @@ -149,7 +153,7 @@ msgstr "Confirmer" msgid "Statistics" msgstr "Statistiques" -#: templates/logs/index.html:32 templates/logs/stats_logs.html:32 views.py:403 +#: templates/logs/index.html:32 templates/logs/stats_logs.html:32 views.py:414 msgid "Actions performed" msgstr "Actions effectuées" @@ -173,7 +177,7 @@ msgstr "Base de données" msgid "Wiring actions" msgstr "Actions de câblage" -#: templates/logs/sidebar.html:53 views.py:325 +#: templates/logs/sidebar.html:53 views.py:336 msgid "Users" msgstr "Utilisateurs" @@ -189,150 +193,154 @@ msgstr "Statistiques sur la base de données" msgid "Statistics about users" msgstr "Statistiques sur les utilisateurs" -#: views.py:191 +#: views.py:194 msgid "Nonexistent revision." msgstr "Révision inexistante." -#: views.py:194 +#: views.py:197 msgid "The action was deleted." msgstr "L'action a été supprimée." -#: views.py:227 +#: views.py:230 msgid "Category" msgstr "Catégorie" -#: views.py:228 +#: views.py:231 msgid "Number of users (members and clubs)" msgstr "Nombre d'utilisateurs (adhérents et clubs)" -#: views.py:229 +#: views.py:232 msgid "Number of members" msgstr "Nombre d'adhérents" -#: views.py:230 +#: views.py:233 msgid "Number of clubs" msgstr "Nombre de clubs" -#: views.py:234 +#: views.py:237 msgid "Activated users" msgstr "Utilisateurs activés" -#: views.py:242 +#: views.py:245 msgid "Disabled users" msgstr "Utilisateurs désactivés" -#: views.py:250 +#: views.py:253 msgid "Archived users" msgstr "Utilisateurs archivés" -#: views.py:258 +#: views.py:261 +msgid "Not yet active users" +msgstr "Utilisateurs pas encore actifs" + +#: views.py:269 msgid "Contributing members" msgstr "Adhérents cotisants" -#: views.py:264 +#: views.py:275 msgid "Users benefiting from a connection" msgstr "Utilisateurs bénéficiant d'une connexion" -#: views.py:270 +#: views.py:281 msgid "Banned users" msgstr "Utilisateurs bannis" -#: views.py:276 +#: views.py:287 msgid "Users benefiting from a free connection" msgstr "Utilisateurs bénéficiant d'une connexion gratuite" -#: views.py:282 +#: views.py:293 msgid "Active interfaces (with access to the network)" msgstr "Interfaces actives (ayant accès au réseau)" -#: views.py:292 +#: views.py:303 msgid "Active interfaces assigned IPv4" msgstr "Interfaces actives assignées IPv4" -#: views.py:305 +#: views.py:316 msgid "IP range" msgstr "Plage d'IP" -#: views.py:306 +#: views.py:317 msgid "VLAN" msgstr "VLAN" -#: views.py:307 +#: views.py:318 msgid "Total number of IP addresses" msgstr "Nombre total d'adresses IP" -#: views.py:308 +#: views.py:319 msgid "Number of assigned IP addresses" msgstr "Nombre d'adresses IP non assignées" -#: views.py:309 +#: views.py:320 msgid "Number of IP address assigned to an activated machine" msgstr "Nombre d'adresses IP assignées à une machine activée" -#: views.py:310 +#: views.py:321 msgid "Number of nonassigned IP addresses" msgstr "Nombre d'adresses IP non assignées" -#: views.py:337 +#: views.py:348 msgid "Subscriptions" msgstr "Cotisations" -#: views.py:359 views.py:420 +#: views.py:370 views.py:431 msgid "Machines" msgstr "Machines" -#: views.py:386 +#: views.py:397 msgid "Topology" msgstr "Topologie" -#: views.py:405 +#: views.py:416 msgid "Number of actions" msgstr "Nombre d'actions" -#: views.py:419 views.py:437 views.py:442 views.py:447 views.py:462 +#: views.py:430 views.py:448 views.py:453 views.py:458 views.py:473 msgid "User" msgstr "Utilisateur" -#: views.py:423 +#: views.py:434 msgid "Invoice" msgstr "Facture" -#: views.py:426 +#: views.py:437 msgid "Ban" msgstr "Bannissement" -#: views.py:429 +#: views.py:440 msgid "Whitelist" msgstr "Accès gracieux" -#: views.py:432 +#: views.py:443 msgid "Rights" msgstr "Droits" -#: views.py:436 +#: views.py:447 msgid "School" msgstr "Établissement" -#: views.py:441 +#: views.py:452 msgid "Payment method" msgstr "Moyen de paiement" -#: views.py:446 +#: views.py:457 msgid "Bank" msgstr "Banque" -#: views.py:463 +#: views.py:474 msgid "Action" msgstr "Action" -#: views.py:494 +#: views.py:505 msgid "No model found." msgstr "Aucun modèle trouvé." -#: views.py:500 +#: views.py:511 msgid "Nonexistent entry." msgstr "Entrée inexistante." -#: views.py:507 +#: views.py:518 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_logs.html b/logs/templates/logs/aff_stats_logs.html index 1ca79df9..44a937f9 100644 --- a/logs/templates/logs/aff_stats_logs.html +++ b/logs/templates/logs/aff_stats_logs.html @@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% if revisions_list.paginator %} -{% include "pagination.html" with list=revisions_list %} + {% include 'pagination.html' with list=revisions_list %} {% endif %} {% load logs_extra %} @@ -36,9 +36,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Edited object" %} {% trans "Object type" %} {% trans "Edited by" as tr_edited_by %} - {% include "buttons/sort.html" with prefix='logs' col='author' text=tr_edited_by %} + {% include 'buttons/sort.html' with prefix='logs' col='author' text=tr_edited_by %} {% trans "Date of editing" as tr_date_of_editing %} - {% include "buttons/sort.html" with prefix='logs' col='date' text=tr_date_of_editing %} + {% include 'buttons/sort.html' with prefix='logs' col='date' text=tr_date_of_editing %} {% trans "Comment" %} @@ -65,6 +65,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if revisions_list.paginator %} -{% include "pagination.html" with list=revisions_list %} + {% include 'pagination.html' with list=revisions_list %} {% endif %} diff --git a/logs/templates/logs/aff_summary.html b/logs/templates/logs/aff_summary.html index 366e07e7..3c43e2ac 100644 --- a/logs/templates/logs/aff_summary.html +++ b/logs/templates/logs/aff_summary.html @@ -23,7 +23,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% if versions_list.paginator %} -{% include "pagination.html" with list=versions_list %} + {% include 'pagination.html' with list=versions_list %} {% endif %} {% load logs_extra %} @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "Date" as tr_date %} - {% include "buttons/sort.html" with prefix='sum' col='date' text=tr_date %} + {% include 'buttons/sort.html' with prefix='sum' col='date' text=tr_date %} {% trans "Editing" %} @@ -154,6 +154,6 @@ with this program; if not, write to the Free Software Foundation, Inc., {% if versions_list.paginator %} -{% include "pagination.html" with list=versions_list %} + {% include 'pagination.html' with list=versions_list %} {% endif %} diff --git a/logs/templates/logs/delete.html b/logs/templates/logs/delete.html index 6ad11195..a8f6b52f 100644 --- a/logs/templates/logs/delete.html +++ b/logs/templates/logs/delete.html @@ -1,4 +1,4 @@ -{% extends "logs/sidebar.html" %} +{% extends 'logs/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 diff --git a/logs/templates/logs/index.html b/logs/templates/logs/index.html index dde47c7d..3bd61b40 100644 --- a/logs/templates/logs/index.html +++ b/logs/templates/logs/index.html @@ -1,4 +1,4 @@ -{% extends "logs/sidebar.html" %} +{% extends 'logs/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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

{% trans "Actions performed" %}

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


diff --git a/logs/templates/logs/sidebar.html b/logs/templates/logs/sidebar.html index 87011cfc..e997abd5 100644 --- a/logs/templates/logs/sidebar.html +++ b/logs/templates/logs/sidebar.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.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 @@ -28,27 +28,27 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block sidebar %} {% can_view_app logs %} - - + + {% trans "Summary" %} - - + + {% trans "Events" %} - - + + {% trans "General" %} - + {% trans "Database" %} - + {% trans "Wiring actions" %} - + {% trans "Users" %} diff --git a/logs/templates/logs/stats_general.html b/logs/templates/logs/stats_general.html index 07e3ec26..96d5612c 100644 --- a/logs/templates/logs/stats_general.html +++ b/logs/templates/logs/stats_general.html @@ -1,4 +1,4 @@ -{% extends "logs/sidebar.html" %} +{% extends 'logs/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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

{% trans "General statistics" %}

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


diff --git a/logs/templates/logs/stats_logs.html b/logs/templates/logs/stats_logs.html index 4f547cc3..df9708b1 100644 --- a/logs/templates/logs/stats_logs.html +++ b/logs/templates/logs/stats_logs.html @@ -1,4 +1,4 @@ -{% extends "logs/sidebar.html" %} +{% extends 'logs/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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

{% trans "Actions performed" %}

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


diff --git a/logs/templates/logs/stats_models.html b/logs/templates/logs/stats_models.html index 9b912da2..ddc66c15 100644 --- a/logs/templates/logs/stats_models.html +++ b/logs/templates/logs/stats_models.html @@ -1,4 +1,4 @@ -{% extends "logs/sidebar.html" %} +{% extends 'logs/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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

{% trans "Database statistics" %}

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


diff --git a/logs/templates/logs/stats_users.html b/logs/templates/logs/stats_users.html index 8cc645ab..d55d1e52 100644 --- a/logs/templates/logs/stats_users.html +++ b/logs/templates/logs/stats_users.html @@ -1,4 +1,4 @@ -{% extends "logs/sidebar.html" %} +{% extends 'logs/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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

{% trans "Statistics about users" %}

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


diff --git a/logs/views.py b/logs/views.py index a9fe5418..a54edd56 100644 --- a/logs/views.py +++ b/logs/views.py @@ -102,15 +102,18 @@ from re2o.utils import ( all_baned, all_has_access, all_adherent, + all_active_assigned_interfaces_count, + all_active_interfaces_count, +) +from re2o.base import ( re2o_paginator, + SortTable ) from re2o.acl import ( can_view_all, can_view_app, can_edit_history, ) -from re2o.utils import all_active_assigned_interfaces_count -from re2o.utils import all_active_interfaces_count, SortTable @login_required @@ -254,6 +257,14 @@ def stats_general(request): .count()), Club.objects.filter(state=Club.STATE_ARCHIVE).count() ], + 'not_active_users': [ + _("Not yet active users"), + User.objects.filter(state=User.STATE_NOT_YET_ACTIVE).count(), + (Adherent.objects + .filter(state=Adherent.STATE_NOT_YET_ACTIVE) + .count()), + Club.objects.filter(state=Club.STATE_NOT_YET_ACTIVE).count() + ], 'adherent_users': [ _("Contributing members"), _all_adherent.count(), diff --git a/machines/acl.py b/machines/acl.py index 45cb6ec2..53f70c27 100644 --- a/machines/acl.py +++ b/machines/acl.py @@ -41,4 +41,3 @@ def can_view(user): can = user.has_module_perms('machines') 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 af721ff9..bafebb80 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -29,7 +29,6 @@ from __future__ import unicode_literals from django.contrib import admin from reversion.admin import VersionAdmin -from .models import IpType, Machine, MachineType, Domain, IpList, Interface from .models import ( Extension, SOA, @@ -47,6 +46,7 @@ from .models import ( Ipv6List, OuverturePortList, ) +from .models import IpType, Machine, MachineType, Domain, IpList, Interface class MachineAdmin(VersionAdmin): @@ -98,6 +98,7 @@ class TxtAdmin(VersionAdmin): """ Admin view of a TXT object """ pass + class DNameAdmin(VersionAdmin): """ Admin view of a DName object """ pass @@ -147,12 +148,12 @@ 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) admin.site.register(IpType, IpTypeAdmin) diff --git a/machines/forms.py b/machines/forms.py index 4af060d3..94b9293a 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -35,13 +35,12 @@ Formulaires d'ajout, edition et suppressions de : from __future__ import unicode_literals -from django.forms import ModelForm, Form from django import forms +from django.forms import ModelForm, Form from django.utils.translation import ugettext_lazy as _ from re2o.field_permissions import FieldPermissionFormMixin from re2o.mixins import FormRevMixin - from .models import ( Domain, Machine, @@ -68,6 +67,7 @@ from .models import ( class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Formulaire d'édition d'une machine""" + class Meta: model = Machine fields = '__all__' @@ -80,12 +80,14 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class NewMachineForm(EditMachineForm): """Creation d'une machine, ne renseigne que le nom""" + class Meta(EditMachineForm.Meta): fields = ['name'] class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Edition d'une interface. Edition complète""" + class Meta: model = Interface fields = ['machine', 'type', 'ipv4', 'mac_address', 'details'] @@ -116,7 +118,7 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): interface=self.instance ) if "machine" in self.fields: - self.fields['machine'].queryset = Machine.objects.all()\ + self.fields['machine'].queryset = Machine.objects.all() \ .select_related('user') can_use_all_machinetype, _reason = MachineType.can_use_all(user) if not can_use_all_machinetype: @@ -128,12 +130,14 @@ class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class AddInterfaceForm(EditInterfaceForm): """Ajout d'une interface à une machine. En fonction des droits, affiche ou non l'ensemble des ip disponibles""" + class Meta(EditInterfaceForm.Meta): fields = ['type', 'ipv4', 'mac_address', 'details'] class AliasForm(FormRevMixin, ModelForm): """Ajout d'un alias (et edition), CNAME, contenant nom et extension""" + class Meta: model = Domain fields = ['name', 'extension'] @@ -151,6 +155,7 @@ class AliasForm(FormRevMixin, ModelForm): class DomainForm(FormRevMixin, ModelForm): """Ajout et edition d'un enregistrement de nom, relié à interface""" + class Meta: model = Domain fields = ['name'] @@ -183,6 +188,7 @@ class DelAliasForm(FormRevMixin, Form): class MachineTypeForm(FormRevMixin, ModelForm): """Ajout et edition d'un machinetype, relié à un iptype""" + class Meta: model = MachineType fields = ['type', 'ip_type'] @@ -214,6 +220,7 @@ class DelMachineTypeForm(FormRevMixin, Form): class IpTypeForm(FormRevMixin, ModelForm): """Formulaire d'ajout d'un iptype. Pas d'edition de l'ip de start et de stop après creation""" + class Meta: model = IpType fields = '__all__' @@ -227,6 +234,7 @@ class IpTypeForm(FormRevMixin, ModelForm): 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', 'domaine_ip_network', 'domaine_ip_netmask', 'prefix_v6', 'prefix_v6_length', @@ -253,6 +261,7 @@ class DelIpTypeForm(FormRevMixin, Form): class ExtensionForm(FormRevMixin, ModelForm): """Formulaire d'ajout et edition d'une extension""" + class Meta: model = Extension fields = '__all__' @@ -264,6 +273,7 @@ class ExtensionForm(FormRevMixin, ModelForm): self.fields['origin'].label = _("A record origin") self.fields['origin_v6'].label = _("AAAA record origin") self.fields['soa'].label = _("SOA record to use") + self.fields['dnssec'].label = _("Sign with DNSSEC") class DelExtensionForm(FormRevMixin, Form): @@ -285,6 +295,7 @@ class DelExtensionForm(FormRevMixin, Form): class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): """Gestion des ipv6 d'une machine""" + class Meta: model = Ipv6List fields = ['ipv6', 'slaac_ip'] @@ -296,6 +307,7 @@ class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class SOAForm(FormRevMixin, ModelForm): """Ajout et edition d'un SOA""" + class Meta: model = SOA fields = '__all__' @@ -324,6 +336,7 @@ class DelSOAForm(FormRevMixin, Form): class MxForm(FormRevMixin, ModelForm): """Ajout et edition d'un MX""" + class Meta: model = Mx fields = ['zone', 'priority', 'name'] @@ -357,6 +370,7 @@ class NsForm(FormRevMixin, ModelForm): """Ajout d'un NS pour une zone On exclue les CNAME dans les objets domain (interdit par la rfc) donc on prend uniquemet """ + class Meta: model = Ns fields = ['zone', 'ns'] @@ -388,6 +402,7 @@ class DelNsForm(FormRevMixin, Form): class TxtForm(FormRevMixin, ModelForm): """Ajout d'un txt pour une zone""" + class Meta: model = Txt fields = '__all__' @@ -416,6 +431,7 @@ class DelTxtForm(FormRevMixin, Form): class DNameForm(FormRevMixin, ModelForm): """Add a DNAME entry for a zone""" + class Meta: model = DName fields = '__all__' @@ -444,6 +460,7 @@ class DelDNameForm(FormRevMixin, Form): class SrvForm(FormRevMixin, ModelForm): """Ajout d'un srv pour une zone""" + class Meta: model = Srv fields = '__all__' @@ -473,6 +490,7 @@ class DelSrvForm(FormRevMixin, Form): class NasForm(FormRevMixin, ModelForm): """Ajout d'un type de nas (machine d'authentification, swicths, bornes...)""" + class Meta: model = Nas fields = '__all__' @@ -501,6 +519,7 @@ class DelNasForm(FormRevMixin, Form): class RoleForm(FormRevMixin, ModelForm): """Add and edit role.""" + class Meta: model = Role fields = '__all__' @@ -509,9 +528,9 @@ class RoleForm(FormRevMixin, ModelForm): 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' - )) + .select_related( + 'domain__extension' + )) class DelRoleForm(FormRevMixin, Form): @@ -533,6 +552,7 @@ class DelRoleForm(FormRevMixin, Form): class ServiceForm(FormRevMixin, ModelForm): """Ajout et edition d'une classe de service : dns, dhcp, etc""" + class Meta: model = Service fields = '__all__' @@ -541,9 +561,9 @@ class ServiceForm(FormRevMixin, ModelForm): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(ServiceForm, self).__init__(*args, prefix=prefix, **kwargs) self.fields['servers'].queryset = (Interface.objects.all() - .select_related( - 'domain__extension' - )) + .select_related( + 'domain__extension' + )) def save(self, commit=True): # TODO : None of the parents of ServiceForm use the commit @@ -574,15 +594,27 @@ class DelServiceForm(FormRevMixin, Form): class VlanForm(FormRevMixin, ModelForm): """Ajout d'un vlan : id, nom""" + class Meta: model = Vlan - fields = '__all__' + fields = ['vlan_id', 'name', 'comment'] def __init__(self, *args, **kwargs): prefix = kwargs.pop('prefix', self.Meta.model.__name__) super(VlanForm, self).__init__(*args, prefix=prefix, **kwargs) +class EditOptionVlanForm(FormRevMixin, ModelForm): + """Ajout d'un vlan : id, nom""" + class Meta: + model = Vlan + fields = ['dhcp_snooping', 'dhcpv6_snooping', 'arp_protect', 'igmp', 'mld'] + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(EditOptionVlanForm, self).__init__(*args, prefix=prefix, **kwargs) + + class DelVlanForm(FormRevMixin, Form): """Suppression d'un ou plusieurs vlans""" vlan = forms.ModelMultipleChoiceField( @@ -603,6 +635,7 @@ class DelVlanForm(FormRevMixin, Form): class EditOuverturePortConfigForm(FormRevMixin, ModelForm): """Edition de la liste des profils d'ouverture de ports pour l'interface""" + class Meta: model = Interface fields = ['port_lists'] @@ -619,6 +652,7 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm): class EditOuverturePortListForm(FormRevMixin, ModelForm): """Edition de la liste des ports et profils d'ouverture des ports""" + class Meta: model = OuverturePortList fields = '__all__' @@ -631,9 +665,10 @@ class EditOuverturePortListForm(FormRevMixin, ModelForm): **kwargs ) - + class SshFpForm(FormRevMixin, ModelForm): """Edits a SSHFP record.""" + class Meta: model = SshFp exclude = ('machine',) @@ -645,4 +680,3 @@ 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 deleted file mode 100644 index c9696d92..00000000 Binary files a/machines/locale/fr/LC_MESSAGES/django.mo and /dev/null differ diff --git a/machines/locale/fr/LC_MESSAGES/django.po b/machines/locale/fr/LC_MESSAGES/django.po index 50ab03a8..bd36fb61 100644 --- a/machines/locale/fr/LC_MESSAGES/django.po +++ b/machines/locale/fr/LC_MESSAGES/django.po @@ -21,7 +21,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-15 18:10+0200\n" +"POT-Creation-Date: 2019-01-12 16:43+0100\n" "PO-Revision-Date: 2018-06-23 16:35+0200\n" "Last-Translator: Laouen Fernet \n" "Language-Team: \n" @@ -38,220 +38,232 @@ msgstr "Vous n'avez pas le droit de voir cette application." msgid "Machine name" msgstr "Nom de la machine" -#: forms.py:97 templates/machines/aff_machines.html:46 +#: forms.py:99 templates/machines/aff_machines.html:46 msgid "MAC address" msgstr "Adresse MAC" -#: forms.py:98 templates/machines/aff_machinetype.html:32 +#: forms.py:100 templates/machines/aff_machinetype.html:32 #: templates/machines/machine.html:112 msgid "Machine type" msgstr "Type de machine" -#: forms.py:99 +#: forms.py:101 msgid "Select a machine type" msgstr "Sélectionnez un type de machine" -#: forms.py:101 +#: forms.py:103 msgid "Automatic IPv4 assignment" msgstr "Assignation automatique IPv4" -#: forms.py:172 +#: forms.py:177 msgid "Current aliases" msgstr "Alias actuels" -#: forms.py:193 +#: forms.py:199 msgid "Machine type to add" msgstr "Type de machine à ajouter" -#: forms.py:194 +#: forms.py:200 msgid "Related IP type" msgstr "Type d'IP relié" -#: forms.py:201 +#: forms.py:207 msgid "Current machine types" msgstr "Types de machines actuels" -#: forms.py:224 +#: forms.py:231 msgid "IP type to add" msgstr "Type d'IP à ajouter" -#: forms.py:241 +#: forms.py:249 msgid "Current IP types" msgstr "Types d'IP actuels" -#: forms.py:263 +#: forms.py:272 msgid "Extension to add" msgstr "Extension à ajouter" -#: forms.py:264 templates/machines/aff_extension.html:37 +#: forms.py:273 templates/machines/aff_extension.html:37 msgid "A record origin" msgstr "Enregistrement A origin" -#: forms.py:265 templates/machines/aff_extension.html:39 +#: forms.py:274 templates/machines/aff_extension.html:39 msgid "AAAA record origin" msgstr "Enregistrement AAAA origin" -#: forms.py:266 +#: forms.py:275 msgid "SOA record to use" msgstr "Enregistrement SOA à utiliser" -#: forms.py:273 +#: forms.py:276 +msgid "Sign with DNSSEC" +msgstr "Signer avec DNSSEC" + +#: forms.py:283 msgid "Current extensions" msgstr "Extensions actuelles" -#: forms.py:312 +#: forms.py:324 msgid "Current SOA records" msgstr "Enregistrements SOA actuels" -#: forms.py:343 +#: forms.py:356 msgid "Current MX records" msgstr "Enregistrements MX actuels" -#: forms.py:376 +#: forms.py:390 msgid "Current NS records" msgstr "Enregistrements NS actuels" -#: forms.py:404 +#: forms.py:419 msgid "Current TXT records" msgstr "Enregistrements TXT actuels" -#: forms.py:432 +#: forms.py:448 msgid "Current DNAME records" msgstr "Enregistrements DNAME actuels" -#: forms.py:460 +#: forms.py:477 msgid "Current SRV records" msgstr "Enregistrements SRV actuels" -#: forms.py:489 +#: forms.py:507 msgid "Current NAS devices" msgstr "Dispositifs NAS actuels" -#: forms.py:521 +#: forms.py:540 msgid "Current roles" msgstr "Rôles actuels" -#: forms.py:562 +#: forms.py:582 msgid "Current services" msgstr "Services actuels" -#: forms.py:590 +#: forms.py:622 msgid "Current VLANs" msgstr "VLANs actuels" -#: models.py:63 +#: models.py:61 msgid "Optional" msgstr "Optionnel" -#: models.py:71 +#: models.py:69 msgid "Can view a machine object" msgstr "Peut voir un objet machine" -#: models.py:73 +#: models.py:71 msgid "Can change the user of a machine" msgstr "Peut changer l'utilisateur d'une machine" -#: models.py:75 +#: models.py:73 msgid "machine" msgstr "machine" -#: models.py:76 +#: models.py:74 msgid "machines" msgstr "machines" -#: models.py:109 +#: models.py:107 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 +#: models.py:116 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 +#: models.py:130 msgid "Nonexistent user." msgstr "Utilisateur inexistant." -#: models.py:140 +#: models.py:138 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 +#: models.py:140 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 +#: models.py:143 models.py:1182 #, 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)." +"même (%s)." -#: models.py:164 models.py:1177 models.py:1194 models.py:1296 models.py:1313 +#: models.py:162 models.py:1207 models.py:1224 models.py:1326 models.py:1343 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 +#: models.py:180 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 +#: models.py:192 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 +#: models.py:204 templates/machines/aff_machines.html:53 +msgid "No name" +msgstr "Sans nom" + +#: models.py:254 msgid "Can view a machine type object" msgstr "Peut voir un objet type de machine" -#: models.py:242 +#: models.py:255 msgid "Can use all machine types" msgstr "Peut utiliser tous les types de machine" -#: models.py:244 +#: models.py:257 msgid "machine type" msgstr "type de machine" -#: models.py:245 +#: models.py:258 msgid "machine types" msgstr "types de machine" -#: models.py:263 +#: models.py:276 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 +#: models.py:295 msgid "Network containing the domain's IPv4 range (optional)" msgstr "Réseau contenant la plage IPv4 du domaine (optionnel)" -#: models.py:290 +#: models.py:303 msgid "Netmask for the domain's IPv4 range" msgstr "Masque de sous-réseau pour la plage IPv4 du domaine" -#: models.py:294 +#: models.py:307 msgid "Enable reverse DNS for IPv4" msgstr "Activer DNS inverse pour IPv4" -#: models.py:310 +#: models.py:323 msgid "Enable reverse DNS for IPv6" -msgstr "Activer DNS inverser pour IPv6" +msgstr "Activer DNS inverse pour IPv6" -#: models.py:326 +#: models.py:339 msgid "Can view an IP type object" msgstr "Peut voir un objet type d'IP" -#: models.py:327 +#: models.py:340 msgid "Can use all IP types" msgstr "Peut utiliser tous les types d'IP" -#: models.py:329 templates/machines/aff_iptype.html:35 +#: models.py:342 templates/machines/aff_iptype.html:35 #: templates/machines/machine.html:108 msgid "IP type" msgstr "type d'IP" -#: models.py:433 +#: models.py:343 +msgid "IP types" +msgstr "types d'IP" + +#: models.py:446 msgid "" "One or several IP addresses from the range are affected, impossible to " "delete the range." @@ -259,57 +271,69 @@ msgstr "" "Une ou plusieurs adresses IP de la plage sont affectées, impossible de " "supprimer la plage." -#: models.py:475 +#: models.py:488 msgid "Range end must be after range start..." msgstr "La fin de la plage doit être après le début..." -#: models.py:478 +#: models.py:491 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 +#: models.py:496 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 +#: models.py:504 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." +"Si vous renseignez un réseau ou masque de sous-réseau, il doit contenir la " +"plage IP du domaine." -#: models.py:521 +#: models.py:537 +msgid "v4 multicast management" +msgstr "gestion de multidiffusion v4" + +#: models.py:541 +msgid "v6 multicast management" +msgstr "gestion de multidiffusion v6" + +#: models.py:546 msgid "Can view a VLAN object" msgstr "Peut voir un objet VLAN" -#: models.py:523 templates/machines/machine.html:160 +#: models.py:548 templates/machines/machine.html:160 msgid "VLAN" msgstr "VLAN" -#: models.py:524 templates/machines/sidebar.html:57 +#: models.py:549 templates/machines/sidebar.html:57 msgid "VLANs" msgstr "VLANs" -#: models.py:560 +#: models.py:562 +msgid "MAC-address" +msgstr "MAC-address" + +#: models.py:585 msgid "Can view a NAS device object" msgstr "Peut voir un objet dispositif NAS" -#: models.py:562 templates/machines/machine.html:164 +#: models.py:587 templates/machines/machine.html:164 msgid "NAS device" msgstr "dispositif NAS" -#: models.py:563 templates/machines/sidebar.html:63 +#: models.py:588 templates/machines/sidebar.html:63 msgid "NAS devices" msgstr "dispositifs NAS" -#: models.py:577 +#: models.py:602 msgid "Contact email address for the zone" msgstr "Adresse mail de contact pour la zone" -#: models.py:581 +#: models.py:606 msgid "" "Seconds before the secondary DNS have to ask the primary DNS serial to " "detect a modification" @@ -317,7 +341,7 @@ msgstr "" "Secondes avant que le DNS secondaire demande au DNS primaire le serial pour " "détecter une modification" -#: models.py:586 +#: models.py:611 msgid "" "Seconds before the secondary DNS ask the serial again in case of a primary " "DNS timeout" @@ -325,7 +349,7 @@ 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 +#: models.py:616 msgid "" "Seconds before the secondary DNS stop answering requests in case of primary " "DNS timeout" @@ -333,108 +357,112 @@ 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 +#: models.py:621 models.py:878 msgid "Time to Live" msgstr "Temps de vie" -#: models.py:601 +#: models.py:626 msgid "Can view an SOA record object" msgstr "Peut voir un objet enregistrement SOA" -#: models.py:603 templates/machines/aff_extension.html:36 +#: models.py:628 templates/machines/aff_extension.html:36 #: templates/machines/machine.html:120 msgid "SOA record" msgstr "enregistrement SOA" -#: models.py:604 +#: models.py:629 msgid "SOA records" msgstr "enregistrements SOA" -#: models.py:643 +#: models.py:668 msgid "SOA to edit" msgstr "SOA à modifier" -#: models.py:654 +#: models.py:679 msgid "Zone name, must begin with a dot (.example.org)" msgstr "Nom de zone, doit commencer par un point (.example.org)" -#: models.py:662 +#: models.py:687 msgid "A record associated with the zone" msgstr "Enregistrement A associé à la zone" -#: models.py:668 +#: models.py:693 msgid "AAAA record associated with the zone" msgstr "Enregristrement AAAA associé avec la zone" -#: models.py:677 +#: models.py:701 +msgid "Should the zone be signed with DNSSEC" +msgstr "La zone doit-elle être signée avec DNSSEC" + +#: models.py:706 msgid "Can view an extension object" msgstr "Peut voir un objet extension" -#: models.py:678 +#: models.py:707 msgid "Can use all extensions" msgstr "Peut utiliser toutes les extensions" -#: models.py:680 +#: models.py:709 msgid "DNS extension" msgstr "extension DNS" -#: models.py:681 +#: models.py:710 msgid "DNS extensions" msgstr "extensions DNS" -#: models.py:732 +#: models.py:764 msgid "An extension must begin with a dot." msgstr "Une extension doit commencer par un point." -#: models.py:746 +#: models.py:778 msgid "Can view an MX record object" msgstr "Peut voir un objet enregistrement MX" -#: models.py:748 templates/machines/machine.html:124 +#: models.py:780 templates/machines/machine.html:124 msgid "MX record" msgstr "enregistrement MX" -#: models.py:749 +#: models.py:781 msgid "MX records" msgstr "enregistrements MX" -#: models.py:771 +#: models.py:803 msgid "Can view an NS record object" msgstr "Peut voir un objet enregistrement NS" -#: models.py:773 templates/machines/machine.html:128 +#: models.py:805 templates/machines/machine.html:128 msgid "NS record" msgstr "enregistrement NS" -#: models.py:774 +#: models.py:806 msgid "NS records" msgstr "enregistrements NS" -#: models.py:793 +#: models.py:825 msgid "Can view a TXT record object" msgstr "Peut voir un objet enregistrement TXT" -#: models.py:795 templates/machines/machine.html:132 +#: models.py:827 templates/machines/machine.html:132 msgid "TXT record" msgstr "enregistrement TXT" -#: models.py:796 +#: models.py:828 msgid "TXT records" msgstr "enregistrements TXT" -#: models.py:815 +#: models.py:847 msgid "Can view a DNAME record object" msgstr "Peut voir un objet enregistrement DNAME" -#: models.py:817 templates/machines/machine.html:136 +#: models.py:849 templates/machines/machine.html:136 msgid "DNAME record" msgstr "enregistrement DNAME" -#: models.py:818 +#: models.py:850 msgid "DNAME records" msgstr "enregistrements DNAME" -#: models.py:851 +#: models.py:883 msgid "" "Priority of the target server (positive integer value, the lower it is, the " "more the server will be used if available)" @@ -442,7 +470,7 @@ msgstr "" "Priorité du serveur cible (entier positif, plus il est bas, plus le serveur " "sera utilisé si disponible)" -#: models.py:858 +#: models.py:890 msgid "" "Relative weight for records with the same priority (integer value between 0 " "and 65535)" @@ -450,128 +478,137 @@ msgstr "" "Poids relatif des enregistrements avec la même priorité (entier entre 0 et " "65535)" -#: models.py:863 +#: models.py:895 msgid "TCP/UDP port" msgstr "Port TCP/UDP" -#: models.py:868 +#: models.py:900 msgid "Target server" msgstr "Serveur cible" -#: models.py:873 +#: models.py:905 msgid "Can view an SRV record object" msgstr "Peut voir un objet enregistrement SRV" -#: models.py:875 templates/machines/machine.html:140 +#: models.py:907 templates/machines/machine.html:140 msgid "SRV record" msgstr "enregistrement SRV" -#: models.py:876 +#: models.py:908 msgid "SRV records" msgstr "enregistrements SRV" -#: models.py:940 +#: models.py:937 templates/machines/aff_sshfp.html:31 +msgid "SSH public key" +msgstr "Clé publique SSH" + +#: models.py:945 templates/machines/aff_sshfp.html:33 +#: templates/machines/aff_vlan.html:35 +msgid "Comment" +msgstr "Commentaire" + +#: models.py:972 msgid "Can view an SSHFP record object" msgstr "Peut voir un objet enregistrement SSHFP" -#: models.py:942 templates/machines/machine.html:144 +#: models.py:974 templates/machines/machine.html:144 msgid "SSHFP record" msgstr "enregistrement SSHFP" -#: models.py:943 +#: models.py:975 msgid "SSHFP records" msgstr "enregistrements SSHFP" -#: models.py:981 +#: models.py:1012 msgid "Can view an interface object" msgstr "Peut voir un objet interface" -#: models.py:983 +#: models.py:1014 msgid "Can change the owner of an interface" msgstr "Peut changer l'utilisateur d'une interface" -#: models.py:985 +#: models.py:1016 msgid "interface" msgstr "interface" -#: models.py:986 +#: models.py:1017 msgid "interfaces" msgstr "interfaces" -#: models.py:1080 +#: models.py:1111 msgid "The given MAC address is invalid." msgstr "L'adresse MAC indiquée est invalide." -#: models.py:1093 +#: models.py:1124 msgid "The selected IP type is invalid." msgstr "Le type d'IP sélectionné est invalide." -#: models.py:1106 +#: models.py:1136 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 +#: models.py:1154 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 +#: models.py:1168 msgid "Nonexistent machine." msgstr "Machine inexistante." -#: models.py:1142 +#: models.py:1172 msgid "You can't add a machine." msgstr "Vous ne pouvez pas ajouter une machine." -#: models.py:1148 +#: models.py:1178 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 +#: models.py:1192 msgid "Permission required to edit the machine." msgstr "Permission requise pour modifier la machine." -#: models.py:1206 models.py:1325 models.py:1532 +#: models.py:1236 models.py:1355 models.py:1565 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 +#: models.py:1282 msgid "Can view an IPv6 addresses list object" msgstr "Peut voir un objet list d'adresses IPv6" -#: models.py:1253 +#: models.py:1283 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 +#: models.py:1286 msgid "IPv6 addresses list" msgstr "Liste d'adresses IPv6" -#: models.py:1257 +#: models.py:1287 msgid "IPv6 addresses lists" msgstr "Listes d'adresses IPv6" -#: models.py:1269 models.py:1480 +#: models.py:1299 models.py:1513 msgid "Nonexistent interface." msgstr "Interface inexistante." -#: models.py:1272 models.py:1487 +#: models.py:1302 models.py:1520 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 +#: models.py:1310 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 +#: models.py:1382 msgid "A SLAAC IP address is already registered." msgstr "Une adresse IP SLAAC est déjà enregistrée." -#: models.py:1357 +#: models.py:1390 msgid "" "The v6 prefix is incorrect and doesn't match the type associated with the " "machine." @@ -579,207 +616,207 @@ msgstr "" "Le préfixe v6 est incorrect et ne correspond pas au type associé à la " "machine." -#: models.py:1383 +#: models.py:1416 msgid "Mandatory and unique, must not contain dots." msgstr "Obligatoire et unique, ne doit pas contenir de points." -#: models.py:1397 +#: models.py:1430 msgid "Can view a domain object" msgstr "Peut voir un objet domaine" -#: models.py:1399 +#: models.py:1432 msgid "domain" msgstr "domaine" -#: models.py:1400 +#: models.py:1433 msgid "domains" msgstr "domaines" -#: models.py:1422 +#: models.py:1455 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 +#: models.py:1458 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 +#: models.py:1466 #, 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 +#: models.py:1469 #, python-format msgid "The domain name %s contains forbidden characters." msgstr "Le nom de domaine %s contient des caractères interdits." -#: models.py:1454 +#: models.py:1487 msgid "Invalid extension." msgstr "Extension invalide." -#: models.py:1495 +#: models.py:1528 #, 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 " +"Vous avez atteint le nombre maximal d'alias que vous pouvez créer vous-même " "(%s)." -#: models.py:1508 +#: models.py:1541 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 +#: models.py:1553 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 +#: models.py:1581 msgid "Can view an IPv4 addresses list object" msgstr "Peut voir un object liste d'adresses IPv4" -#: models.py:1550 +#: models.py:1583 msgid "IPv4 addresses list" msgstr "Liste d'adresses IPv4" -#: models.py:1551 +#: models.py:1584 msgid "IPv4 addresses lists" msgstr "Listes d'adresses IPv4" -#: models.py:1562 +#: models.py:1595 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 +#: models.py:1613 msgid "DHCP server" msgstr "Serveur DHCP" -#: models.py:1581 +#: models.py:1614 msgid "Switches configuration server" msgstr "Serveur de configuration des commutateurs réseau" -#: models.py:1582 +#: models.py:1615 msgid "Recursive DNS server" msgstr "Serveur DNS récursif" -#: models.py:1583 +#: models.py:1616 msgid "NTP server" msgstr "Serveur NTP" -#: models.py:1584 +#: models.py:1617 msgid "RADIUS server" msgstr "Serveur RADIUS" -#: models.py:1585 +#: models.py:1618 msgid "Log server" msgstr "Serveur log" -#: models.py:1586 +#: models.py:1619 msgid "LDAP master server" msgstr "Serveur LDAP maître" -#: models.py:1587 +#: models.py:1620 msgid "LDAP backup server" msgstr "Serveur LDAP de secours" -#: models.py:1588 +#: models.py:1621 msgid "SMTP server" msgstr "Serveur SMTP" -#: models.py:1589 +#: models.py:1622 msgid "postgreSQL server" msgstr "Serveur postgreSQL" -#: models.py:1590 +#: models.py:1623 msgid "mySQL server" msgstr "Serveur mySQL" -#: models.py:1591 +#: models.py:1624 msgid "SQL client" msgstr "Client SQL" -#: models.py:1592 +#: models.py:1625 msgid "Gateway" msgstr "Passerelle" -#: models.py:1606 +#: models.py:1639 msgid "Can view a role object" msgstr "Peut voir un objet rôle" -#: models.py:1608 +#: models.py:1641 msgid "server role" msgstr "rôle de serveur" -#: models.py:1609 +#: models.py:1642 msgid "server roles" msgstr "rôles de serveur" -#: models.py:1650 +#: models.py:1676 msgid "Minimal time before regeneration of the service." msgstr "Temps minimal avant régénération du service." -#: models.py:1654 +#: models.py:1680 msgid "Maximal time before regeneration of the service." msgstr "Temps maximal avant régénération du service." -#: models.py:1660 +#: models.py:1686 msgid "Can view a service object" msgstr "Peut voir un objet service" -#: models.py:1662 +#: models.py:1688 msgid "service to generate (DHCP, DNS, ...)" msgstr "service à générer (DHCP, DNS, ...)" -#: models.py:1663 +#: models.py:1689 msgid "services to generate (DHCP, DNS, ...)" msgstr "services à générer (DHCP, DNS, ...)" -#: models.py:1709 +#: models.py:1735 msgid "Can view a service server link object" msgstr "Peut voir un objet lien service serveur" -#: models.py:1711 +#: models.py:1737 msgid "link between service and server" msgstr "lien entre service et serveur" -#: models.py:1712 +#: models.py:1738 msgid "links between service and server" msgstr "liens entre service et serveur" -#: models.py:1754 +#: models.py:1780 msgid "Name of the ports configuration" msgstr "Nom de la configuration de ports" -#: models.py:1760 +#: models.py:1786 msgid "Can view a ports opening list object" msgstr "Peut voir un objet liste d'ouverture de ports" -#: models.py:1763 +#: models.py:1789 msgid "ports opening list" msgstr "liste d'ouverture de ports" -#: models.py:1764 +#: models.py:1790 msgid "ports opening lists" msgstr "listes d'ouverture de ports" -#: models.py:1773 +#: models.py:1799 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 +#: models.py:1802 msgid "This ports opening list is used." msgstr "Cette liste d'ouverture de ports est utilisée." -#: models.py:1849 +#: models.py:1875 msgid "ports opening" msgstr "ouverture de ports" -#: models.py:1850 +#: models.py:1876 msgid "ports openings" msgstr "ouvertures de ports" @@ -807,6 +844,10 @@ msgstr "Extension" msgid "'infra' right required" msgstr "droit 'infra' requis" +#: templates/machines/aff_extension.html:41 +msgid "DNSSEC" +msgstr "DNSSEC" + #: templates/machines/aff_iptype.html:38 msgid "IPv4 range" msgstr "Plage IPv4" @@ -851,43 +892,39 @@ msgstr "Adresse IP" 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 +#: templates/machines/aff_machines.html:62 views.py:374 msgid "Create an interface" msgstr "Créer une interface" -#: templates/machines/aff_machines.html:77 +#: templates/machines/aff_machines.html:79 msgid "Display the aliases" msgstr "Afficher les alias" -#: templates/machines/aff_machines.html:95 +#: templates/machines/aff_machines.html:99 msgid "Display the IPv6 address" msgstr "Afficher les adresses IPv6" -#: templates/machines/aff_machines.html:110 +#: templates/machines/aff_machines.html:116 msgid " Edit" msgstr " Modifier" -#: templates/machines/aff_machines.html:118 +#: templates/machines/aff_machines.html:124 msgid " Manage the aliases" msgstr " Gérer les alias" -#: templates/machines/aff_machines.html:126 +#: templates/machines/aff_machines.html:132 msgid " Manage the IPv6 addresses" msgstr " Gérer les adresses IPv6" -#: templates/machines/aff_machines.html:134 +#: templates/machines/aff_machines.html:140 msgid " Manage the SSH fingerprints" msgstr " Gérer les empreintes SSH" -#: templates/machines/aff_machines.html:142 +#: templates/machines/aff_machines.html:148 msgid " Manage the ports configuration" msgstr " Gérer les configuration de ports" @@ -906,7 +943,7 @@ 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 +#: templates/machines/index_portlist.html:20 msgid "Name" msgstr "Nom" @@ -1019,18 +1056,10 @@ msgstr "Port" 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" @@ -1040,8 +1069,8 @@ 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" +msgid "Deletion of machines" +msgstr "Suppression de machines" #: templates/machines/delete.html:35 #, python-format @@ -1058,16 +1087,16 @@ 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_extension.html:30 +#: templates/machines/index_iptype.html:30 #: 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_portlist.html:25 #: 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/index_sshfp.html:28 templates/machines/index_vlan.html:30 #: templates/machines/machine.html:31 templates/machines/sidebar.html:33 msgid "Machines" msgstr "Machines" @@ -1084,99 +1113,99 @@ msgstr "Créer ou modifier" msgid "List of the aliases of the interface" msgstr "Liste des alias de l'interface" -#: templates/machines/index_alias.html:33 +#: templates/machines/index_alias.html:34 msgid " Add an alias" msgstr " Ajouter un alias" -#: templates/machines/index_alias.html:34 +#: templates/machines/index_alias.html:36 msgid " Delete one or several aliases" msgstr " Supprimer un ou plusieurs alias" -#: templates/machines/index_extension.html:34 +#: templates/machines/index_extension.html:33 msgid "List of extensions" msgstr "Liste des extensions" -#: templates/machines/index_extension.html:36 +#: templates/machines/index_extension.html:37 msgid " Add an extension" msgstr " Ajouter une extension" -#: templates/machines/index_extension.html:38 +#: templates/machines/index_extension.html:40 msgid " Delete one or several extensions" msgstr " Supprimer une ou plusieurs extensions" -#: templates/machines/index_extension.html:41 +#: templates/machines/index_extension.html:44 msgid "List of SOA records" msgstr "Liste des enregistrements SOA" -#: templates/machines/index_extension.html:43 +#: templates/machines/index_extension.html:47 msgid " Add an SOA record" msgstr " Ajouter un enregistrement SOA" -#: templates/machines/index_extension.html:45 +#: templates/machines/index_extension.html:51 msgid " Delete one or several SOA records" msgstr " Supprimer un ou plusieurs enregistrements SOA" -#: templates/machines/index_extension.html:48 +#: templates/machines/index_extension.html:55 msgid "List of MX records" msgstr "Liste des enregistrements MX" -#: templates/machines/index_extension.html:50 +#: templates/machines/index_extension.html:58 msgid " Add an MX record" msgstr " Ajouter un enregistrement MX" -#: templates/machines/index_extension.html:52 +#: templates/machines/index_extension.html:62 msgid " Delete one or several MX records" msgstr " Supprimer un ou plusieurs enregistrements MX" -#: templates/machines/index_extension.html:55 +#: templates/machines/index_extension.html:66 msgid "List of NS records" msgstr "Liste des enregistrements NS" -#: templates/machines/index_extension.html:57 +#: templates/machines/index_extension.html:69 msgid " Add an NS record" msgstr " Ajouter un enregistrement NS" -#: templates/machines/index_extension.html:59 +#: templates/machines/index_extension.html:73 msgid " Delete one or several NS records" msgstr " Supprimer un ou plusieurs enregistrements NS" -#: templates/machines/index_extension.html:62 +#: templates/machines/index_extension.html:77 msgid "List of TXT records" msgstr "Liste des enregistrements TXT" -#: templates/machines/index_extension.html:64 +#: templates/machines/index_extension.html:80 msgid " Add a TXT record" msgstr " Ajouter un enregistrement TXT" -#: templates/machines/index_extension.html:66 +#: templates/machines/index_extension.html:84 msgid " Delete one or several TXT records" msgstr " Supprimer un ou plusieurs enregistrements TXT" -#: templates/machines/index_extension.html:69 +#: templates/machines/index_extension.html:88 msgid "List of DNAME records" msgstr "Liste des enregistrements DNAME" -#: templates/machines/index_extension.html:72 +#: templates/machines/index_extension.html:91 msgid " Add a DNAME record" msgstr " Ajouter un enregistrement DNAME" -#: templates/machines/index_extension.html:76 +#: templates/machines/index_extension.html:95 msgid " Delete one or several DNAME records" msgstr " Supprimer un ou plusieurs enregistrements DNAME" -#: templates/machines/index_extension.html:80 +#: templates/machines/index_extension.html:99 msgid "List of SRV records" msgstr "Liste des enregistrements SRV" -#: templates/machines/index_extension.html:82 +#: templates/machines/index_extension.html:102 msgid " Add an SRV record" msgstr " Ajouter un enregistrement SRV" -#: templates/machines/index_extension.html:84 +#: templates/machines/index_extension.html:106 msgid " Delete one or several SRV records" msgstr " Supprimer un ou plusieurs enregistrements SRV" -#: templates/machines/index_iptype.html:34 +#: templates/machines/index_iptype.html:33 msgid "List of IP types" msgstr "Liste des types d'IP" @@ -1184,7 +1213,7 @@ msgstr "Liste des types d'IP" msgid " Add an IP type" msgstr " Ajouter un type d'IP" -#: templates/machines/index_iptype.html:38 +#: templates/machines/index_iptype.html:40 msgid " Delete one or several IP types" msgstr " Supprimer un ou plusieurs types d'IP" @@ -1192,7 +1221,7 @@ msgstr " Supprimer un ou plusieurs types d'IP" msgid "List of the IPv6 addresses of the interface" msgstr "Liste des adresses IPv6 de l'interface" -#: templates/machines/index_ipv6.html:35 +#: templates/machines/index_ipv6.html:36 msgid " Add an IPv6 address" msgstr " Ajouter une adresse IPv6" @@ -1200,11 +1229,11 @@ msgstr " Ajouter une adresse IPv6" msgid "List of machine types" msgstr "Liste des types de machine" -#: templates/machines/index_machinetype.html:36 +#: templates/machines/index_machinetype.html:37 msgid " Add a machine type" msgstr " Ajouter un type de machine" -#: templates/machines/index_machinetype.html:38 +#: templates/machines/index_machinetype.html:41 msgid " Delete one or several machine types" msgstr " Supprimer un ou plusieurs types de machine" @@ -1223,11 +1252,11 @@ msgstr "" "type de machine à assigner aux machines en fonction du type de dispositif " "NAS." -#: templates/machines/index_nas.html:37 +#: templates/machines/index_nas.html:38 msgid " Add a NAS device type" msgstr " Ajouter un type de dispositif NAS" -#: templates/machines/index_nas.html:39 +#: templates/machines/index_nas.html:42 msgid " Delete one or several NAS device types" msgstr " Supprimer un ou plusieurs types de dispositif NAS" @@ -1235,23 +1264,23 @@ msgstr " Supprimer un ou plusieurs types de dispositif NAS" msgid "List of ports configurations" msgstr "Liste des configurations de ports" -#: templates/machines/index_portlist.html:13 +#: templates/machines/index_portlist.html:14 msgid " Add a configuration" msgstr " Ajouter une configuration" -#: templates/machines/index_portlist.html:19 +#: templates/machines/index_portlist.html:21 msgid "TCP (input)" msgstr "TCP (entrée)" -#: templates/machines/index_portlist.html:20 +#: templates/machines/index_portlist.html:22 msgid "TCP (output)" msgstr "TCP (sortie)" -#: templates/machines/index_portlist.html:21 +#: templates/machines/index_portlist.html:23 msgid "UDP (input)" msgstr "UDP (entrée)" -#: templates/machines/index_portlist.html:22 +#: templates/machines/index_portlist.html:24 msgid "UDP (output)" msgstr "UDP (sortie)" @@ -1259,11 +1288,11 @@ msgstr "UDP (sortie)" msgid "List of roles" msgstr "Liste des rôles" -#: templates/machines/index_role.html:35 +#: templates/machines/index_role.html:36 msgid " Add a role" msgstr " Ajouter un rôle" -#: templates/machines/index_role.html:37 +#: templates/machines/index_role.html:39 msgid " Delete one or several roles" msgstr " Supprimer un ou plusieurs rôles" @@ -1271,15 +1300,15 @@ msgstr " Supprimer un ou plusieurs rôles" msgid "List of services" msgstr "Liste des services" -#: templates/machines/index_service.html:35 +#: templates/machines/index_service.html:36 msgid " Add a service" msgstr " Ajouter un service" -#: templates/machines/index_service.html:37 +#: templates/machines/index_service.html:39 msgid " Delete one or several services" msgstr " Supprimer un ou plusieurs services" -#: templates/machines/index_service.html:39 +#: templates/machines/index_service.html:42 msgid "States of servers" msgstr "États des serveurs" @@ -1291,7 +1320,7 @@ msgstr "Empreintes SSH" msgid " Add an SSH fingerprint" msgstr " Ajouter une empreinte SSH" -#: templates/machines/index_vlan.html:34 +#: templates/machines/index_vlan.html:33 msgid "List of VLANs" msgstr "Liste des VLANs" @@ -1299,7 +1328,7 @@ msgstr "Liste des VLANs" msgid " Add a VLAN" msgstr " Ajouter un VLAN" -#: templates/machines/index_vlan.html:38 +#: templates/machines/index_vlan.html:39 msgid " Delete one or several VLANs" msgstr " Supprimer un ou plusieurs VLANs" @@ -1343,90 +1372,90 @@ msgstr "Rôles de serveur" 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:155 +msgid "Select a machine type first." +msgstr "Sélectionnez un type de machine d'abord." -#: views.py:258 +#: views.py:257 msgid "The machine was created." msgstr "La machine a été créée." -#: views.py:270 +#: views.py:269 msgid "Create a machine" msgstr "Créer une machine" -#: views.py:310 +#: views.py:309 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:321 views.py:445 views.py:511 views.py:567 views.py:629 +#: views.py:690 views.py:748 views.py:805 views.py:862 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 +#: views.py:334 msgid "The machine was deleted." msgstr "La machine a été supprimée." -#: views.py:364 +#: views.py:363 msgid "The interface was created." msgstr "L'interface a été créée." -#: views.py:391 +#: views.py:390 msgid "The interface was deleted." msgstr "L'interface a été supprimée." -#: views.py:416 +#: views.py:415 msgid "The IPv6 addresses list was created." msgstr "La liste d'adresses IPv6 a été créée." -#: views.py:422 +#: views.py:421 msgid "Create an IPv6 addresses list" msgstr "Créer une liste d'adresses IPv6" -#: views.py:440 +#: views.py:439 msgid "The IPv6 addresses list was edited." msgstr "La liste d'adresses IPv6 a été modifiée." -#: views.py:459 +#: views.py:458 msgid "The IPv6 addresses list was deleted." msgstr "La liste d'adresses IPv6 a été supprimée." -#: views.py:483 +#: views.py:482 msgid "The SSHFP record was created." msgstr "L'enregistrement SSHFP a été créé." -#: views.py:489 +#: views.py:488 msgid "Create a SSHFP record" msgstr "Créer un enregistrement SSHFP" -#: views.py:506 +#: views.py:505 msgid "The SSHFP record was edited." msgstr "L'enregistrement SSHFP a été modifié." -#: views.py:525 +#: views.py:524 msgid "The SSHFP record was deleted." msgstr "L'enregistrement SSHFP a été supprimé." -#: views.py:546 +#: views.py:545 msgid "The IP type was created." msgstr "Le type d'IP a été créé." -#: views.py:549 +#: views.py:548 msgid "Create an IP type" msgstr "Créer un type d'IP" -#: views.py:565 +#: views.py:564 msgid "The IP type was edited." msgstr "Le type d'IP a été modifié." -#: views.py:584 +#: views.py:583 msgid "The IP type was deleted." msgstr "Le type d'IP a été supprimé." -#: views.py:588 +#: views.py:587 #, python-format msgid "" "The IP type %s is assigned to at least one machine, you can't delete it." @@ -1434,29 +1463,29 @@ 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:592 views.py:654 views.py:715 views.py:772 views.py:829 +#: views.py:886 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 +#: views.py:605 msgid "The machine type was created." msgstr "Le type de machine a été créé." -#: views.py:609 +#: views.py:608 msgid "Create a machine type" msgstr "Créer un type de machine" -#: views.py:627 +#: views.py:626 msgid "The machine type was edited." msgstr "Le type de machine a été modifié." -#: views.py:646 +#: views.py:645 msgid "The machine type was deleted." msgstr "Le type de machine a été supprimé." -#: views.py:650 +#: views.py:649 #, python-format msgid "" "The machine type %s is assigned to at least one machine, you can't delete it." @@ -1464,23 +1493,23 @@ msgstr "" "Le type de machine %s est assigné à au moins un machine, vous ne pouvez pas " "le supprimer." -#: views.py:668 +#: views.py:667 msgid "The extension was created." msgstr "L'extension a été créée." -#: views.py:671 +#: views.py:670 msgid "Create an extension" msgstr "Créer une extension" -#: views.py:688 +#: views.py:687 msgid "The extension was edited." msgstr "L'extension a été modifiée." -#: views.py:707 +#: views.py:706 msgid "The extension was deleted." msgstr "L'extension a été supprimée." -#: views.py:711 +#: views.py:710 #, python-format msgid "" "The extension %s is assigned to at least one machine type, you can't delete " @@ -1489,65 +1518,65 @@ msgstr "" "L'extension %s est assignée à au moins un type de machine, vous ne pouvez " "pas le supprimer." -#: views.py:729 +#: views.py:728 msgid "The SOA record was created." msgstr "L'enregistrement SOA a été créé." -#: views.py:732 +#: views.py:731 msgid "Create an SOA record" msgstr "Créer un enregistrement SOA" -#: views.py:746 +#: views.py:745 msgid "The SOA record was edited." msgstr "L'enregistrement SOA a été modifié." -#: views.py:765 +#: views.py:764 msgid "The SOA record was deleted." msgstr "L'enregistrement SOA a été supprimé." -#: views.py:769 +#: views.py:768 #, 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 +#: views.py:785 msgid "The MX record was created." msgstr "L'enregistrement MX a été créé." -#: views.py:789 +#: views.py:788 msgid "Create an MX record" msgstr "Créer un enregistrement MX" -#: views.py:803 +#: views.py:802 msgid "The MX record was edited." msgstr "L'enregistrement MX a été modifié." -#: views.py:822 +#: views.py:821 msgid "The MX record was deleted." msgstr "L'enregistrement MX a été supprimé." -#: views.py:826 +#: views.py:825 #, 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 +#: views.py:842 msgid "The NS record was created." msgstr "L'enregistrement NS a été créé." -#: views.py:846 +#: views.py:845 msgid "Create an NS record" msgstr "Créer un enregistrement NS" -#: views.py:860 +#: views.py:859 msgid "The NS record was edited." msgstr "L'enregistrement NS a été modifié." -#: views.py:879 +#: views.py:878 msgid "The NS record was deleted." msgstr "L'enregistrement NS a été supprimé." -#: views.py:883 +#: views.py:882 #, python-format msgid "Error: the NS record %s can't be deleted." msgstr "Erreur : l'enregistrement NS %s ne peut pas être supprimé." diff --git a/machines/migrations/0095_auto_20180919_2225.py b/machines/migrations/0095_auto_20180919_2225.py new file mode 100644 index 00000000..66c082ff --- /dev/null +++ b/machines/migrations/0095_auto_20180919_2225.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-09-19 20:25 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0094_auto_20180815_1918'), + ] + + operations = [ + migrations.AddField( + model_name='vlan', + name='arp_protect', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='vlan', + name='dhcp_snooping', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='vlan', + name='dhcpv6_snooping', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='vlan', + name='igmp', + field=models.BooleanField(default=False, help_text='Gestion multicast v4'), + ), + migrations.AddField( + model_name='vlan', + name='mld', + field=models.BooleanField(default=False, help_text='Gestion multicast v6'), + ), + ] diff --git a/machines/migrations/0096_auto_20181013_1417.py b/machines/migrations/0096_auto_20181013_1417.py new file mode 100644 index 00000000..745fb523 --- /dev/null +++ b/machines/migrations/0096_auto_20181013_1417.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-13 12:17 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ] + + operations = [ + migrations.AlterField( + model_name='machine', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/machines/migrations/0097_extension_dnssec.py b/machines/migrations/0097_extension_dnssec.py new file mode 100644 index 00000000..48e41f77 --- /dev/null +++ b/machines/migrations/0097_extension_dnssec.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-24 14:00 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0096_auto_20181013_1417'), + ] + + operations = [ + migrations.AddField( + model_name='extension', + name='dnssec', + field=models.BooleanField(default=False, help_text='Should the zone be signed with DNSSEC'), + ), + ] diff --git a/machines/migrations/0098_auto_20190102_1745.py b/machines/migrations/0098_auto_20190102_1745.py new file mode 100644 index 00000000..e886e8a1 --- /dev/null +++ b/machines/migrations/0098_auto_20190102_1745.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-02 23:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0097_extension_dnssec'), + ] + + operations = [ + 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'), ('dns-recursive-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), + ), + ] diff --git a/machines/migrations/0099_role_recursive_dns.py b/machines/migrations/0099_role_recursive_dns.py new file mode 100644 index 00000000..c1ce3965 --- /dev/null +++ b/machines/migrations/0099_role_recursive_dns.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-02 23:45 +from __future__ import unicode_literals + +from django.db import migrations, models + + +def migrate(apps, schema_editor): + Role = apps.get_model('machines', 'Role') + + for role in Role.objects.filter(specific_role='dns-recursif-server'): + role.specific_role = 'dns-recursive-server' + role.save() + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0098_auto_20190102_1745'), + ] + + operations = [ + migrations.RunPython(migrate), + ] + + diff --git a/machines/migrations/0100_auto_20190102_1753.py b/machines/migrations/0100_auto_20190102_1753.py new file mode 100644 index 00000000..35f7b78d --- /dev/null +++ b/machines/migrations/0100_auto_20190102_1753.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-02 23:53 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0099_role_recursive_dns'), + ] + + operations = [ + 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-recursive-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), + ), + ] diff --git a/machines/migrations/0101_auto_20190108_1623.py b/machines/migrations/0101_auto_20190108_1623.py new file mode 100644 index 00000000..856721ac --- /dev/null +++ b/machines/migrations/0101_auto_20190108_1623.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-08 22:23 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0100_auto_20190102_1753'), + ] + + operations = [ + migrations.AlterModelOptions( + name='ouvertureport', + options={'verbose_name': 'ports opening', 'verbose_name_plural': 'ports openings'}, + ), + migrations.AlterField( + model_name='nas', + name='port_access_mode', + field=models.CharField(choices=[('802.1X', '802.1X'), ('Mac-address', 'MAC-address')], default='802.1X', max_length=32), + ), + migrations.AlterField( + model_name='vlan', + name='igmp', + field=models.BooleanField(default=False, help_text='v4 multicast management'), + ), + migrations.AlterField( + model_name='vlan', + name='mld', + field=models.BooleanField(default=False, help_text='v6 multicast management'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 4de0b012..cdc6d830 100644 --- a/machines/models.py +++ b/machines/models.py @@ -27,37 +27,35 @@ The models definitions for the Machines app from __future__ import unicode_literals -from datetime import timedelta +import base64 +import hashlib import re +from datetime import timedelta from ipaddress import IPv6Address from itertools import chain -from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress -import hashlib -import base64 +from django.core.validators import MaxValueValidator, MinValueValidator 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, MinValueValidator +from django.utils.functional import cached_property from django.utils.translation import ugettext_lazy as _ +from macaddress.fields import MACAddressField, default_dialect +from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress -from macaddress.fields import MACAddressField - +import preferences.models +import users.models from re2o.field_permissions import FieldPermissionModelMixin from re2o.mixins import AclMixin, RevMixin -import users.models -import preferences.models - class Machine(RevMixin, FieldPermissionModelMixin, models.Model): """ Class définissant une machine, object parent user, objets fils interfaces""" - user = models.ForeignKey('users.User', on_delete=models.PROTECT) + user = models.ForeignKey('users.User', on_delete=models.CASCADE) name = models.CharField( max_length=255, help_text=_("Optional"), @@ -116,7 +114,7 @@ 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'): return False, _("You don't have the right to view all the" - " machines.") + " machines.") return True, None @staticmethod @@ -131,9 +129,9 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): except users.models.User.DoesNotExist: return False, _("Nonexistent user.") max_lambdauser_interfaces = (preferences.models.OptionalMachine - .get_cached_value( - 'max_lambdauser_interfaces' - )) + .get_cached_value( + 'max_lambdauser_interfaces' + )) if not user_request.has_perm('machines.add_machine'): if not (preferences.models.OptionalMachine .get_cached_value('create_machine')): @@ -180,7 +178,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): **kwargs )[0]): return False, _("You don't have the right to delete a machine" - " of another user.") + " of another user.") return True, None def can_view(self, user_request, *_args, **_kwargs): @@ -192,14 +190,24 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): if (not user_request.has_perm('machines.view_machine') and self.user != user_request): return False, _("You don't have the right to view other machines" - " than yours.") + " 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) + interfaces_set = self.interface_set.first() + if interfaces_set: + return str(interfaces_set.domain.name) + else: + return _("No name") + + @cached_property + def complete_name(self): + """Par defaut, renvoie le nom de la première interface + de cette machine""" + return str(self.interface_set.first()) @cached_property def all_short_names(self): @@ -207,14 +215,19 @@ class Machine(RevMixin, FieldPermissionModelMixin, models.Model): machine""" return Domain.objects.filter( interface_parent__machine=self - ).values_list('name', flat=True).distinct() + ).values_list('name', flat=True).distinct() + + @cached_property + def get_name(self): + """Return a name : user provided name or first interface name""" + return self.name or self.short_name @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) @@ -290,8 +303,8 @@ class IpType(RevMixin, AclMixin, models.Model): help_text=_("Netmask for the domain's IPv4 range") ) reverse_v4 = models.BooleanField( - default=False, - help_text=_("Enable reverse DNS for IPv4"), + default=False, + help_text=_("Enable reverse DNS for IPv4"), ) prefix_v6 = models.GenericIPAddressField( protocol='IPv6', @@ -304,10 +317,10 @@ class IpType(RevMixin, AclMixin, models.Model): MaxValueValidator(128), MinValueValidator(0) ] - ) + ) reverse_v6 = models.BooleanField( - default=False, - help_text=_("Enable reverse DNS for IPv6"), + default=False, + help_text=_("Enable reverse DNS for IPv6"), ) vlan = models.ForeignKey( 'Vlan', @@ -327,7 +340,7 @@ class IpType(RevMixin, AclMixin, models.Model): ("use_all_iptype", _("Can use all IP types")), ) verbose_name = _("IP type") - verbose_name_plural = ("IP types") + verbose_name_plural = _("IP types") @cached_property def ip_range(self): @@ -367,9 +380,9 @@ class IpType(RevMixin, AclMixin, models.Model): 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), + '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 } @@ -388,10 +401,10 @@ class IpType(RevMixin, AclMixin, models.Model): 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), + '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 @@ -442,9 +455,9 @@ 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) @@ -515,6 +528,18 @@ class Vlan(RevMixin, AclMixin, models.Model): vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)]) name = models.CharField(max_length=256) comment = models.CharField(max_length=256, blank=True) + #Réglages supplémentaires + arp_protect = models.BooleanField(default=False) + dhcp_snooping = models.BooleanField(default=False) + dhcpv6_snooping = models.BooleanField(default=False) + igmp = models.BooleanField( + default=False, + help_text=_("v4 multicast management") + ) + mld = models.BooleanField( + default=False, + help_text=_("v6 multicast management") + ) class Meta: permissions = ( @@ -534,7 +559,7 @@ class Nas(RevMixin, AclMixin, models.Model): default_mode = '802.1X' AUTH = ( ('802.1X', '802.1X'), - ('Mac-address', 'Mac-address'), + ('Mac-address', _("MAC-address")), ) name = models.CharField(max_length=255, unique=True) @@ -641,7 +666,7 @@ class SOA(RevMixin, AclMixin, models.Model): utilisée dans les migrations de la BDD. """ return cls.objects.get_or_create( name=_("SOA to edit"), - mail="postmaser@example.com" + mail="postmaster@example.com" )[0].pk @@ -671,6 +696,10 @@ class Extension(RevMixin, AclMixin, models.Model): 'SOA', on_delete=models.CASCADE ) + dnssec = models.BooleanField( + default=False, + help_text=_("Should the zone be signed with DNSSEC") + ) class Meta: permissions = ( @@ -716,6 +745,9 @@ class Extension(RevMixin, AclMixin, models.Model): .filter(cname__interface_parent__in=all_active_assigned_interfaces()) .prefetch_related('cname')) + def get_associated_dname_records(self): + return (DName.objects.filter(alias=self)) + @staticmethod def can_use_all(user_request, *_args, **_kwargs): """Superdroit qui permet d'utiliser toutes les extensions sans @@ -796,8 +828,8 @@ class Txt(RevMixin, AclMixin, models.Model): verbose_name_plural = _("TXT records") def __str__(self): - return str(self.zone) + " : " + str(self.field1) + " " +\ - str(self.field2) + return str(self.zone) + " : " + str(self.field1) + " " + \ + str(self.field2) @cached_property def dns_entry(self): @@ -856,7 +888,7 @@ class Srv(RevMixin, AclMixin, models.Model): default=0, validators=[MaxValueValidator(65535)], help_text=_("Relative weight for records with the same priority" - " (integer value between 0 and 65535)") + " (integer value between 0 and 65535)") ) port = models.PositiveIntegerField( validators=[MaxValueValidator(65535)], @@ -876,17 +908,17 @@ class Srv(RevMixin, AclMixin, models.Model): verbose_name_plural = _("SRV records") def __str__(self): - return str(self.service) + ' ' + str(self.protocole) + ' ' +\ - str(self.extension) + ' ' + str(self.priority) +\ - ' ' + str(self.weight) + str(self.port) + str(self.target) + return str(self.service) + ' ' + str(self.protocole) + ' ' + \ + str(self.extension) + ' ' + str(self.priority) + \ + ' ' + str(self.weight) + str(self.port) + str(self.target) @cached_property def dns_entry(self): """Renvoie l'enregistrement SRV complet pour le fichier de zone""" - return str(self.service) + '._' + str(self.protocole).lower() +\ - str(self.extension) + '. ' + str(self.ttl) + ' IN SRV ' +\ - str(self.priority) + ' ' + str(self.weight) + ' ' +\ - str(self.port) + ' ' + str(self.target) + '.' + return str(self.service) + '._' + str(self.protocole).lower() + \ + str(self.extension) + '. ' + str(self.ttl) + ' IN SRV ' + \ + str(self.priority) + ' ' + str(self.weight) + ' ' + \ + str(self.port) + ' ' + str(self.target) + '.' class SshFp(RevMixin, AclMixin, models.Model): @@ -902,7 +934,7 @@ class SshFp(RevMixin, AclMixin, models.Model): machine = models.ForeignKey('Machine', on_delete=models.CASCADE) pub_key_entry = models.TextField( - help_text="SSH public key", + help_text=_("SSH public key"), max_length=2048 ) algo = models.CharField( @@ -910,7 +942,7 @@ class SshFp(RevMixin, AclMixin, models.Model): max_length=32 ) comment = models.CharField( - help_text="Comment", + help_text=_("Comment"), max_length=255, null=True, blank=True @@ -931,8 +963,8 @@ class SshFp(RevMixin, AclMixin, models.Model): """Return the hashess for the pub key with correct id cf RFC, 1 is sha1 , 2 sha256""" return { - "1" : hashlib.sha1(base64.b64decode(self.pub_key_entry)).hexdigest(), - "2" : hashlib.sha256(base64.b64decode(self.pub_key_entry)).hexdigest(), + "1": hashlib.sha1(base64.b64decode(self.pub_key_entry)).hexdigest(), + "2": hashlib.sha256(base64.b64decode(self.pub_key_entry)).hexdigest(), } class Meta: @@ -955,7 +987,6 @@ class SshFp(RevMixin, AclMixin, models.Model): return str(self.algo) + ' ' + str(self.comment) - class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ Une interface. Objet clef de l'application machine : - une address mac unique. Possibilité de la rendre unique avec le @@ -1049,7 +1080,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): .get_cached_value('ipv6_mode') == 'SLAAC'): self.sync_ipv6_slaac() elif (preferences.models.OptionalMachine - .get_cached_value('ipv6_mode') == 'DHCPV6'): + .get_cached_value('ipv6_mode') == 'DHCPV6'): self.sync_ipv6_dhcpv6() else: return @@ -1062,10 +1093,10 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): .get_cached_value('ipv6_mode') == 'SLAAC'): return self.ipv6list.all() elif (preferences.models.OptionalMachine - .get_cached_value('ipv6_mode') == 'DHCPV6'): + .get_cached_value('ipv6_mode') == 'DHCPV6'): return self.ipv6list.filter(slaac_ip=False) else: - return None + return [] def mac_bare(self): """ Formatage de la mac type mac_bare""" @@ -1075,28 +1106,10 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ Tente un formatage mac_bare, si échoue, lève une erreur de validation""" try: - self.mac_address = str(EUI(self.mac_address)) + self.mac_address = str(EUI(self.mac_address, dialect=default_dialect())) except: raise ValidationError(_("The given MAC address is invalid.")) - def clean(self, *args, **kwargs): - """ Formate l'addresse mac en mac_bare (fonction filter_mac) - et assigne une ipv4 dans le bon range si inexistante ou incohérente""" - # If type was an invalid value, django won't create an attribute type - # but try clean() as we may be able to create it from another value - # so even if the error as yet been detected at this point, django - # continues because the error might not prevent us from creating the - # instance. - # But in our case, it's impossible to create a type value so we raise - # the error. - if not hasattr(self, 'type'): - 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: - self.assign_ipv4() - super(Interface, self).clean(*args, **kwargs) - def assign_ipv4(self): """ Assigne une ip à l'interface """ free_ips = self.type.ip_type.free_ip() @@ -1116,6 +1129,42 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): self.clean() self.save() + def has_private_ip(self): + """ True si l'ip associée est privée""" + if self.ipv4: + return IPAddress(str(self.ipv4)).is_private() + else: + return False + + def may_have_port_open(self): + """ True si l'interface a une ip et une ip publique. + Permet de ne pas exporter des ouvertures sur des ip privées + (useless)""" + return self.ipv4 and not self.has_private_ip() + + def clean(self, *args, **kwargs): + """ Formate l'addresse mac en mac_bare (fonction filter_mac) + et assigne une ipv4 dans le bon range si inexistante ou incohérente""" + # If type was an invalid value, django won't create an attribute type + # but try clean() as we may be able to create it from another value + # so even if the error as yet been detected at this point, django + # continues because the error might not prevent us from creating the + # instance. + # But in our case, it's impossible to create a type value so we raise + # the error. + if not hasattr(self, 'type'): + raise ValidationError(_("The selected IP type is invalid.")) + self.filter_macaddress() + if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: + self.assign_ipv4() + super(Interface, self).clean(*args, **kwargs) + + def validate_unique(self, *args, **kwargs): + super(Interface, self).validate_unique(*args, **kwargs) + interfaces_similar = Interface.objects.filter(mac_address=self.mac_address, type__ip_type=self.type.ip_type) + if interfaces_similar and interfaces_similar.first() != self: + raise ValidationError(_("Mac address already registered in this Machine Type/Subnet")) + def save(self, *args, **kwargs): self.filter_macaddress() # On verifie la cohérence en forçant l'extension par la méthode @@ -1123,6 +1172,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): if self.type.ip_type != self.ipv4.ip_type: raise ValidationError(_("The IPv4 address and the machine type" " don't match.")) + self.validate_unique() super(Interface, self).save(*args, **kwargs) @staticmethod @@ -1141,9 +1191,9 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): .get_cached_value('create_machine')): return False, _("You can't add a machine.") max_lambdauser_interfaces = (preferences.models.OptionalMachine - .get_cached_value( - 'max_lambdauser_interfaces' - )) + .get_cached_value( + 'max_lambdauser_interfaces' + )) if machine.user != user_request: return False, _("You don't have the right to add an interface" " to a machine of another user.") @@ -1220,19 +1270,6 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): domain = None return str(domain) - def has_private_ip(self): - """ True si l'ip associée est privée""" - if self.ipv4: - return IPAddress(str(self.ipv4)).is_private() - else: - return False - - def may_have_port_open(self): - """ True si l'interface a une ip et une ip publique. - Permet de ne pas exporter des ouvertures sur des ip privées - (useless)""" - return self.ipv4 and not self.has_private_ip() - class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): """ A list of IPv6 """ @@ -1347,10 +1384,13 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): def clean(self, *args, **kwargs): if self.slaac_ip and (Ipv6List.objects - .filter(interface=self.interface, slaac_ip=True) - .exclude(id=self.id)): + .filter(interface=self.interface, slaac_ip=True) + .exclude(id=self.id)): raise ValidationError(_("A SLAAC IP address is already registered.")) - prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') + try: + prefix_v6 = self.interface.type.ip_type.prefix_v6.encode().decode('utf-8') + except AttributeError: # Prevents from crashing when there is no defined prefix_v6 + prefix_v6 = None if prefix_v6: if (IPv6Address(self.ipv6.encode().decode('utf-8')).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]): @@ -1480,18 +1520,18 @@ class Domain(RevMixin, AclMixin, models.Model): 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' - )) + .get_cached_value( + 'max_lambdauser_aliases' + )) if interface.machine.user != user_request: 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()) + .user_interfaces()) ) - ).count() >= max_lambdauser_aliases: + ).count() >= max_lambdauser_aliases: return False, _("You reached the maximum number of alias that" " you are allowed to create yourself (%s). " % max_lambdauser_aliases) @@ -1579,7 +1619,7 @@ class Role(RevMixin, AclMixin, models.Model): ROLE = ( ('dhcp-server', _("DHCP server")), ('switch-conf-server', _("Switches configuration server")), - ('dns-recursif-server', _("Recursive DNS server")), + ('dns-recursive-server', _("Recursive DNS server")), ('ntp-server', _("NTP server")), ('radius-server', _("RADIUS server")), ('log-server', _("Log server")), @@ -1608,18 +1648,6 @@ class Role(RevMixin, AclMixin, models.Model): 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""" @@ -1634,6 +1662,11 @@ class Role(RevMixin, AclMixin, models.Model): machine__interface__role=cls.objects.filter(specific_role=roletype) ) + @classmethod + def interface_for_roletype(cls, roletype): + """Return interfaces for a roletype""" + return Interface.objects.filter(role=cls.objects.filter(specific_role=roletype)) + def save(self, *args, **kwargs): super(Role, self).save(*args, **kwargs) @@ -1664,7 +1697,7 @@ class Service(RevMixin, AclMixin, models.Model): def ask_regen(self): """ Marque à True la demande de régénération pour un service x """ - Service_link.objects.filter(service=self).exclude(asked_regen=True)\ + Service_link.objects.filter(service=self).exclude(asked_regen=True) \ .update(asked_regen=True) return @@ -1672,11 +1705,11 @@ 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)\ + Service_link.objects.filter(service=self).exclude(server__in=servers) \ .delete() return @@ -1723,10 +1756,10 @@ class Service_link(RevMixin, AclMixin, models.Model): régénération de service""" return bool( (self.asked_regen and ( - self.last_regen + self.service.min_time_regen + self.last_regen + self.service.min_time_regen ) < timezone.now() - ) or ( - self.last_regen + self.service.regular_time_regen + ) or ( + self.last_regen + self.service.regular_time_regen ) < timezone.now() ) @@ -1844,10 +1877,10 @@ class OuverturePort(RevMixin, AclMixin, models.Model): ), default=OUT, ) - + class Meta: verbose_name = _("ports opening") - verbose_name = _("ports openings") + verbose_name_plural = _("ports openings") def __str__(self): if self.begin == self.end: @@ -2013,4 +2046,3 @@ def srv_post_save(**_kwargs): def srv_post_delete(**_kwargs): """Regeneration dns après modification d'un SRV""" regen('dns') - diff --git a/machines/serializers.py b/machines/serializers.py index f3a47c55..3f5fb966 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -26,8 +26,8 @@ Serializers for the Machines app """ - from rest_framework import serializers + from machines.models import ( Interface, IpType, @@ -148,13 +148,13 @@ class TypeSerializer(serializers.ModelSerializer): get ForeignKey values. Infos about the general port policy is added """ extension = ExtensionNameField(read_only=True) - ouverture_ports_tcp_in = serializers\ + ouverture_ports_tcp_in = serializers \ .SerializerMethodField('get_port_policy_input_tcp') - ouverture_ports_tcp_out = serializers\ + ouverture_ports_tcp_out = serializers \ .SerializerMethodField('get_port_policy_output_tcp') - ouverture_ports_udp_in = serializers\ + ouverture_ports_udp_in = serializers \ .SerializerMethodField('get_port_policy_input_udp') - ouverture_ports_udp_out = serializers\ + ouverture_ports_udp_out = serializers \ .SerializerMethodField('get_port_policy_output_udp') class Meta: @@ -400,7 +400,7 @@ class OuverturePortsSerializer(serializers.Serializer): return { i.ipv4.ipv4: { "tcp_in": [j.tcp_ports_in() for j in i.port_lists.all()], - "tcp_out": [j.tcp_ports_out()for j in i.port_lists.all()], + "tcp_out": [j.tcp_ports_out() for j in i.port_lists.all()], "udp_in": [j.udp_ports_in() for j in i.port_lists.all()], "udp_out": [j.udp_ports_out() for j in i.port_lists.all()], } @@ -413,7 +413,7 @@ class OuverturePortsSerializer(serializers.Serializer): return { i.ipv6: { "tcp_in": [j.tcp_ports_in() for j in i.port_lists.all()], - "tcp_out": [j.tcp_ports_out()for j in i.port_lists.all()], + "tcp_out": [j.tcp_ports_out() for j in i.port_lists.all()], "udp_in": [j.udp_ports_in() for j in i.port_lists.all()], "udp_out": [j.udp_ports_out() for j in i.port_lists.all()], } diff --git a/machines/templates/machines/aff_alias.html b/machines/templates/machines/aff_alias.html index 17266a6e..ee8580b0 100644 --- a/machines/templates/machines/aff_alias.html +++ b/machines/templates/machines/aff_alias.html @@ -28,10 +28,10 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - + + + + {% for alias in alias_list %} @@ -45,4 +45,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Aliases" %}
{% trans "Aliases" %}
- diff --git a/machines/templates/machines/aff_dname.html b/machines/templates/machines/aff_dname.html index 7e043d7c..6d07a7bc 100644 --- a/machines/templates/machines/aff_dname.html +++ b/machines/templates/machines/aff_dname.html @@ -26,11 +26,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - + + + + + {% for dname in dname_list %} @@ -45,4 +45,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Target zone" %}{% trans "Record" %}
{% trans "Target zone" %}{% trans "Record" %}
- diff --git a/machines/templates/machines/aff_extension.html b/machines/templates/machines/aff_extension.html index 43bb9e39..1083b1b1 100644 --- a/machines/templates/machines/aff_extension.html +++ b/machines/templates/machines/aff_extension.html @@ -30,34 +30,35 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - - - - - {% if ipv6_enabled %} + + + + + + {% if ipv6_enabled %} - {% endif %} - - + {% endif %} + + + {% for extension in extension_list %} - - - - - - {% if ipv6_enabled %} - - {% endif %} - - + + + + + + {% if ipv6_enabled %} + + {% endif %} + + + {% endfor %}
{% trans "Extension" %}{% trans "'infra' right required" %}{% trans "SOA record" %}{% trans "A record origin" %}
{% trans "Extension" %}{% trans "'infra' right required" %}{% trans "SOA record" %}{% trans "A record origin" %}{% trans "AAAA record origin" %}
{% trans "DNSSEC" %}
{{ extension.name }}{{ extension.need_infra|tick }}{{ extension.soa }}{{ extension.origin }}{{ extension.origin_v6 }} - {% can_edit extension %} - {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} - {% acl_end %} - {% history_button extension %} -
{{ extension.name }}{{ extension.need_infra|tick }}{{ extension.soa }}{{ extension.origin }}{{ extension.origin_v6 }}{{ extension.dnssec|tick }} + {% can_edit extension %} + {% include 'buttons/edit.html' with href='machines:edit-extension' id=extension.id %} + {% acl_end %} + {% history_button extension %} +
- diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html index b8ed5293..c30c0c73 100644 --- a/machines/templates/machines/aff_iptype.html +++ b/machines/templates/machines/aff_iptype.html @@ -31,36 +31,36 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - - - - - - - - - - + + + + + + + + + + + {% for type in iptype_list %} - - - - - - - - - - - + + + + + + + + + + + {% endfor %}
{% 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" %}
{% 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 }}{% 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 }} - {% can_edit type %} - {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} - {% acl_end %} - {% history_button type %} -
{{ type.type }}{{ type.extension }}{{ type.need_infra|tick }}{{ 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 }} + {% can_edit type %} + {% include 'buttons/edit.html' with href='machines:edit-iptype' id=type.id %} + {% acl_end %} + {% history_button type %} +
- diff --git a/machines/templates/machines/aff_ipv6.html b/machines/templates/machines/aff_ipv6.html index a98c0327..f67bf4c3 100644 --- a/machines/templates/machines/aff_ipv6.html +++ b/machines/templates/machines/aff_ipv6.html @@ -28,12 +28,12 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - + + + + + + {% for ipv6 in ipv6_list %} @@ -50,4 +50,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "IPv6 addresses" %}{% trans "SLAAC" %}
{% trans "IPv6 addresses" %}{% trans "SLAAC" %}
{{ ipv6.ipv6 }}
- diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index 33ae617a..4363cd6e 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -28,7 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% if machines_list.paginator %} - {% include "pagination.html" with list=machines_list %} + {% include 'pagination.html' with list=machines_list go_to_id="machines" %} {% endif %} @@ -41,7 +41,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans "DNS name" as tr_dns_name %} - + @@ -52,96 +52,102 @@ with this program; if not, write to the Free Software Foundation, Inc., - - {% for interface in machine.interface_set.all %} + {% can_create Interface machine.id %} + {% 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 %} + {% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %} + {% acl_end %} + + + {% for interface in machine.interface_set.all %} + - {% if ipv6_enabled and interface.ipv6 != 'None'%} - - - + {% if ipv6_enabled and interface.ipv6 != 'None' %} + + + {% endif %} {% if interface.domain.related_domain.all %} - - - + + + {% endif %} - {% endfor %} - - - {% endfor %} - - + + + + {% endfor %} +
{% include "buttons/sort.html" with prefix='machine' col='name' text=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 "No name" as tr_no_name %} {% trans "View the profile" as tr_view_the_profile %} - {{ machine.name|default:tr_no_name }} + {{ machine.get_name|default:tr_no_name }} {{ machine.user }} - {% can_create Interface machine.id %} - {% 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 %} - {% include 'buttons/suppr.html' with href='machines:del-machine' id=machine.id %} - {% acl_end %} -
{% if interface.domain.related_domain.all %} - {{ interface.domain }} - + {{ interface.domain }} + {% else %} - {{ interface.domain }} + {{ interface.domain }} {% endif %} {{ interface.type }} - {{ interface.mac_address }} IPv4 {{ interface.ipv4 }}
- {% if ipv6_enabled and interface.ipv6 != 'None'%} - IPv6 - + {% if ipv6_enabled and interface.ipv6 != 'None' %} + IPv6 + {% endif %}
-
@@ -152,65 +158,63 @@ with this program; if not, write to the Free Software Foundation, Inc.,
-
-
    - {% for ipv6 in interface.ipv6.all %} -
  • - {{ ipv6 }} -
  • - {% endfor %} -
-
-
+
+
    + {% for ipv6 in interface.ipv6.all %} +
  • + {{ ipv6 }} +
  • + {% endfor %} +
+
+
-
-
    - {% for al in interface.domain.related_domain.all %} -
  • - - {{ al }} - - -
  • - {% endfor %} -
-
-
+
+
    + {% for al in interface.domain.related_domain.all %} +
  • + + {{ al }} + + +
  • + {% endfor %} +
+
+
{% if machines_list.paginator %} - {% include "pagination.html" with list=machines_list %} + {% include 'pagination.html' with list=machines_list go_to_id="machines" %} {% endif %}
- diff --git a/machines/templates/machines/aff_machinetype.html b/machines/templates/machines/aff_machinetype.html index 4725ffb8..ebd77b4f 100644 --- a/machines/templates/machines/aff_machinetype.html +++ b/machines/templates/machines/aff_machinetype.html @@ -28,11 +28,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - + + + + + {% for type in machinetype_list %} @@ -47,4 +47,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Machine type" %}{% trans "Matching IP type" %}
{% trans "Machine type" %}{% trans "Matching IP type" %}
- diff --git a/machines/templates/machines/aff_mx.html b/machines/templates/machines/aff_mx.html index e57e73a4..b10d4eff 100644 --- a/machines/templates/machines/aff_mx.html +++ b/machines/templates/machines/aff_mx.html @@ -28,12 +28,12 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - + + + + + + {% for mx in mx_list %} @@ -49,4 +49,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Concerned zone" %}{% trans "Priority" %}{% trans "Record" %}
{% trans "Concerned zone" %}{% trans "Priority" %}{% trans "Record" %}
- diff --git a/machines/templates/machines/aff_nas.html b/machines/templates/machines/aff_nas.html index 0dd612cc..7a98a7f4 100644 --- a/machines/templates/machines/aff_nas.html +++ b/machines/templates/machines/aff_nas.html @@ -29,19 +29,19 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - - - + + + + + + + + {% for nas in nas_list %} - + @@ -54,4 +54,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Name" %}{% trans "NAS device type" %}{% trans "Machine type linked to the NAS device" %}{% trans "Access mode" %}{% trans "MAC address auto capture" %}
{% trans "Name" %}{% trans "NAS device type" %}{% trans "Machine type linked to the NAS device" %}{% trans "Access mode" %}{% trans "MAC address auto capture" %}
{{ nas.name }}{{ nas.nas_type }}{{ nas.nas_type }} {{ nas.machine_type }} {{ nas.port_access_mode }} {{ nas.autocapture_mac|tick }}
- diff --git a/machines/templates/machines/aff_ns.html b/machines/templates/machines/aff_ns.html index e2187e3e..dda578f3 100644 --- a/machines/templates/machines/aff_ns.html +++ b/machines/templates/machines/aff_ns.html @@ -28,11 +28,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - + + + + + {% for ns in ns_list %} @@ -47,4 +47,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Concerned zone" %}{% trans "Authoritarian interface for the concerned zone" %}
{% trans "Concerned zone" %}{% trans "Authoritarian interface for the concerned zone" %}
- diff --git a/machines/templates/machines/aff_role.html b/machines/templates/machines/aff_role.html index bba094b3..6f285c89 100644 --- a/machines/templates/machines/aff_role.html +++ b/machines/templates/machines/aff_role.html @@ -29,13 +29,13 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - - + + + + + + + {% for role in role_list %} @@ -43,12 +43,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - + {% can_edit role %} + {% include 'buttons/edit.html' with href='machines:edit-role' id=role.id %} + {% acl_end %} + {% history_button role %} + + {% endfor %}
{% trans "Role name" %}{% trans "Specific role" %}{% trans "Servers" %}
{% trans "Role name" %}{% trans "Specific role" %}{% trans "Servers" %}
{{ role.specific_role }} {% for serv in role.servers.all %}{{ serv }}, {% endfor %} - {% can_edit role %} - {% include 'buttons/edit.html' with href='machines:edit-role' id=role.id %} - {% acl_end %} - {% history_button role %} -
- diff --git a/machines/templates/machines/aff_servers.html b/machines/templates/machines/aff_servers.html index 7dd25495..4134c269 100644 --- a/machines/templates/machines/aff_servers.html +++ b/machines/templates/machines/aff_servers.html @@ -27,13 +27,13 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - - + + + + + + + {% for server in servers_list %} @@ -47,4 +47,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Service name" %}{% trans "Server" %}{% trans "Last regeneration" %}{% trans "Regeneration required" %}{% trans "Regeneration activated" %}
{% trans "Service name" %}{% trans "Server" %}{% trans "Last regeneration" %}{% trans "Regeneration required" %}{% trans "Regeneration activated" %}
- diff --git a/machines/templates/machines/aff_service.html b/machines/templates/machines/aff_service.html index df04b93c..d3eb16ba 100644 --- a/machines/templates/machines/aff_service.html +++ b/machines/templates/machines/aff_service.html @@ -28,14 +28,14 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - - - + + + + + + + + {% for service in service_list %} @@ -43,7 +43,8 @@ with this program; if not, write to the Free Software Foundation, Inc., - + {% endfor %}
{% trans "Service name" %}{% trans "Minimal time before regeneration" %}{% trans "Maximal time before regeneration" %}{% trans "Included servers" %}{% trans "Ask for regeneration" %}
{% trans "Service name" %}{% trans "Minimal time before regeneration" %}{% trans "Maximal time before regeneration" %}{% trans "Included servers" %}{% trans "Ask for regeneration" %}
{{ service.min_time_regen }} {{ service.regular_time_regen }} {% for serv in service.servers.all %}{{ serv }}, {% endfor %} {% can_edit service %} {% include 'buttons/edit.html' with href='machines:edit-service' id=service.id %} @@ -53,4 +54,3 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- diff --git a/machines/templates/machines/aff_soa.html b/machines/templates/machines/aff_soa.html index fbf206e0..50a4a5c3 100644 --- a/machines/templates/machines/aff_soa.html +++ b/machines/templates/machines/aff_soa.html @@ -28,15 +28,15 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - - - - + + + + + + + + + {% for soa in soa_list %} @@ -55,4 +55,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Name" %}{% trans "Mail" %}{% trans "Refresh" %}{% trans "Retry" %}{% trans "Expire" %}{% trans "TTL" %}
{% trans "Name" %}{% trans "Mail" %}{% trans "Refresh" %}{% trans "Retry" %}{% trans "Expire" %}{% trans "TTL" %}
- diff --git a/machines/templates/machines/aff_srv.html b/machines/templates/machines/aff_srv.html index 2ee569e0..1a699b49 100644 --- a/machines/templates/machines/aff_srv.html +++ b/machines/templates/machines/aff_srv.html @@ -28,17 +28,17 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - - - - - - - + + + + + + + + + + + {% for srv in srv_list %} @@ -59,4 +59,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Service" %}{% trans "Protocol" %}{% trans "Extension" %}{% trans "TTL" %}{% trans "Priority" %}{% trans "Weight" %}{% trans "Port" %}{% trans "Target" %}
{% trans "Service" %}{% trans "Protocol" %}{% trans "Extension" %}{% trans "TTL" %}{% trans "Priority" %}{% trans "Weight" %}{% trans "Port" %}{% trans "Target" %}
- diff --git a/machines/templates/machines/aff_sshfp.html b/machines/templates/machines/aff_sshfp.html index f684d6a0..ca88d0f4 100644 --- a/machines/templates/machines/aff_sshfp.html +++ b/machines/templates/machines/aff_sshfp.html @@ -27,12 +27,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - - - - - + + + + + + {% for sshfp in sshfp_list %} @@ -52,4 +52,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "SSH public key" %}{% trans "Algorithm used" %}{% trans "Comment" %}
{% trans "SSH public key" %}{% trans "Algorithm used" %}{% trans "Comment" %}
- diff --git a/machines/templates/machines/aff_txt.html b/machines/templates/machines/aff_txt.html index 41778cba..3da268ca 100644 --- a/machines/templates/machines/aff_txt.html +++ b/machines/templates/machines/aff_txt.html @@ -28,11 +28,11 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - - - + + + + + {% for txt in txt_list %} @@ -47,4 +47,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
{% trans "Concerned zone" %}{% trans "Record" %}
{% trans "Concerned zone" %}{% trans "Record" %}
- diff --git a/machines/templates/machines/aff_vlan.html b/machines/templates/machines/aff_vlan.html index 0c1ed053..0b10262b 100644 --- a/machines/templates/machines/aff_vlan.html +++ b/machines/templates/machines/aff_vlan.html @@ -29,20 +29,20 @@ with this program; if not, write to the Free Software Foundation, Inc.,
- - - - - - - + + + + + + + {% for vlan in vlan_list %} - + - +
{% trans "ID" %}{% trans "Name" %}{% trans "Comment" %}{% trans "IP ranges" %}
{% trans "ID" %}{% trans "Name" %}{% trans "Comment" %}{% trans "IP ranges" %}
{{ vlan.vlan_id }}{{ vlan.name }}{{ vlan.name }} {{ vlan.comment }}{% for range in vlan.iptype_set.all %}{{ range }}, {% endfor%}{% for range in vlan.iptype_set.all %}{{ range }}, {% endfor %} {% can_edit vlan %} {% include 'buttons/edit.html' with href='machines:edit-vlan' id=vlan.id %} @@ -53,4 +53,3 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endfor %}
- diff --git a/machines/templates/machines/delete.html b/machines/templates/machines/delete.html index 1c9811f2..59ba2102 100644 --- a/machines/templates/machines/delete.html +++ b/machines/templates/machines/delete.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -26,18 +26,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load i18n %} -{% block title %}{% trans "Creation and editing of machines" %}{% endblock %} +{% block title %}{% trans "Deletion of machines" %}{% endblock %} {% block content %} -
- {% csrf_token %} -

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

- {% trans "Confirm" as tr_confirm %} - {% bootstrap_button tr_confirm button_type="submit" icon="trash" %} -
-
-
-
+
+ {% csrf_token %} +

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

+ {% trans "Confirm" as tr_confirm %} + {% bootstrap_button tr_confirm button_type="submit" icon='trash' button_class='btn-danger' %} +
+
+
+
{% endblock %} - diff --git a/machines/templates/machines/edit_portlist.html b/machines/templates/machines/edit_portlist.html index d6a5e9df..fa9f771a 100644 --- a/machines/templates/machines/edit_portlist.html +++ b/machines/templates/machines/edit_portlist.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -29,44 +29,45 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -{% bootstrap_form_errors port_list %} + {% bootstrap_form_errors port_list %} -
- {% csrf_token %} - {% bootstrap_form port_list %} - {{ ports.management_form }} -
- {% for form in ports.forms %} -
-

- {{ form }} -

-
- {% endfor %} -
+ + {% csrf_token %} + {% bootstrap_form port_list %} + {{ ports.management_form }} +
+ {% for form in ports.forms %} +
+

+ {{ form }} +

+
+ {% endfor %} +
-

- {% trans "Add a port" as value %} - -

- {% trans "Create or edit" as tr_create_or_edit %} - {% bootstrap_button tr_create_or_edit button_type="submit" icon="star" %} -
- + function add_port() { + var new_index = document.getElementsByClassName('port').length; + document.getElementById('id_form-TOTAL_FORMS').value = + parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1; + var new_port = document.createElement('div'); + new_port.className = 'port'; + new_port.innerHTML = template.replace(/__prefix__/g, new_index); + document.getElementById('formset').appendChild(new_port); + } + + document.addEventListener("DOMContentLoaded", function () { + document.getElementById("add_one").addEventListener("click", add_port, true); + }); + {% endblock %} - diff --git a/machines/templates/machines/index.html b/machines/templates/machines/index.html index 509334a0..3aad6d06 100644 --- a/machines/templates/machines/index.html +++ b/machines/templates/machines/index.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -29,10 +29,9 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% trans "Machines" %}

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

{% trans "Machines" %}

+ {% include 'machines/aff_machines.html' with machines_list=machines_list %} +
+
+
{% endblock %} - diff --git a/machines/templates/machines/index_alias.html b/machines/templates/machines/index_alias.html index 2d33177f..7f392667 100644 --- a/machines/templates/machines/index_alias.html +++ b/machines/templates/machines/index_alias.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -30,11 +30,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

{% 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 %} -
-
-
+ {% 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 6669d197..636b314e 100644 --- a/machines/templates/machines/index_extension.html +++ b/machines/templates/machines/index_extension.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -24,67 +24,86 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} - {% load acl %} {% load i18n %} {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% trans "List of extensions" %}

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

{% trans "List of extensions" %}

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

{% trans "List of SOA records" %}

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

{% trans "List of SOA records" %}

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

{% trans "List of MX records" %}

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

{% trans "List of MX records" %}

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

{% trans "List of NS records" %}

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

{% trans "List of NS records" %}

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

{% trans "List of TXT records" %}

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

{% trans "List of TXT records" %}

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

{% trans "List of DNAME records" %}

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

{% trans "List of SRV records" %}

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

{% trans "List of DNAME records" %}

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

{% trans "List of SRV records" %}

+ {% can_create Srv %} + + {% trans " Add an SRV record" %} + + {% acl_end %} + + {% trans " Delete one or several SRV records" %} + + {% include 'machines/aff_srv.html' with srv_list=srv_list %} +{% endblock %} diff --git a/machines/templates/machines/index_iptype.html b/machines/templates/machines/index_iptype.html index 5be4561d..7ffa8cf3 100644 --- a/machines/templates/machines/index_iptype.html +++ b/machines/templates/machines/index_iptype.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -24,21 +24,20 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} - {% load acl %} {% load i18n %} {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% trans "List of IP types" %}

- {% can_create IpType %} - {% trans " Add an IP type" %} - {% acl_end %} - {% trans " Delete one or several IP types" %} - {% include "machines/aff_iptype.html" with iptype_list=iptype_list %} -
-
-
+

{% trans "List of IP types" %}

+ {% can_create IpType %} + + {% trans " Add an IP type" %} + + {% acl_end %} + + {% trans " Delete one or several IP types" %} + + {% include 'machines/aff_iptype.html' with iptype_list=iptype_list %} {% endblock %} - diff --git a/machines/templates/machines/index_ipv6.html b/machines/templates/machines/index_ipv6.html index 06e287e2..98ea697a 100644 --- a/machines/templates/machines/index_ipv6.html +++ b/machines/templates/machines/index_ipv6.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -30,13 +30,14 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

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

- {% can_create Ipv6List interface_id %} - {% trans " Add an IPv6 address" %} - {% acl_end %} - {% include "machines/aff_ipv6.html" with ipv6_list=ipv6_list %} -
-
-
+

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

+ {% can_create Ipv6List interface_id %} + + {% trans " Add an IPv6 address" %} + + {% acl_end %} + {% include 'machines/aff_ipv6.html' with ipv6_list=ipv6_list %} +
+
+
{% endblock %} - diff --git a/machines/templates/machines/index_machinetype.html b/machines/templates/machines/index_machinetype.html index de1e2f58..407aef5b 100644 --- a/machines/templates/machines/index_machinetype.html +++ b/machines/templates/machines/index_machinetype.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -31,14 +31,17 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% trans "List of machine types" %}

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

{% trans "List of machine types" %}

+ {% can_create MachineType %} + + {% trans " Add a machine type" %} + + {% acl_end %} + + {% trans " Delete one or several machine types" %} + + {% include 'machines/aff_machinetype.html' with machinetype_list=machinetype_list %} +
+
+
{% endblock %} - diff --git a/machines/templates/machines/index_nas.html b/machines/templates/machines/index_nas.html index 88f68213..bb17395a 100644 --- a/machines/templates/machines/index_nas.html +++ b/machines/templates/machines/index_nas.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -31,15 +31,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% 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 %} - {% trans " Add a NAS device type" %} - {% acl_end %} - {% trans " Delete one or several NAS device types" %} - {% include "machines/aff_nas.html" with nas_list=nas_list %} -
-
-
+

{% 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 %} + + {% trans " Add a NAS device type" %} + + {% acl_end %} + + {% trans " Delete one or several NAS device types" %} + + {% include 'machines/aff_nas.html' with nas_list=nas_list %} +
+
+
{% endblock %} - diff --git a/machines/templates/machines/index_portlist.html b/machines/templates/machines/index_portlist.html index 0d3d0741..c540478d 100644 --- a/machines/templates/machines/index_portlist.html +++ b/machines/templates/machines/index_portlist.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% extends 'machines/sidebar.html' %} {% load bootstrap3 %} @@ -8,60 +8,58 @@ {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% trans "List of ports configurations" %}

- {% can_create OuverturePortList %} - {% trans " Add a configuration" %} - {% acl_end %} - - - - - - - - - - - - - {% for pl in port_list %} - - - - - - - - - {%endfor%} -
{% trans "Name" %}{% trans "TCP (input)" %}{% trans "TCP (output)" %}{% trans "UDP (input)" %}{% trans "UDP (output)" %}{% trans "Machines" %}
{{pl.name}}{% for p in pl.tcp_ports_in %}{{p.show_port}}, {%endfor%}{% for p in pl.tcp_ports_out %}{{p.show_port}}, {%endfor%}{% for p in pl.udp_ports_in %}{{p.show_port}}, {%endfor%}{% for p in pl.udp_ports_out %}{{p.show_port}}, {%endfor%} - {% if pl.interface_set.all %} - - {% endif %} - - {% can_edit pl %} - {% include 'buttons/edit.html' with href='machines:edit-portlist' id=pl.id %} - {% acl_end %} - {% can_delete pl %} - {% include 'buttons/suppr.html' with href='machines:del-portlist' id=pl.id %} - {% acl_end %} -
-
-
-
- +

{% trans "List of ports configurations" %}

+ {% can_create OuverturePortList %} + + {% trans " Add a configuration" %} + + {% acl_end %} + + + + + + + + + + + + + {% for pl in port_list %} + + + + + + + + + {% endfor %} +
{% trans "Name" %}{% trans "TCP (input)" %}{% trans "TCP (output)" %}{% trans "UDP (input)" %}{% trans "UDP (output)" %}{% trans "Machines" %}
{{ pl.name }}{% for p in pl.tcp_ports_in %}{{ p.show_port }}, {% endfor %}{% for p in pl.tcp_ports_out %}{{ p.show_port }}, {% endfor %}{% for p in pl.udp_ports_in %}{{ p.show_port }}, {% endfor %}{% for p in pl.udp_ports_out %}{{ p.show_port }}, {% endfor %} + {% if pl.interface_set.all %} + + {% endif %} + + {% can_edit pl %} + {% include 'buttons/edit.html' with href='machines:edit-portlist' id=pl.id %} + {% acl_end %} + {% can_delete pl %} + {% include 'buttons/suppr.html' with href='machines:del-portlist' id=pl.id %} + {% acl_end %} +
{% endblock %} - diff --git a/machines/templates/machines/index_role.html b/machines/templates/machines/index_role.html index ddc2ea8b..38799aa5 100644 --- a/machines/templates/machines/index_role.html +++ b/machines/templates/machines/index_role.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -30,13 +30,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {% 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 %} -
-
+

{% 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 9dec7032..19244e71 100644 --- a/machines/templates/machines/index_service.html +++ b/machines/templates/machines/index_service.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -30,16 +30,15 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% trans "List of services" %}

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

{% trans "States of servers" %}

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

{% trans "List of services" %}

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

{% trans "States of servers" %}

+ {% include 'machines/aff_servers.html' with servers_list=servers_list %} +{% endblock %} diff --git a/machines/templates/machines/index_sshfp.html b/machines/templates/machines/index_sshfp.html index ce16d621..c1508eaf 100644 --- a/machines/templates/machines/index_sshfp.html +++ b/machines/templates/machines/index_sshfp.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -34,6 +34,5 @@ with this program; if not, write to the Free Software Foundation, Inc., {% trans " Add an SSH fingerprint" %} {% acl_end %} - {% include "machines/aff_sshfp.html" with sshfp_list=sshfp_list %} + {% 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 7af31fd5..830cde65 100644 --- a/machines/templates/machines/index_vlan.html +++ b/machines/templates/machines/index_vlan.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -24,21 +24,18 @@ with this program; if not, write to the Free Software Foundation, Inc., {% endcomment %} {% load bootstrap3 %} - {% load acl %} {% load i18n %} {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -

{% trans "List of VLANs" %}

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

{% trans "List of VLANs" %}

+ {% can_create Vlan %} + {% trans " Add a VLAN" %} + {% acl_end %} + {% trans " Delete one or several VLANs" %} + {% include 'machines/aff_vlan.html' with vlan_list=vlan_list %} {% endblock %} - diff --git a/machines/templates/machines/machine.html b/machines/templates/machines/machine.html index 432d11f8..27c7ea27 100644 --- a/machines/templates/machines/machine.html +++ b/machines/templates/machines/machine.html @@ -1,4 +1,4 @@ -{% extends "machines/sidebar.html" %} +{% 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 @@ -31,147 +31,144 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Machines" %}{% endblock %} {% block content %} -{% if machineform %} - {% bootstrap_form_errors machineform %} -{% endif %} -{% if interfaceform %} - {% bootstrap_form_errors interfaceform %} -{% endif %} -{% if domainform %} - {% bootstrap_form_errors domainform %} -{% endif %} -{% if iptypeform %} - {% bootstrap_form_errors iptypeform %} -{% endif %} -{% if machinetypeform %} - {% bootstrap_form_errors machinetypeform %} -{% endif %} -{% if extensionform %} - {% bootstrap_form_errors extensionform %} -{% endif %} -{% if mxform %} - {% bootstrap_form_errors mxform %} -{% endif %} -{% if nsform %} - {% bootstrap_form_errors nsform %} -{% endif %} -{% if txtform %} - {% bootstrap_form_errors txtform %} -{% endif %} -{% if dnameform %} - {% bootstrap_form_errors dnameform %} -{% endif %} -{% if srvform %} - {% bootstrap_form_errors srvform %} -{% endif %} -{% if aliasform %} - {% bootstrap_form_errors aliasform %} -{% endif %} -{% if serviceform %} - {% bootstrap_form_errors serviceform %} -{% endif %} -{% if sshfpform %} - {% bootstrap_form_errors sshfpform %} -{% endif %} -{% if roleform %} - {% bootstrap_form_errors roleform %} -{% endif %} -{% if vlanform %} - {% bootstrap_form_errors vlanform %} -{% endif %} -{% if nasform %} - {% bootstrap_form_errors nasform %} -{% endif %} -{% if ipv6form %} - {% bootstrap_form_errors ipv6form %} -{% endif %} - -
- {% csrf_token %} {% if machineform %} -

{% trans "Machine" %}

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

{% trans "Interface" %}

- {% if i_mbf_param %} - {% massive_bootstrap_form interfaceform 'ipv4,machine' mbf_param=i_mbf_param %} - {% else %} - {% massive_bootstrap_form interfaceform 'ipv4,machine' %} - {% endif %} + {% bootstrap_form_errors interfaceform %} {% endif %} {% if domainform %} -

{% trans "Domain" %}

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

{% trans "IP type" %}

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

{% trans "Machine type" %}

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

{% trans "Extension" %}

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

{% trans "SOA record" %}

- {% bootstrap_form soaform %} + {% bootstrap_form_errors extensionform %} {% endif %} {% if mxform %} -

{% trans "MX record" %}

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

{% trans "NS record" %}

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

{% trans "TXT record" %}

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

{% trans "DNAME record" %}

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

{% trans "SRV record" %}

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

{% trans "SSHFP record" %}

- {% bootstrap_form sshfpform %} + {% bootstrap_form_errors srvform %} {% endif %} {% if aliasform %} -

{% trans "Alias" %}

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

{% trans "Service" %}

- {% massive_bootstrap_form serviceform 'servers' %} + {% bootstrap_form_errors serviceform %} + {% endif %} + {% if sshfpform %} + {% bootstrap_form_errors sshfpform %} {% endif %} {% if roleform %} -

Role

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

{% trans "VLAN" %}

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

{% trans "NAS device" %}

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

{% trans "IPv6 address" %}

- {% bootstrap_form ipv6form %} + {% bootstrap_form_errors ipv6form %} {% endif %} - {% bootstrap_button action_name button_type="submit" icon="star" %} -
-
-
-
+ +
+ {% csrf_token %} + {% if machineform %} +

{% trans "Machine" %}

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

{% trans "Interface" %}

+ {% if i_mbf_param %} + {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' mbf_param=i_mbf_param %} + {% else %} + {% massive_bootstrap_form interfaceform 'ipv4,machine,port_lists' %} + {% endif %} + {% endif %} + {% if domainform %} +

{% trans "Domain" %}

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

{% trans "IP type" %}

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

{% trans "Machine type" %}

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

{% trans "Extension" %}

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

{% trans "SOA record" %}

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

{% trans "MX record" %}

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

{% trans "NS record" %}

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

{% trans "TXT record" %}

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

{% trans "DNAME record" %}

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

{% trans "SRV record" %}

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

{% trans "SSHFP record" %}

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

{% trans "Alias" %}

+ {% massive_bootstrap_form aliasform 'extension' %} + {% endif %} + {% if serviceform %} +

{% trans "Service" %}

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

Role

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

{% trans "VLAN" %}

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

{% trans "NAS device" %}

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

{% trans "IPv6 address" %}

+ {% bootstrap_form ipv6form %} + {% endif %} + {% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %} +
{% endblock %} diff --git a/machines/templates/machines/sidebar.html b/machines/templates/machines/sidebar.html index 68e14ae0..f7c63f6f 100644 --- a/machines/templates/machines/sidebar.html +++ b/machines/templates/machines/sidebar.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.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 @@ -28,57 +28,57 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block sidebar %} {% can_view_all Machine %} - + - {% trans "Machines" %} + {% trans "Machines" %} {% acl_end %} {% can_view_all MachineType %} - + {% trans "Machine types" %} {% acl_end %} {% can_view_all Extension %} - + - {% trans "Extensions and zones" %} + {% trans "Extensions and zones" %} {% acl_end %} {% can_view_all IpType %} - + - {% trans "IP ranges" %} + {% trans "IP ranges" %} {% acl_end %} {% can_view_all Vlan %} - + - {% trans "VLANs" %} + {% trans "VLANs" %} {% acl_end %} {% can_view_all Nas %} - + - {% trans "NAS devices" %} + {% trans "NAS devices" %} {% acl_end %} {% can_view_all machines.Service %} - + - {% trans "Services (DHCP, DNS, ...)" %} + {% trans "Services (DHCP, DNS, ...)" %} {% acl_end %} {% can_view_all Role %} - + {% trans "Server roles" %} {% acl_end %} {% can_view_all OuverturePortList %} - + - {% trans "Ports openings" %} + {% trans "Ports openings" %} {% acl_end %} {% endblock %} diff --git a/machines/urls.py b/machines/urls.py index d6f3a541..77fb5e2e 100644 --- a/machines/urls.py +++ b/machines/urls.py @@ -27,6 +27,7 @@ The defined URLs for the Machines app from __future__ import unicode_literals from django.conf.urls import url + from . import views urlpatterns = [ diff --git a/machines/views.py b/machines/views.py index 02c3c671..59d4bd5a 100644 --- a/machines/views.py +++ b/machines/views.py @@ -32,26 +32,18 @@ The views for the Machines app from __future__ import unicode_literals -from django.urls import reverse -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.http import HttpResponse +from django.shortcuts import render, redirect +from django.urls import reverse from django.utils.translation import ugettext as _ - +from django.views.decorators.csrf import csrf_exempt from rest_framework.renderers import JSONRenderer -from users.models import User from preferences.models import GeneralOption -from re2o.utils import ( - all_active_assigned_interfaces, - filter_active_interfaces, - SortTable, - re2o_paginator, -) from re2o.acl import ( can_create, can_edit, @@ -60,21 +52,16 @@ from re2o.acl import ( can_view_all, can_delete_set, ) -from re2o.views import form - -from .serializers import ( - FullInterfaceSerializer, - InterfaceSerializer, - TypeSerializer, - DomainSerializer, - TxtSerializer, - SrvSerializer, - MxSerializer, - ExtensionSerializer, - ServiceServersSerializer, - NsSerializer, +from re2o.utils import ( + all_active_assigned_interfaces, + filter_active_interfaces, ) - +from re2o.base import ( + SortTable, + re2o_paginator, +) +from re2o.views import form +from users.models import User from .forms import ( NewMachineForm, EditMachineForm, @@ -139,6 +126,18 @@ from .models import ( OuverturePort, Ipv6List, ) +from .serializers import ( + FullInterfaceSerializer, + InterfaceSerializer, + TypeSerializer, + DomainSerializer, + TxtSerializer, + SrvSerializer, + MxSerializer, + ExtensionSerializer, + ServiceServersSerializer, + NsSerializer, +) def f_type_id(is_type_tt): @@ -153,12 +152,12 @@ def generate_ipv4_choices(form_obj): """ f_ipv4 = form_obj.fields['ipv4'] used_mtype_id = [] - choices = '{"":[{key:"",value:"'+_("Select a machine type first.") + '"}' + choices = '{"":[{key:"",value:"' + _("Select a machine type first.") + '"}' mtype_id = -1 for ip in (f_ipv4.queryset - .annotate(mtype_id=F('ip_type__machinetype__id')) - .order_by('mtype_id', 'id')): + .annotate(mtype_id=F('ip_type__machinetype__id')) + .order_by('mtype_id', 'id')): if mtype_id != ip.mtype_id: mtype_id = ip.mtype_id used_mtype_id.append(mtype_id) @@ -172,7 +171,7 @@ def generate_ipv4_choices(form_obj): ) for t in form_obj.fields['type'].queryset.exclude(id__in=used_mtype_id): - choices += '], "'+str(t.id)+'": [' + choices += '], "' + str(t.id) + '": [' choices += '{key: "", value: "' + str(f_ipv4.empty_label) + '"},' choices += ']}' return choices @@ -889,6 +888,7 @@ def del_ns(request, instances): request ) + @login_required @can_create(DName) def add_dname(request): @@ -935,9 +935,9 @@ def del_dname(request, instances): messages.success(request, _("The DNAME record was deleted.")) except ProtectedError: messages.error( - request, - _("Error: the DNAME record %s can't be deleted.") - % dname_del + request, + _("Error: the DNAME record %s can't be deleted.") + % dname_del ) return redirect(reverse('machines:index-extension')) return form( @@ -1252,16 +1252,16 @@ def del_service(request, instances): request ) + @login_required @can_edit(Service) -def regen_service(request,service, **_kwargs): +def regen_service(request, service, **_kwargs): """Ask for a regen of the service""" regen(service) return index_service(request) - @login_required @can_create(Vlan) def add_vlan(request): @@ -1388,10 +1388,10 @@ def index(request): .prefetch_related('interface_set__domain__extension') .prefetch_related('interface_set__ipv4__ip_type') .prefetch_related( - 'interface_set__type__ip_type__extension' - ).prefetch_related( - 'interface_set__domain__related_domain__extension' - ).prefetch_related('interface_set__ipv6list')) + 'interface_set__type__ip_type__extension' + ).prefetch_related( + 'interface_set__domain__related_domain__extension' + ).prefetch_related('interface_set__ipv6list')) machines_list = SortTable.sort( machines_list, request.GET.get('col'), @@ -1548,8 +1548,8 @@ def index_role(request): """ View used to display the list of existing roles """ role_list = (Role.objects .prefetch_related( - 'servers__domain__extension' - ).all()) + 'servers__domain__extension' + ).all()) return render( request, 'machines/index_role.html', @@ -1563,8 +1563,8 @@ def index_service(request): """ View used to display the list of existing services """ service_list = (Service.objects .prefetch_related( - 'service_link_set__server__domain__extension' - ).all()) + 'service_link_set__server__domain__extension' + ).all()) servers_list = (Service_link.objects .select_related('server__domain__extension') .select_related('service') @@ -1740,10 +1740,10 @@ def alias(_request): alias = (Domain.objects .filter(interface_parent=None) .filter( - cname__in=Domain.objects.filter( - interface_parent__in=Interface.objects.exclude(ipv4=None) - ) - ).select_related('extension') + cname__in=Domain.objects.filter( + interface_parent__in=Interface.objects.exclude(ipv4=None) + ) + ).select_related('extension') .select_related('cname__extension')) seria = DomainSerializer(alias, many=True) return JSONResponse(seria.data) @@ -1801,10 +1801,10 @@ def ns(_request): """ API view to list the NS records """ ns = (Ns.objects .exclude( - ns__in=Domain.objects.filter( - interface_parent__in=Interface.objects.filter(ipv4=None) - ) - ).select_related('zone') + ns__in=Domain.objects.filter( + interface_parent__in=Interface.objects.filter(ipv4=None) + ) + ).select_related('zone') .select_related('ns__extension')) seria = NsSerializer(ns, many=True) return JSONResponse(seria.data) @@ -1859,9 +1859,9 @@ def ouverture_ports(_request): """ API view to list the port policies for each IP """ r = {'ipv4': {}, 'ipv6': {}} for o in (OuverturePortList.objects - .all() - .prefetch_related('ouvertureport_set') - .prefetch_related('interface_set', 'interface_set__ipv4')): + .all() + .prefetch_related('ouvertureport_set') + .prefetch_related('interface_set', 'interface_set__ipv4')): pl = { "tcp_in": set(map( str, @@ -1926,17 +1926,16 @@ def regen_achieved(request): """ API view to list the regen status for each (Service link, Server) couple """ obj = (Service_link.objects - .filter( - service__in=Service.objects.filter( - service_type=request.POST['service'] - ), - server__in=Interface.objects.filter( - domain__in=Domain.objects.filter( - name=request.POST['server'] - ) - ) - )) + .filter( + service__in=Service.objects.filter( + service_type=request.POST['service'] + ), + server__in=Interface.objects.filter( + domain__in=Domain.objects.filter( + name=request.POST['server'] + ) + ) + )) if obj: obj.first().done_regen() return HttpResponse("Ok") - diff --git a/preferences/admin.py b/preferences/admin.py index 5ca90095..efeefc87 100644 --- a/preferences/admin.py +++ b/preferences/admin.py @@ -37,7 +37,11 @@ from .models import ( MailContact, AssoOption, MailMessageOption, - HomeOption + HomeOption, + RadiusKey, + SwitchManagementCred, + Reminder, + DocumentTemplate ) @@ -86,6 +90,24 @@ class HomeOptionAdmin(VersionAdmin): pass +class RadiusKeyAdmin(VersionAdmin): + """Class radiuskey""" + pass + +class SwitchManagementCredAdmin(VersionAdmin): + """Class managementcred for switch""" + pass + +class ReminderAdmin(VersionAdmin): + """Class reminder for switch""" + pass + + +class DocumentTemplateAdmin(VersionAdmin): + """Admin class for DocumentTemplate""" + pass + + admin.site.register(OptionalUser, OptionalUserAdmin) admin.site.register(OptionalMachine, OptionalMachineAdmin) admin.site.register(OptionalTopologie, OptionalTopologieAdmin) @@ -93,5 +115,9 @@ admin.site.register(GeneralOption, GeneralOptionAdmin) admin.site.register(HomeOption, HomeOptionAdmin) admin.site.register(Service, ServiceAdmin) admin.site.register(MailContact, MailContactAdmin) +admin.site.register(Reminder, ReminderAdmin) +admin.site.register(RadiusKey, RadiusKeyAdmin) +admin.site.register(SwitchManagementCred, SwitchManagementCredAdmin) admin.site.register(AssoOption, AssoOptionAdmin) admin.site.register(MailMessageOption, MailMessageOptionAdmin) +admin.site.register(DocumentTemplate, DocumentTemplateAdmin) diff --git a/preferences/forms.py b/preferences/forms.py index b9ccd531..d2bede7c 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -38,8 +38,15 @@ from .models import ( MailMessageOption, HomeOption, Service, - MailContact + MailContact, + Reminder, + RadiusKey, + SwitchManagementCred, + RadiusOption, + CotisationsOption, + DocumentTemplate ) +from topologie.models import Switch class EditOptionalUserForm(ModelForm): @@ -92,7 +99,14 @@ class EditOptionalMachineForm(ModelForm): class EditOptionalTopologieForm(ModelForm): - """Options de topologie, formulaire d'edition (vlan par default etc)""" + """Options de topologie, formulaire d'edition (vlan par default etc) + On rajoute un champ automatic provision switchs pour gérer facilement + l'ajout de switchs au provisionning automatique""" + automatic_provision_switchs = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False + ) + class Meta: model = OptionalTopologie fields = '__all__' @@ -104,11 +118,14 @@ class EditOptionalTopologieForm(ModelForm): prefix=prefix, **kwargs ) - 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") + + self.initial['automatic_provision_switchs'] = Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name') + + def save(self, commit=True): + instance = super().save(commit) + Switch.objects.all().update(automatic_provision=False) + self.cleaned_data['automatic_provision_switchs'].update(automatic_provision=True) + return instance class EditGeneralOptionForm(ModelForm): @@ -208,6 +225,41 @@ class EditHomeOptionForm(ModelForm): self.fields['twitter_account_name'].label = _("Twitter account name") +class EditRadiusOptionForm(ModelForm): + """Edition forms for Radius options""" + class Meta: + model = RadiusOption + fields = '__all__' + + def clean(self): + cleaned_data = super().clean() + ignored=('radius_general_policy', 'vlan_decision_ok') + fields = ( + f for f in self.fields.keys() + if 'vlan' not in f and f not in ignored + ) + for f in fields: + choice = cleaned_data.get(f) + vlan = cleaned_data.get(f+'_vlan') + if choice == RadiusOption.SET_VLAN and vlan is None: + self.add_error( + f, + _("You chose to set vlan but did not set any VLAN."), + ) + self.add_error( + f+'_vlan', + _("Please, choose a VLAN."), + ) + return cleaned_data + + +class EditCotisationsOptionForm(ModelForm): + """Edition forms for Cotisations options""" + class Meta: + model = CotisationsOption + fields = '__all__' + + class ServiceForm(ModelForm): """Edition, ajout de services sur la page d'accueil""" class Meta: @@ -239,8 +291,68 @@ class DelServiceForm(Form): else: self.fields['services'].queryset = Service.objects.all() -class MailContactForm(FormRevMixin, ModelForm): - """Edit and add contact email adress""" +class ReminderForm(FormRevMixin, ModelForm): + """Edition, ajout de services sur la page d'accueil""" + class Meta: + model = Reminder + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(ReminderForm, self).__init__(*args, prefix=prefix, **kwargs) + + +class RadiusKeyForm(FormRevMixin, ModelForm): + """Edition, ajout de clef radius""" + members = forms.ModelMultipleChoiceField( + queryset=Switch.objects.all(), + required=False + ) + + class Meta: + model = RadiusKey + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(RadiusKeyForm, self).__init__(*args, prefix=prefix, **kwargs) + instance = kwargs.get('instance', None) + if instance: + self.initial['members'] = Switch.objects.filter(radius_key=instance) + + def save(self, commit=True): + instance = super().save(commit) + instance.switch_set = self.cleaned_data['members'] + return instance + + +class SwitchManagementCredForm(FormRevMixin, ModelForm): + """Edition, ajout de creds de management pour gestion + et interface rest des switchs""" + members = forms.ModelMultipleChoiceField( + Switch.objects.all(), + required=False + ) + + class Meta: + model = SwitchManagementCred + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(SwitchManagementCredForm, self).__init__(*args, prefix=prefix, **kwargs) + instance = kwargs.get('instance', None) + if instance: + self.initial['members'] = Switch.objects.filter(management_creds=instance) + + def save(self, commit=True): + instance = super().save(commit) + instance.switch_set = self.cleaned_data['members'] + return instance + + +class MailContactForm(ModelForm): + """Edition, ajout d'adresse de contact""" class Meta: model = MailContact fields = '__all__' @@ -254,7 +366,7 @@ class DelMailContactForm(Form): """Delete contact email adress""" mailcontacts = forms.ModelMultipleChoiceField( queryset=MailContact.objects.none(), - label="Enregistrements adresses actuels", + label=_("Current email addresses"), widget=forms.CheckboxSelectMultiple ) @@ -266,3 +378,36 @@ class DelMailContactForm(Form): else: self.fields['mailcontacts'].queryset = MailContact.objects.all() + +class DocumentTemplateForm(FormRevMixin, ModelForm): + """ + Form used to create a document template. + """ + class Meta: + model = DocumentTemplate + fields = '__all__' + + def __init__(self, *args, **kwargs): + prefix = kwargs.pop('prefix', self.Meta.model.__name__) + super(DocumentTemplateForm, self).__init__( + *args, prefix=prefix, **kwargs) + + +class DelDocumentTemplateForm(FormRevMixin, Form): + """ + Form used to delete one or more document templatess. + The use must choose the one to delete by checking the boxes. + """ + document_templates = forms.ModelMultipleChoiceField( + queryset=DocumentTemplate.objects.none(), + label=_("Available document templates"), + widget=forms.CheckboxSelectMultiple + ) + + def __init__(self, *args, **kwargs): + instances = kwargs.pop('instances', None) + super(DelDocumentTemplateForm, self).__init__(*args, **kwargs) + if instances: + self.fields['document_templates'].queryset = instances + else: + self.fields['document_templates'].queryset = Banque.objects.all() diff --git a/preferences/locale/fr/LC_MESSAGES/django.mo b/preferences/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 657adab3..00000000 Binary files a/preferences/locale/fr/LC_MESSAGES/django.mo and /dev/null differ diff --git a/preferences/locale/fr/LC_MESSAGES/django.po b/preferences/locale/fr/LC_MESSAGES/django.po index 3535b8d7..3528330e 100644 --- a/preferences/locale/fr/LC_MESSAGES/django.po +++ b/preferences/locale/fr/LC_MESSAGES/django.po @@ -21,7 +21,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-18 13:26+0200\n" +"POT-Creation-Date: 2019-01-12 15:13+0100\n" "PO-Revision-Date: 2018-06-24 15:54+0200\n" "Last-Translator: Laouen Fernet \n" "Language-Team: \n" @@ -34,264 +34,386 @@ msgstr "" msgid "You don't have the right to view this application." msgstr "Vous n'avez pas le droit de voir cette application." -#: forms.py:59 templates/preferences/display_preferences.html:41 +#: forms.py:63 templates/preferences/display_preferences.html:142 msgid "Telephone number required" msgstr "Numéro de téléphone requis" -#: forms.py:61 +#: forms.py:65 msgid "GPG fingerprint" msgstr "Empreinte GPG" -#: forms.py:62 +#: forms.py:66 msgid "All can create a club" msgstr "Tous peuvent créer un club" -#: forms.py:63 +#: forms.py:67 msgid "All can create a member" msgstr "Tous peuvent créer un adhérent" -#: forms.py:64 templates/preferences/display_preferences.html:43 +#: forms.py:68 templates/preferences/display_preferences.html:120 msgid "Self registration" msgstr "Autoinscription" -#: forms.py:65 +#: forms.py:69 msgid "Default shell" -msgstr "Interface système par défaut" +msgstr "Interface en ligne de commande par défaut" -#: forms.py:81 +#: forms.py:85 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 +#: forms.py:87 templates/preferences/display_preferences.html:172 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 +#: forms.py:91 templates/preferences/display_preferences.html:176 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 +#: forms.py:94 msgid "IPv6 mode" msgstr "Mode IPv6" -#: forms.py:91 +#: forms.py:95 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:141 +msgid "General message in French" +msgstr "Message général en français" -#: 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:142 +msgid "General message in English" +msgstr "Message général en anglais" -#: 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 +#: forms.py:143 templates/preferences/display_preferences.html:58 msgid "Number of results displayed when searching" msgstr "Nombre de résultats affichés lors de la recherche" -#: forms.py:131 +#: forms.py:146 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 +#: forms.py:149 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 +#: forms.py:152 templates/preferences/display_preferences.html:66 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 +#: forms.py:155 templates/preferences/display_preferences.html:52 msgid "Website name" msgstr "Nom du site" -#: forms.py:141 templates/preferences/display_preferences.html:133 +#: forms.py:156 templates/preferences/display_preferences.html:54 msgid "Email address for automatic emailing" msgstr "Adresse mail pour les mails automatiques" -#: forms.py:143 templates/preferences/display_preferences.html:151 +#: forms.py:158 templates/preferences/display_preferences.html:76 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 +#: forms.py:160 templates/preferences/display_preferences.html:78 msgid "General Terms of Use" msgstr "Conditions Générales d'Utilisation" -#: forms.py:161 +#: forms.py:176 msgid "Organisation name" msgstr "Nom de l'association" -#: forms.py:162 templates/preferences/display_preferences.html:170 +#: forms.py:177 templates/preferences/display_preferences.html:323 msgid "SIRET number" msgstr "Numéro SIRET" -#: forms.py:163 +#: forms.py:178 msgid "Address (line 1)" msgstr "Adresse (ligne 1)" -#: forms.py:164 +#: forms.py:179 msgid "Address (line 2)" msgstr "Adresse (ligne 2)" -#: forms.py:165 models.py:288 -#: templates/preferences/display_preferences.html:178 +#: forms.py:180 models.py:476 +#: templates/preferences/display_preferences.html:331 msgid "Contact email address" msgstr "Adresse mail de contact" -#: forms.py:166 templates/preferences/display_preferences.html:182 +#: forms.py:181 templates/preferences/display_preferences.html:335 msgid "Telephone number" msgstr "Numéro de téléphone" -#: forms.py:167 templates/preferences/display_preferences.html:184 +#: forms.py:182 templates/preferences/display_preferences.html:337 msgid "Usual name" msgstr "Nom d'usage" -#: forms.py:168 +#: forms.py:183 msgid "Account used for editing from /admin" msgstr "Compte utilisé pour les modifications depuis /admin" -#: forms.py:170 +#: forms.py:185 msgid "Payment" msgstr "Paiement" -#: forms.py:171 +#: forms.py:186 msgid "Payment ID" msgstr "ID de paiement" -#: forms.py:172 +#: forms.py:187 msgid "Payment password" msgstr "Mot de passe de paiement" -#: forms.py:173 forms.py:224 templates/preferences/aff_service.html:33 +#: forms.py:188 forms.py:267 templates/preferences/aff_service.html:33 msgid "Description" msgstr "Description" -#: forms.py:189 +#: forms.py:204 msgid "Message for the French welcome email" msgstr "Message pour le mail de bienvenue en français" -#: forms.py:191 +#: forms.py:206 msgid "Message for the English welcome email" msgstr "Message pour le mail de bienvenue en anglais" -#: forms.py:208 +#: forms.py:223 msgid "Facebook URL" msgstr "URL du compte Facebook" -#: forms.py:209 +#: forms.py:224 msgid "Twitter URL" msgstr "URL du compte Twitter" -#: forms.py:210 templates/preferences/display_preferences.html:233 +#: forms.py:225 templates/preferences/display_preferences.html:445 msgid "Twitter account name" msgstr "Nom du compte Twitter" -#: forms.py:222 templates/preferences/aff_service.html:31 -#: templates/preferences/display_preferences.html:168 +#: forms.py:247 +msgid "You chose to set vlan but did not set any VLAN." +msgstr "" +"Vous avez choisi de paramétrer vlan mais vous n'avez indiqué aucun VLAN." + +#: forms.py:251 +msgid "Please, choose a VLAN." +msgstr "Veuillez choisir un VLAN." + +#: forms.py:265 templates/preferences/aff_service.html:31 +#: templates/preferences/display_preferences.html:321 msgid "Name" msgstr "Nom" -#: forms.py:223 templates/preferences/aff_service.html:32 +#: forms.py:266 templates/preferences/aff_service.html:32 msgid "URL" msgstr "URL" -#: forms.py:225 templates/preferences/aff_service.html:34 +#: forms.py:268 templates/preferences/aff_service.html:34 msgid "Image" msgstr "Image" -#: forms.py:232 +#: forms.py:275 msgid "Current services" msgstr "Services actuels" -#: models.py:71 -msgid "Users can create a club" -msgstr "Les utilisateurs peuvent créer un club" +#: forms.py:362 +msgid "Current email addresses" +msgstr "Adresses mail actuelles" -#: models.py:75 -msgid "Users can create a member" -msgstr "Les utilisateurs peuvent créer un adhérent" +#: models.py:76 +msgid "Users can create a club." +msgstr "Les utilisateurs peuvent créer un club." -#: 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:80 +msgid "Users can create a member." +msgstr "Les utilisateurs peuvent créer un adhérent." -#: 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:91 +msgid "Users can edit their shell." +msgstr "Les utilisateurs peuvent modifier leur interface en ligne de commande." -#: models.py:93 -msgid "Enable local email accounts for users" -msgstr "Active les comptes mail locaux pour les utilisateurs" +#: models.py:95 +msgid "Users can edit their room." +msgstr "Les utilisateurs peuvent modifier leur chambre." -#: models.py:98 +#: models.py:99 +msgid "Enable local email accounts for users." +msgstr "Activer les comptes mail locaux pour les utilisateurs." + +#: models.py:104 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 "Maximum number of local email addresses for a standard user." +msgstr "" +"Nombre maximum d'adresses mail locales autorisé pour un utilisateur standard." + +#: models.py:113 +msgid "Not yet active users will be deleted after this number of days." +msgstr "" +"Les utilisateurs n'ayant jamais adhéré seront supprimés après ce nombre de " +"jours." + +#: models.py:118 +msgid "A new user can create their account on Re2o." +msgstr "Un nouvel utilisateur peut créer son compte sur Re2o." + +#: models.py:122 +msgid "" +"If True, all new created and connected users are active. If False, only when " +"a valid registration has been paid." +msgstr "" +"Si True, tous les nouveaux utilisations créés et connectés sont actifs. Si " +"False, seulement quand une inscription validée a été payée." + +#: models.py:128 msgid "Can view the user options" msgstr "Peut voir les options d'utilisateur" -#: models.py:110 +#: models.py:130 msgid "user options" msgstr "options d'utilisateur" -#: models.py:117 +#: models.py:137 msgid "Email domain must begin with @" msgstr "Un domaine mail doit commencer par @" -#: models.py:135 +#: models.py:155 msgid "Autoconfiguration by RA" msgstr "Configuration automatique par RA" -#: models.py:136 +#: models.py:156 msgid "IP addresses assigning by DHCPv6" msgstr "Attribution d'adresses IP par DHCPv6" -#: models.py:137 +#: models.py:157 msgid "Disabled" msgstr "Désactivé" -#: models.py:159 +#: models.py:179 msgid "Can view the machine options" msgstr "Peut voir les options de machine" -#: models.py:161 +#: models.py:181 msgid "machine options" msgstr "options de machine" -#: models.py:180 +#: models.py:200 models.py:592 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 +#: models.py:201 models.py:593 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 +#: models.py:210 templates/preferences/display_preferences.html:246 +msgid "Web management, activated in case of automatic provision" +msgstr "Gestion web, activée en cas de provision automatique" + +#: models.py:214 +msgid "" +"SSL web management, make sure that a certificate is installed on the switch" +msgstr "" +"Gestion web SSL, vérifiez qu'un certificat est installé sur le commutateur " +"réseau" + +#: models.py:219 templates/preferences/display_preferences.html:248 +msgid "REST management, activated in case of automatic provision" +msgstr "Gestion REST, activée en cas de provision automatique" + +#: models.py:226 templates/preferences/display_preferences.html:262 +msgid "IP range for the management of switches" +msgstr "Plage d'IP pour la gestion des commutateurs réseau" + +#: models.py:232 templates/preferences/display_preferences.html:270 +msgid "Provision of configuration mode for switches" +msgstr "Mode de provision de configuration pour les commutateurs réseau" + +#: models.py:238 +msgid "SFTP login for switches" +msgstr "Identifiant SFTP pour les commutateurs réseau" + +#: models.py:244 +msgid "SFTP password" +msgstr "Mot de passe SFTP" + +#: models.py:303 msgid "Can view the topology options" msgstr "Peut voir les options de topologie" -#: models.py:208 +#: models.py:305 msgid "topology options" msgstr "options de topologie" -#: models.py:225 +#: models.py:319 models.py:337 +msgid "RADIUS key" +msgstr "Clé RADIUS" + +#: models.py:325 +msgid "Comment for this key" +msgstr "Commentaire pour cette clé" + +#: models.py:330 +msgid "Default key for switches" +msgstr "Clé par défaut pour les commutateurs réseau" + +#: models.py:335 +msgid "Can view a RADIUS key object" +msgstr "Peut voir un objet clé RADIUS" + +#: models.py:338 templates/preferences/display_preferences.html:221 +msgid "RADIUS keys" +msgstr "clés RADIUS" + +#: models.py:341 +msgid "RADIUS key " +msgstr "clé RADIUS " + +#: models.py:348 templates/preferences/aff_switchmanagementcred.html:31 +msgid "Switch login" +msgstr "Identifiant du commutateur réseau" + +#: models.py:352 +msgid "Password" +msgstr "Mot de passe" + +#: models.py:357 +msgid "Default credentials for switches" +msgstr "Identifiants par défaut pour les commutateurs réseau" + +#: models.py:362 +msgid "Can view a switch management credentials object" +msgstr "Peut voir un objet identifiants de gestion de commutateur réseau" + +#: models.py:365 +msgid "switch management credentials" +msgstr "identifiants de gestion de commutateur réseau" + +#: models.py:368 +msgid "Switch login " +msgstr "Identifiant du commutateur réseau " + +#: models.py:380 +msgid "Delay between the email and the membership's end" +msgstr "Délai entre le mail et la fin d'adhésion" + +#: models.py:387 +msgid "Message displayed specifically for this reminder" +msgstr "Message affiché spécifiquement pour ce rappel" + +#: models.py:392 +msgid "Can view a reminder object" +msgstr "Peut voir un objet rappel" + +#: models.py:394 +msgid "reminder" +msgstr "rappel" + +#: models.py:395 +msgid "reminders" +msgstr "rappels" + +#: models.py:412 msgid "" "General message displayed on the French version of the website (e.g. in case " "of maintenance)" @@ -299,7 +421,7 @@ msgstr "" "Message général affiché sur la version française du site (ex : en cas de " "maintenance)" -#: models.py:231 +#: models.py:418 msgid "" "General message displayed on the English version of the website (e.g. in " "case of maintenance)" @@ -307,169 +429,199 @@ msgstr "" "Message général affiché sur la version anglaise du site (ex : en cas de " "maintenance)" -#: models.py:253 +#: models.py:441 msgid "Can view the general options" msgstr "Peut voir les options générales" -#: models.py:255 +#: models.py:443 msgid "general options" msgstr "options générales" -#: models.py:275 +#: models.py:463 msgid "Can view the service options" msgstr "Peut voir les options de service" -#: models.py:277 +#: models.py:465 msgid "service" msgstr "service" -#: models.py:278 +#: models.py:466 msgid "services" msgstr "services" -#: models.py:295 +#: models.py:482 msgid "Description of the associated email address." msgstr "Description de l'adresse mail associée." -#: models.py:305 +#: models.py:492 msgid "Can view a contact email address object" msgstr "Peut voir un objet adresse mail de contact" -#: models.py:307 +#: models.py:494 msgid "contact email address" msgstr "adresse mail de contact" -#: models.py:308 +#: models.py:495 msgid "contact email addresses" msgstr "adresses mail de contact" -#: models.py:318 +#: models.py:505 msgid "Networking organisation school Something" msgstr "Association de réseau de l'école Machin" -#: models.py:322 +#: models.py:509 msgid "Threadneedle Street" msgstr "1 rue de la Vrillière" -#: models.py:323 +#: models.py:510 msgid "London EC2R 8AH" msgstr "75001 Paris" -#: models.py:326 +#: models.py:513 msgid "Organisation" msgstr "Association" -#: models.py:340 +#: models.py:527 msgid "Can view the organisation options" msgstr "Peut voir les options d'association" -#: models.py:342 +#: models.py:529 msgid "organisation options" msgstr "options d'association" -#: models.py:371 +#: models.py:558 msgid "Can view the homepage options" msgstr "Peut voir les options de page d'accueil" -#: models.py:373 +#: models.py:560 msgid "homepage options" msgstr "options de page d'accueil" -#: models.py:391 +#: models.py:573 +msgid "Welcome email in French" +msgstr "Mail de bienvenue en français" + +#: models.py:574 +msgid "Welcome email in English" +msgstr "Mail de bienvenue en anglais" + +#: models.py:578 msgid "Can view the email message options" msgstr "Peut voir les options de message pour les mails" -#: models.py:394 +#: models.py:581 msgid "email message options" msgstr "options de messages pour les mails" +#: models.py:586 +msgid "RADIUS policy" +msgstr "Politique de RADIUS" + +#: models.py:587 +msgid "RADIUS policies" +msgstr "Politiques de RADIUS" + +#: models.py:598 +msgid "Reject the machine" +msgstr "Rejeter la machine" + +#: models.py:599 +msgid "Place the machine on the VLAN" +msgstr "Placer la machine sur le VLAN" + +#: models.py:610 +msgid "Policy for unknown machines" +msgstr "Politique pour les machines inconnues" + +#: models.py:618 +msgid "Unknown machines VLAN" +msgstr "VLAN pour les machines inconnues" + +#: models.py:619 +msgid "VLAN for unknown machines if not rejected" +msgstr "VLAN pour les machines inconnues si non rejeté" + +#: models.py:625 +msgid "Policy for unknown ports" +msgstr "Politique pour les ports inconnus" + +#: models.py:633 +msgid "Unknown ports VLAN" +msgstr "VLAN pour les ports inconnus" + +#: models.py:634 +msgid "VLAN for unknown ports if not rejected" +msgstr "VLAN pour les ports inconnus si non rejeté" + +#: models.py:640 +msgid "" +"Policy for machines connecting from unregistered rooms (relevant on ports " +"with STRICT RADIUS mode)" +msgstr "" +"Politique pour les machines se connectant depuis des chambre non " +"enregistrées (pertinent pour les ports avec le mode de RADIUS STRICT)" + +#: models.py:649 +msgid "Unknown rooms VLAN" +msgstr "VLAN pour les chambres inconnues" + +#: models.py:650 +msgid "VLAN for unknown rooms if not rejected" +msgstr "VLAN pour les chambres inconnues si non rejeté" + +#: models.py:656 +msgid "Policy for non members" +msgstr "Politique pour les non adhérents" + +#: models.py:664 +msgid "Non members VLAN" +msgstr "VLAN pour les non adhérents" + +#: models.py:665 +msgid "VLAN for non members if not rejected" +msgstr "VLAN pour les non adhérents si non rejeté" + +#: models.py:671 +msgid "Policy for banned users" +msgstr "Politique pour les utilisateurs bannis" + +#: models.py:679 +msgid "Banned users VLAN" +msgstr "VLAN pour les utilisateurs bannis" + +#: models.py:680 +msgid "VLAN for banned users if not rejected" +msgstr "VLAN pour les utilisateurs bannis si non rejeté" + #: templates/preferences/aff_mailcontact.html:31 -#: templates/preferences/display_preferences.html:174 +#: templates/preferences/display_preferences.html:327 msgid "Address" msgstr "Adresse" #: templates/preferences/aff_mailcontact.html:32 +#: templates/preferences/aff_radiuskey.html:32 msgid "Comment" msgstr "Commentaire" -#: 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/aff_radiuskey.html:31 +msgid "RADIUS key ID" +msgstr "ID de la clé RADIUS" -#: templates/preferences/display_preferences.html:34 -msgid "User preferences" -msgstr "Préférences d'utilisateur" +#: templates/preferences/aff_radiuskey.html:33 +msgid "Default RADIUS key for switches" +msgstr "Clé RADIUS par défaut pour les commutateurs réseau" -#: 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/aff_radiuskey.html:34 +msgid "RADIUS key used by the swithes" +msgstr "Clé RADIUS utilisée par les commutateurs réseau" -#: 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 +#: templates/preferences/aff_radiusoptions.html:28 +#: templates/preferences/display_preferences.html:204 msgid "General policy for VLAN setting" msgstr "Politique générale pour le placement sur un VLAN" -#: templates/preferences/display_preferences.html:112 +#: templates/preferences/aff_radiusoptions.html:30 +#: templates/preferences/display_preferences.html:206 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 " @@ -479,75 +631,356 @@ msgstr "" "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 +#: templates/preferences/aff_radiusoptions.html:33 +#: templates/preferences/display_preferences.html:210 +msgid "VLAN for machines accepted by RADIUS" +msgstr "VLAN pour les machines acceptées par RADIUS" + +#: templates/preferences/aff_radiusoptions.html:34 +#, python-format +msgid "VLAN %(vlan_decision_ok)s" +msgstr "VLAN %(vlan_decision_ok)s" + +#: templates/preferences/aff_radiusoptions.html:41 +msgid "Situation" +msgstr "Situation" + +#: templates/preferences/aff_radiusoptions.html:42 +msgid "Behaviour" +msgstr "Comportement" + +#: templates/preferences/aff_radiusoptions.html:46 +msgid "Unknown machine" +msgstr "Machine inconnue" + +#: templates/preferences/aff_radiusoptions.html:49 +#: templates/preferences/aff_radiusoptions.html:59 +#: templates/preferences/aff_radiusoptions.html:69 +#: templates/preferences/aff_radiusoptions.html:79 +#: templates/preferences/aff_radiusoptions.html:89 +msgid "Reject" +msgstr "Rejeter" + +#: templates/preferences/aff_radiusoptions.html:51 +#, python-format +msgid "VLAN %(unknown_machine_vlan)s" +msgstr "VLAN %(unknown_machine_vlan)s" + +#: templates/preferences/aff_radiusoptions.html:56 +msgid "Unknown port" +msgstr "Port inconnu" + +#: templates/preferences/aff_radiusoptions.html:61 +#, python-format +msgid "VLAN %(unknown_port_vlan)s" +msgstr "VLAN %(unknown_port_vlan)s" + +#: templates/preferences/aff_radiusoptions.html:66 +msgid "Unknown room" +msgstr "Chambre inconnue" + +#: templates/preferences/aff_radiusoptions.html:71 +#, python-format +msgid "VLAN %(unknown_room_vlan)s" +msgstr "VLAN %(unknown_room_vlan)s" + +#: templates/preferences/aff_radiusoptions.html:76 +msgid "Non member" +msgstr "Non adhérent" + +#: templates/preferences/aff_radiusoptions.html:81 +#, python-format +msgid "VLAN %(non_member_vlan)s" +msgstr "VLAN %(non_member_vlan)s" + +#: templates/preferences/aff_radiusoptions.html:86 +msgid "Banned user" +msgstr "Utilisateur banni" + +#: templates/preferences/aff_radiusoptions.html:91 +#, python-format +msgid "VLAN %(banned_vlan)s" +msgstr "VLAN %(banned_vlan)s" + +#: templates/preferences/aff_reminder.html:31 +msgid "Number of days before the reminder" +msgstr "Nombre de jours avant le rappel" + +#: templates/preferences/aff_reminder.html:32 +msgid "Message for this reminder" +msgstr "Message pour ce rappel" + +#: templates/preferences/aff_switchmanagementcred.html:32 +msgid "Default switch management credentials" +msgstr "Identifiants de gestion de commutateur réseau par défaut" + +#: templates/preferences/aff_switchmanagementcred.html:33 +msgid "Management credentials used by the switches" +msgstr "Identifiants de gestion utilisés par les commutateurs réseau" + +#: templates/preferences/delete.html:29 +msgid "Deletion of preferences" +msgstr "Suppression de préférences" + +#: templates/preferences/delete.html:35 +#, python-format +msgid "" +"Warning: are you sure you want to delete this %(objet_name)s object " +"( %(objet)s )?" +msgstr "" +"Attention : voulez-vous vraiment supprimer cet objet %(objet_name)s " +"( %(objet)s ) ?" + +#: templates/preferences/delete.html:36 +msgid "Confirm" +msgstr "Confirmer" + +#: templates/preferences/display_preferences.html:31 +#: templates/preferences/edit_preferences.html:30 +#: templates/preferences/preferences.html:30 +msgid "Preferences" +msgstr "Préférences" + +#: templates/preferences/display_preferences.html:39 msgid "General preferences" msgstr "Préférences générales" -#: templates/preferences/display_preferences.html:139 +#: templates/preferences/display_preferences.html:46 +#: templates/preferences/display_preferences.html:108 +#: templates/preferences/display_preferences.html:165 +#: templates/preferences/display_preferences.html:199 +#: templates/preferences/display_preferences.html:240 +#: templates/preferences/display_preferences.html:301 +#: templates/preferences/display_preferences.html:316 +#: templates/preferences/display_preferences.html:360 +#: templates/preferences/display_preferences.html:438 +#: templates/preferences/edit_preferences.html:46 views.py:178 views.py:226 +#: views.py:272 views.py:321 views.py:381 +msgid "Edit" +msgstr "Modifier" + +#: templates/preferences/display_preferences.html:60 msgid "Number of items per page (standard size)" msgstr "Nombre d'éléments par page (taille standard)" -#: templates/preferences/display_preferences.html:143 +#: templates/preferences/display_preferences.html:64 msgid "Number of items per page (large size)" msgstr "Nombre d'éléments par page (taille importante)" -#: templates/preferences/display_preferences.html:149 +#: templates/preferences/display_preferences.html:70 msgid "General message displayed on the website" msgstr "Message général affiché sur le site" -#: templates/preferences/display_preferences.html:159 +#: templates/preferences/display_preferences.html:72 +msgid "Main site URL" +msgstr "URL du site principal" + +#: templates/preferences/display_preferences.html:84 +msgid "Local email accounts enabled" +msgstr "Comptes mail locaux activés" + +#: templates/preferences/display_preferences.html:86 +msgid "Local email domain" +msgstr "Domaine de mail local" + +#: templates/preferences/display_preferences.html:90 +msgid "Maximum number of email aliases allowed" +msgstr "Nombre maximum d'alias mail autorisé pour un utilisateur standard" + +#: templates/preferences/display_preferences.html:100 +msgid "User preferences" +msgstr "Préférences d'utilisateur" + +#: templates/preferences/display_preferences.html:114 +msgid "Creation of members by everyone" +msgstr "Création d'adhérents par tous" + +#: templates/preferences/display_preferences.html:116 +msgid "Creation of clubs by everyone" +msgstr "Création de clubs par tous" + +#: templates/preferences/display_preferences.html:122 +msgid "Delete not yet active users after" +msgstr "Suppression des utilisateurs n'ayant jamais adhéré après" + +#: templates/preferences/display_preferences.html:123 +#, python-format +msgid "%(delete_notyetactive)s days" +msgstr "%(delete_notyetactive)s jours" + +#: templates/preferences/display_preferences.html:126 +msgid "All users are active by default" +msgstr "Tous les utilisateurs sont actifs par défault" + +#: templates/preferences/display_preferences.html:131 +msgid "Users general permissions" +msgstr "Permissions générales des utilisateurs" + +#: templates/preferences/display_preferences.html:134 +msgid "Default shell for users" +msgstr "Interface en ligne de commande par défaut pour les utilisateurs" + +#: templates/preferences/display_preferences.html:136 +msgid "Users can edit their shell" +msgstr "Les utilisateurs peuvent modifier leur interface en ligne de commande" + +#: templates/preferences/display_preferences.html:140 +msgid "Users can edit their room" +msgstr "Les utilisateurs peuvent modifier leur chambre" + +#: templates/preferences/display_preferences.html:146 +msgid "GPG fingerprint field" +msgstr "Champ empreinte GPG" + +#: templates/preferences/display_preferences.html:157 +msgid "Machines preferences" +msgstr "Préférences de machines" + +#: templates/preferences/display_preferences.html:170 +msgid "Password per machine" +msgstr "Mot de passe par machine" + +#: templates/preferences/display_preferences.html:178 +msgid "IPv6 support" +msgstr "Support de l'IPv6" + +#: templates/preferences/display_preferences.html:182 +msgid "Creation of machines" +msgstr "Création de machines" + +#: templates/preferences/display_preferences.html:192 +msgid "Topology preferences" +msgstr "Préférences de topologie" + +#: templates/preferences/display_preferences.html:212 +msgid "VLAN for machines rejected by RADIUS" +msgstr "VLAN pour les machines rejetées par RADIUS" + +#: templates/preferences/display_preferences.html:216 +msgid "VLAN for non members machines" +msgstr "VLAN pour les machines des non adhérents" + +#: templates/preferences/display_preferences.html:223 +msgid " Add a RADIUS key" +msgstr " Ajouter une clé RADIUS" + +#: templates/preferences/display_preferences.html:233 +msgid "Configuration of switches" +msgstr "Configuration de commutateurs réseau" + +#: templates/preferences/display_preferences.html:255 +msgid "Provision of configuration for switches" +msgstr "Provision de configuration pour les commutateurs réseau" + +#: templates/preferences/display_preferences.html:258 +msgid "Switches with automatic provision" +msgstr "Commutateurs réseau avec provision automatique" + +#: templates/preferences/display_preferences.html:259 +#: templates/preferences/display_preferences.html:263 +#: templates/preferences/display_preferences.html:267 +#: templates/preferences/display_preferences.html:275 +#: templates/preferences/display_preferences.html:279 +#: templates/preferences/display_preferences.html:289 +msgid "OK" +msgstr "OK" + +#: templates/preferences/display_preferences.html:259 +#: templates/preferences/display_preferences.html:263 +#: templates/preferences/display_preferences.html:267 +#: templates/preferences/display_preferences.html:289 +msgid "Missing" +msgstr "Manquant" + +#: templates/preferences/display_preferences.html:266 +msgid "Server for the configuration of switches" +msgstr "Serveur pour la configuration des commutateurs réseau" + +#: templates/preferences/display_preferences.html:274 +msgid "TFTP mode" +msgstr "Mode TFTP" + +#: templates/preferences/display_preferences.html:278 +msgid "SFTP mode" +msgstr "Mode SFTP" + +#: templates/preferences/display_preferences.html:279 +msgid "Missing credentials" +msgstr "Identifiants manquants" + +#: templates/preferences/display_preferences.html:283 +msgid "Switch management credentials" +msgstr "Identifiants de gestion de commutateur réseau" + +#: templates/preferences/display_preferences.html:285 +msgid " Add switch management credentials" +msgstr " Ajouter des identifiants de gestion de commutateur réseau" + +#: templates/preferences/display_preferences.html:296 +msgid "RADIUS preferences" +msgstr "Préférences RADIUS" + +#: templates/preferences/display_preferences.html:310 msgid "Information about the organisation" msgstr "Informations sur l'association" -#: templates/preferences/display_preferences.html:188 +#: templates/preferences/display_preferences.html:341 msgid "User object of the organisation" msgstr "Objet utilisateur de l'association" -#: templates/preferences/display_preferences.html:190 +#: templates/preferences/display_preferences.html:343 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:353 +msgid "Message for emails" +msgstr "Message pour les mails" -#: templates/preferences/display_preferences.html:203 +#: templates/preferences/display_preferences.html:366 msgid "Welcome email (in French)" msgstr "Mail de bienvenue (en français)" -#: templates/preferences/display_preferences.html:207 +#: templates/preferences/display_preferences.html:370 msgid "Welcome email (in English)" msgstr "Mail de bienvenue (en anglais)" -#: templates/preferences/display_preferences.html:211 +#: templates/preferences/display_preferences.html:380 +msgid "Options for the membership's end email" +msgstr "Options pour le mail de fin d'adhésion" + +#: templates/preferences/display_preferences.html:386 +msgid " Add a reminder" +msgstr " Ajouter un rappel" + +#: templates/preferences/display_preferences.html:397 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 +#: templates/preferences/display_preferences.html:403 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 +#: templates/preferences/display_preferences.html:414 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:420 +msgid " Add an address" +msgstr " Ajouter une adresse" -#: templates/preferences/display_preferences.html:225 -msgid "Delete one or several addresses" +#: templates/preferences/display_preferences.html:422 +msgid " Delete one or several addresses" msgstr " Supprimer une ou plusieurs adresses" -#: templates/preferences/display_preferences.html:231 +#: templates/preferences/display_preferences.html:431 +msgid "Social networks" +msgstr "Réseaux sociaux" + +#: templates/preferences/display_preferences.html:443 msgid "Twitter account URL" msgstr "URL du compte Twitter" -#: templates/preferences/display_preferences.html:237 +#: templates/preferences/display_preferences.html:449 msgid "Facebook account URL" msgstr "URL du compte Facebook" @@ -555,55 +988,112 @@ msgstr "URL du compte Facebook" msgid "Editing of preferences" msgstr "Modification des préférences" -#: views.py:98 -msgid "Unknown object" -msgstr "Objet inconnu" +#: views.py:114 +msgid "Unknown object." +msgstr "Objet inconnu." -#: views.py:104 +#: views.py:120 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 +#: views.py:137 msgid "The preferences were edited." msgstr "Les préférences ont été modifiées." -#: views.py:140 +#: views.py:155 msgid "The service was added." msgstr "Le service a été ajouté." -#: views.py:143 +#: views.py:158 msgid "Add a service" -msgstr " Ajouter un service" +msgstr "Ajouter un service" -#: views.py:167 +#: views.py:175 msgid "The service was edited." msgstr "Le service a été modifié." -#: views.py:188 +#: views.py:189 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:204 +msgid "The reminder was added." +msgstr "Le rappel a été ajouté." -#: views.py:194 views.py:256 -msgid "Delete" -msgstr "Supprimer" +#: views.py:207 +msgid "Add a reminder" +msgstr "Ajouter un rappel" -#: views.py:210 +#: views.py:223 +msgid "The reminder was edited." +msgstr "Le rappel a été modifié." + +#: views.py:239 +msgid "The reminder was deleted." +msgstr "Le rappel a été supprimé." + +#: views.py:255 +msgid "The RADIUS key was added." +msgstr "La clé RADIUS a été ajoutée." + +#: views.py:258 +msgid "Add a RADIUS key" +msgstr "Ajouter une clé RADIUS" + +#: views.py:269 +msgid "The RADIUS key was edited." +msgstr "La clé RADIUS a été modifiée." + +#: views.py:285 +msgid "The RADIUS key was deleted." +msgstr "La clé RADIUS a été supprimée." + +#: views.py:287 +msgid "The RADIUS key is assigned to at least one switch, you can't delete it." +msgstr "" +"La clé RADIUS est assignée a au moins un commutateur réseau, vous ne pouvez " +"pas la supprimer." + +#: views.py:304 +msgid "The switch management credentials were added." +msgstr "Les identifiants de gestion de commutateur réseay ont été ajoutés." + +#: views.py:307 +msgid "Add switch management credentials" +msgstr "Ajouter des identifiants de gestion de commutateur réseau" + +#: views.py:318 +msgid "The switch management credentials were edited." +msgstr "Les identifiants de gestion de commutateur réseau ont été modifiés." + +#: views.py:334 +msgid "The switch management credentials were deleted." +msgstr "Les identifiants de gestion de commutateur réseau ont été supprimés." + +#: views.py:336 +msgid "" +"The switch management credentials are assigned to at least one switch, you " +"can't delete them." +msgstr "" +"Les identifiants de gestion de commutateur réseau sont assignés à au moins " +"un commutateur réseau , vous ne pouvez pas les supprimer." + +#: views.py:357 msgid "The contact email address was created." msgstr "L'adresse mail de contact a été supprimée." -#: views.py:214 +#: views.py:361 msgid "Add a contact email address" msgstr "Ajouter une adresse mail de contact" -#: views.py:231 +#: views.py:378 msgid "The contact email address was edited." msgstr "L'adresse mail de contact a été modifiée." -#: views.py:253 +#: views.py:400 msgid "The contact email adress was deleted." msgstr "L'adresse mail de contact a été supprimée." + +#: views.py:403 +msgid "Delete" +msgstr "Supprimer" diff --git a/preferences/migrations/0051_auto_20180919_2225.py b/preferences/migrations/0051_auto_20180919_2225.py new file mode 100644 index 00000000..f776a9a6 --- /dev/null +++ b/preferences/migrations/0051_auto_20180919_2225.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-09-19 20:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.aes_field +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ('preferences', '0050_auto_20180818_1329'), + ] + + operations = [ + migrations.CreateModel( + name='RadiusKey', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('radius_key', re2o.aes_field.AESEncryptedField(help_text='Clef radius', max_length=255)), + ('comment', models.CharField(blank=True, help_text='Commentaire de cette clef', max_length=255, null=True)), + ('default_switch', models.BooleanField(default=True, help_text='Clef par défaut des switchs', unique=True)), + ], + options={ + 'permissions': (('view_radiuskey', 'Peut voir un objet radiuskey'),), + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.CreateModel( + name='Reminder', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('days', models.IntegerField(default=7, help_text="Délais entre le mail et la fin d'adhésion", unique=True)), + ('message', models.CharField(blank=True, default='', help_text='Message affiché spécifiquement pour ce rappel', max_length=255, null=True)), + ], + options={ + 'permissions': (('view_reminder', 'Peut voir un objet reminder'),), + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.CreateModel( + name='SwitchManagementCred', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('management_id', models.CharField(help_text='Login du switch', max_length=63)), + ('management_pass', re2o.aes_field.AESEncryptedField(help_text='Mot de passe', max_length=63)), + ('default_switch', models.BooleanField(default=True, help_text='Creds par défaut des switchs', unique=True)), + ], + options={ + 'permissions': (('view_switchmanagementcred', 'Peut voir un objet switchmanagementcred'),), + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.AddField( + model_name='optionaltopologie', + name='sftp_login', + field=models.CharField(blank=True, help_text='Login sftp des switchs', max_length=32, null=True), + ), + migrations.AddField( + model_name='optionaltopologie', + name='sftp_pass', + field=re2o.aes_field.AESEncryptedField(blank=True, help_text='Mot de passe sftp', max_length=63, null=True), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_ip_type', + field=models.OneToOneField(blank=True, help_text="Plage d'ip de management des switchs", null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.IpType'), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_provision', + field=models.CharField(choices=[('sftp', 'sftp'), ('tftp', 'tftp')], default='tftp', help_text='Mode de récupération des confs par les switchs', max_length=32), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_rest_management', + field=models.BooleanField(default=False, help_text='Rest management, activé si provision auto'), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_web_management', + field=models.BooleanField(default=False, help_text='Web management, activé si provision automatique'), + ), + migrations.AddField( + model_name='optionaltopologie', + name='switchs_web_management_ssl', + field=models.BooleanField(default=False, help_text='Web management ssl. Assurez-vous que un certif est installé sur le switch !'), + ), + migrations.AlterField( + model_name='mailmessageoption', + name='welcome_mail_en', + field=models.TextField(default='', help_text='Mail de bienvenue en anglais'), + ), + migrations.AlterField( + model_name='mailmessageoption', + name='welcome_mail_fr', + field=models.TextField(default='', help_text='Mail de bienvenue en français'), + ), + ] diff --git a/preferences/migrations/0052_optionaluser_delete_notyetactive.py b/preferences/migrations/0052_optionaluser_delete_notyetactive.py new file mode 100644 index 00000000..589c6dc7 --- /dev/null +++ b/preferences/migrations/0052_optionaluser_delete_notyetactive.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-11 12:51 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0051_auto_20180919_2225'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='delete_notyetactive', + field=models.IntegerField(default=15, help_text='Inactive users will be deleted after this number of days'), + ), + ] diff --git a/preferences/migrations/0053_optionaluser_self_change_room.py b/preferences/migrations/0053_optionaluser_self_change_room.py new file mode 100644 index 00000000..07840c5d --- /dev/null +++ b/preferences/migrations/0053_optionaluser_self_change_room.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-14 22:14 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0052_optionaluser_delete_notyetactive'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='self_change_room', + field=models.BooleanField(default=False, help_text='Users can edit their room'), + ), + ] diff --git a/preferences/migrations/0055_generaloption_main_site_url.py b/preferences/migrations/0055_generaloption_main_site_url.py new file mode 100644 index 00000000..655c0b07 --- /dev/null +++ b/preferences/migrations/0055_generaloption_main_site_url.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-11-14 16:46 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0053_optionaluser_self_change_room'), + ] + + operations = [ + migrations.AddField( + model_name='generaloption', + name='main_site_url', + field=models.URLField(default='http://re2o.example.org', max_length=255), + ), + ] diff --git a/preferences/migrations/0056_1_radiusoption.py b/preferences/migrations/0056_1_radiusoption.py new file mode 100644 index 00000000..8a7cb45c --- /dev/null +++ b/preferences/migrations/0056_1_radiusoption.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-13 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ('preferences', '0055_generaloption_main_site_url'), + ] + + operations = [ + migrations.CreateModel( + name='RadiusOption', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('radius_general_policy', 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)), + ], + options={ + 'verbose_name': 'radius policies', + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.AddField( + model_name='radiusoption', + name='banned_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for banned if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='banned_vlan', to='machines.Vlan', verbose_name='Banned Vlan'), + ), + migrations.AddField( + model_name='radiusoption', + name='non_member_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for non members if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='non_member_vlan', to='machines.Vlan', verbose_name='Non member Vlan'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_machine_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for unknown machines if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_vlan', to='machines.Vlan', verbose_name='Unknown machine Vlan'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_port_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for unknown ports if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_vlan', to='machines.Vlan', verbose_name='Unknown port Vlan'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_room_vlan', + field=models.ForeignKey(blank=True, help_text='Vlan for unknown room if not rejected.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_vlan', to='machines.Vlan', verbose_name='Unknown room Vlan'), + ), + migrations.AddField( + model_name='radiusoption', + name='banned', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for banned users.'), + ), + migrations.AddField( + model_name='radiusoption', + name='non_member', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy non member users.'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_machine', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_port', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown machines'), + ), + migrations.AddField( + model_name='radiusoption', + name='unknown_room', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for machine connecting from unregistered room (relevant on ports with STRICT radius mode)'), + ), + migrations.AddField( + model_name='radiusoption', + name='vlan_decision_ok', + field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='vlan_ok_option', to='machines.Vlan'), + ), + ] diff --git a/preferences/migrations/0056_2_radiusoption.py b/preferences/migrations/0056_2_radiusoption.py new file mode 100644 index 00000000..1a8ecccd --- /dev/null +++ b/preferences/migrations/0056_2_radiusoption.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-13 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +def create_radius_policy(apps, schema_editor): + OptionalTopologie = apps.get_model('preferences', 'OptionalTopologie') + RadiusOption = apps.get_model('preferences', 'RadiusOption') + + option,_ = OptionalTopologie.objects.get_or_create() + + radius_option = RadiusOption() + radius_option.radius_general_policy = option.radius_general_policy + radius_option.vlan_decision_ok = option.vlan_decision_ok + + radius_option.save() + +def revert_radius(apps, schema_editor): + pass + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ('preferences', '0055_generaloption_main_site_url'), + ('preferences', '0056_1_radiusoption'), + ] + + operations = [ + migrations.RunPython(create_radius_policy, revert_radius), + ] diff --git a/preferences/migrations/0056_3_radiusoption.py b/preferences/migrations/0056_3_radiusoption.py new file mode 100644 index 00000000..f3e5f98c --- /dev/null +++ b/preferences/migrations/0056_3_radiusoption.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-10-13 14:29 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.mixins + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0095_auto_20180919_2225'), + ('preferences', '0055_generaloption_main_site_url'), + ('preferences', '0056_2_radiusoption'), + ] + + operations = [ + migrations.RemoveField( + model_name='optionaltopologie', + name='radius_general_policy', + ), + migrations.RemoveField( + model_name='optionaltopologie', + name='vlan_decision_nok', + ), + migrations.RemoveField( + model_name='optionaltopologie', + name='vlan_decision_ok', + ), + ] diff --git a/preferences/migrations/0056_4_radiusoption.py b/preferences/migrations/0056_4_radiusoption.py new file mode 100644 index 00000000..8d93fff9 --- /dev/null +++ b/preferences/migrations/0056_4_radiusoption.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2018-12-04 13:57 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0056_3_radiusoption'), + ] + + operations = [ + migrations.AlterField( + model_name='radiusoption', + name='unknown_port', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown port'), + ), + ] diff --git a/preferences/migrations/0057_optionaluser_all_users_active.py b/preferences/migrations/0057_optionaluser_all_users_active.py new file mode 100644 index 00000000..3f0cc8c1 --- /dev/null +++ b/preferences/migrations/0057_optionaluser_all_users_active.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-05 17:15 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0056_4_radiusoption'), + ] + + operations = [ + migrations.AddField( + model_name='optionaluser', + name='all_users_active', + field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid'), + ), + ] diff --git a/preferences/migrations/0058_auto_20190108_1650.py b/preferences/migrations/0058_auto_20190108_1650.py new file mode 100644 index 00000000..e90b6067 --- /dev/null +++ b/preferences/migrations/0058_auto_20190108_1650.py @@ -0,0 +1,208 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-08 22:50 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import re2o.aes_field + + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0057_optionaluser_all_users_active'), + ] + + operations = [ + migrations.AlterModelOptions( + name='radiuskey', + options={'permissions': (('view_radiuskey', 'Can view a RADIUS key object'),), 'verbose_name': 'RADIUS key', 'verbose_name_plural': 'RADIUS keys'}, + ), + migrations.AlterModelOptions( + name='radiusoption', + options={'verbose_name': 'RADIUS policy', 'verbose_name_plural': 'RADIUS policies'}, + ), + migrations.AlterModelOptions( + name='reminder', + options={'permissions': (('view_reminder', 'Can view a reminder object'),), 'verbose_name': 'reminder', 'verbose_name_plural': 'reminders'}, + ), + migrations.AlterModelOptions( + name='switchmanagementcred', + options={'permissions': (('view_switchmanagementcred', 'Can view a switch management credentials object'),), 'verbose_name': 'switch management credentials'}, + ), + migrations.AlterField( + model_name='mailmessageoption', + name='welcome_mail_en', + field=models.TextField(default='', help_text='Welcome email in English'), + ), + migrations.AlterField( + model_name='mailmessageoption', + name='welcome_mail_fr', + field=models.TextField(default='', help_text='Welcome email in French'), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='sftp_login', + field=models.CharField(blank=True, help_text='SFTP login for switches', max_length=32, null=True), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='sftp_pass', + field=re2o.aes_field.AESEncryptedField(blank=True, help_text='SFTP password', max_length=63, null=True), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='switchs_ip_type', + field=models.OneToOneField(blank=True, help_text='IP range for the management of switches', null=True, on_delete=django.db.models.deletion.PROTECT, to='machines.IpType'), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='switchs_provision', + field=models.CharField(choices=[('sftp', 'sftp'), ('tftp', 'tftp')], default='tftp', help_text='Provision of configuration mode for switches', max_length=32), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='switchs_rest_management', + field=models.BooleanField(default=False, help_text='REST management, activated in case of automatic provision'), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='switchs_web_management', + field=models.BooleanField(default=False, help_text='Web management, activated in case of automatic provision'), + ), + migrations.AlterField( + model_name='optionaltopologie', + name='switchs_web_management_ssl', + field=models.BooleanField(default=False, help_text='SSL web management, make sure that a certificate is installed on the switch'), + ), + 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='all_users_active', + field=models.BooleanField(default=False, help_text='If True, all new created and connected users are active. If False, only when a valid registration has been paid.'), + ), + migrations.AlterField( + model_name='optionaluser', + name='delete_notyetactive', + field=models.IntegerField(default=15, help_text='Not yet active users will be deleted after this number of days.'), + ), + migrations.AlterField( + model_name='optionaluser', + name='local_email_accounts_enabled', + field=models.BooleanField(default=False, help_text='Enable local email accounts for users.'), + ), + 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_room', + field=models.BooleanField(default=False, help_text='Users can edit their room.'), + ), + migrations.AlterField( + model_name='optionaluser', + name='self_change_shell', + field=models.BooleanField(default=False, help_text='Users can edit their shell.'), + ), + migrations.AlterField( + model_name='radiuskey', + name='comment', + field=models.CharField(blank=True, help_text='Comment for this key', max_length=255, null=True), + ), + migrations.AlterField( + model_name='radiuskey', + name='default_switch', + field=models.BooleanField(default=True, help_text='Default key for switches', unique=True), + ), + migrations.AlterField( + model_name='radiuskey', + name='radius_key', + field=re2o.aes_field.AESEncryptedField(help_text='RADIUS key', max_length=255), + ), + migrations.AlterField( + model_name='radiusoption', + name='banned', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for banned users'), + ), + migrations.AlterField( + model_name='radiusoption', + name='banned_vlan', + field=models.ForeignKey(blank=True, help_text='VLAN for banned users if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='banned_vlan', to='machines.Vlan', verbose_name='Banned users VLAN'), + ), + migrations.AlterField( + model_name='radiusoption', + name='non_member', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for non members'), + ), + migrations.AlterField( + model_name='radiusoption', + name='non_member_vlan', + field=models.ForeignKey(blank=True, help_text='VLAN for non members if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='non_member_vlan', to='machines.Vlan', verbose_name='Non members VLAN'), + ), + migrations.AlterField( + model_name='radiusoption', + name='unknown_machine_vlan', + field=models.ForeignKey(blank=True, help_text='VLAN for unknown machines if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_machine_vlan', to='machines.Vlan', verbose_name='Unknown machines VLAN'), + ), + migrations.AlterField( + model_name='radiusoption', + name='unknown_port', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for unknown ports'), + ), + migrations.AlterField( + model_name='radiusoption', + name='unknown_port_vlan', + field=models.ForeignKey(blank=True, help_text='VLAN for unknown ports if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_port_vlan', to='machines.Vlan', verbose_name='Unknown ports VLAN'), + ), + migrations.AlterField( + model_name='radiusoption', + name='unknown_room', + field=models.CharField(choices=[('REJECT', 'Reject the machine'), ('SET_VLAN', 'Place the machine on the VLAN')], default='REJECT', max_length=32, verbose_name='Policy for machines connecting from unregistered rooms (relevant on ports with STRICT RADIUS mode)'), + ), + migrations.AlterField( + model_name='radiusoption', + name='unknown_room_vlan', + field=models.ForeignKey(blank=True, help_text='VLAN for unknown rooms if not rejected', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='unknown_room_vlan', to='machines.Vlan', verbose_name='Unknown rooms VLAN'), + ), + migrations.AlterField( + model_name='reminder', + name='days', + field=models.IntegerField(default=7, help_text="Delay between the email and the membership's end", unique=True), + ), + migrations.AlterField( + model_name='reminder', + name='message', + field=models.CharField(blank=True, default='', help_text='Message displayed specifically for this reminder', max_length=255, null=True), + ), + migrations.AlterField( + model_name='switchmanagementcred', + name='default_switch', + field=models.BooleanField(default=True, help_text='Default credentials for switches', unique=True), + ), + migrations.AlterField( + model_name='switchmanagementcred', + name='management_id', + field=models.CharField(help_text='Switch login', max_length=63), + ), + migrations.AlterField( + model_name='switchmanagementcred', + name='management_pass', + field=re2o.aes_field.AESEncryptedField(help_text='Password', max_length=63), + ), + ] diff --git a/preferences/migrations/0059_auto_20190120_1739.py b/preferences/migrations/0059_auto_20190120_1739.py new file mode 100644 index 00000000..23447ce8 --- /dev/null +++ b/preferences/migrations/0059_auto_20190120_1739.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2019-01-20 23:39 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion +import preferences.models +import re2o.mixins + + +def create_defaults(apps, schema_editor): + CotisationsOption = apps.get_model('preferences', 'CotisationsOption') + CotisationsOption.objects.get_or_create() + +class Migration(migrations.Migration): + + dependencies = [ + ('preferences', '0058_auto_20190108_1650'), + ] + + operations = [ + migrations.CreateModel( + name='CotisationsOption', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('send_voucher_mail', models.BooleanField(default=False, verbose_name='Send voucher by email when the invoice is controlled.')), + ], + options={ + 'verbose_name': 'cotisations options', + }, + bases=(re2o.mixins.AclMixin, models.Model), + ), + migrations.CreateModel( + name='DocumentTemplate', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('template', models.FileField(upload_to='templates/', verbose_name='template')), + ('name', models.CharField(max_length=125, unique=True, verbose_name='name')), + ], + options={ + 'verbose_name': 'document template', + 'verbose_name_plural': 'document templates', + }, + bases=(re2o.mixins.RevMixin, re2o.mixins.AclMixin, models.Model), + ), + migrations.AddField( + model_name='assooption', + name='pres_name', + field=models.CharField(default='', help_text='Displayed on subscription vouchers', max_length=255, verbose_name='President of the association'), + ), + migrations.AddField( + model_name='cotisationsoption', + name='invoice_template', + field=models.OneToOneField(default=preferences.models.default_invoice, on_delete=django.db.models.deletion.PROTECT, related_name='invoice_template', to='preferences.DocumentTemplate', verbose_name='Template for invoices'), + ), + migrations.AddField( + model_name='cotisationsoption', + name='voucher_template', + field=models.OneToOneField(default=preferences.models.default_voucher, on_delete=django.db.models.deletion.PROTECT, related_name='voucher_template', to='preferences.DocumentTemplate', verbose_name='Template for subscription voucher'), + ), + migrations.RunPython(create_defaults), + ] diff --git a/preferences/models.py b/preferences/models.py index 3199dd6c..445d7139 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -24,8 +24,10 @@ Reglages généraux, machines, utilisateurs, mail, general pour l'application. """ from __future__ import unicode_literals +import os from django.utils.functional import cached_property +from django.utils import timezone from django.db import models from django.db.models.signals import post_save from django.dispatch import receiver @@ -34,7 +36,11 @@ from django.forms import ValidationError from django.utils.translation import ugettext_lazy as _ import machines.models -from re2o.mixins import AclMixin + +from re2o.mixins import AclMixin, RevMixin +from re2o.aes_field import AESEncryptedField + +from datetime import timedelta class PreferencesModel(models.Model): @@ -68,16 +74,13 @@ class OptionalUser(AclMixin, PreferencesModel): gpg_fingerprint = models.BooleanField(default=True) all_can_create_club = models.BooleanField( default=False, - help_text=_("Users can create a club") + help_text=_("Users can create a club.") ) all_can_create_adherent = models.BooleanField( default=False, - help_text=_("Users can create a member"), - ) - self_adhesion = models.BooleanField( - default=False, - help_text=_("A new user can create their account on Re2o") + help_text=_("Users can create a member."), ) + shell_default = models.OneToOneField( 'users.ListShell', on_delete=models.PROTECT, @@ -86,11 +89,15 @@ class OptionalUser(AclMixin, PreferencesModel): ) self_change_shell = models.BooleanField( default=False, - help_text=_("Users can edit their shell") + help_text=_("Users can edit their shell.") + ) + self_change_room = models.BooleanField( + default=False, + help_text=_("Users can edit their room.") ) 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, @@ -100,7 +107,21 @@ class OptionalUser(AclMixin, PreferencesModel): max_email_address = models.IntegerField( default=15, help_text=_("Maximum number of local email addresses for a standard" - " user") + " user.") + ) + delete_notyetactive = models.IntegerField( + default=15, + help_text=_("Not yet active users will be deleted after this number of" + " days.") + ) + self_adhesion = models.BooleanField( + default=False, + help_text=_("A new user can create their account on Re2o.") + ) + all_users_active = models.BooleanField( + default=False, + help_text=_("If True, all new created and connected users are active." + " If False, only when a valid registration has been paid.") ) class Meta: @@ -180,27 +201,104 @@ class OptionalTopologie(AclMixin, PreferencesModel): (MACHINE, _("On the IP range's VLAN of the machine")), (DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")), ) + CHOICE_PROVISION = ( + ('sftp', 'sftp'), + ('tftp', 'tftp'), + ) - radius_general_policy = models.CharField( + switchs_web_management = models.BooleanField( + default=False, + help_text=_("Web management, activated in case of automatic provision") + ) + switchs_web_management_ssl = models.BooleanField( + default=False, + help_text=_("SSL web management, make sure that a certificate is" + " installed on the switch") + ) + switchs_rest_management = models.BooleanField( + default=False, + help_text=_("REST management, activated in case of automatic provision") + ) + switchs_ip_type = models.OneToOneField( + 'machines.IpType', + on_delete=models.PROTECT, + blank=True, + null=True, + help_text=_("IP range for the management of switches") + ) + switchs_provision = models.CharField( max_length=32, - choices=CHOICE_RADIUS, - default='DEFINED' + choices=CHOICE_PROVISION, + default='tftp', + help_text=_("Provision of configuration mode for switches") ) - vlan_decision_ok = models.OneToOneField( - 'machines.Vlan', - on_delete=models.PROTECT, - related_name='decision_ok', + sftp_login = models.CharField( + max_length=32, + null=True, blank=True, - null=True + help_text=_("SFTP login for switches") ) - vlan_decision_nok = models.OneToOneField( - 'machines.Vlan', - on_delete=models.PROTECT, - related_name='decision_nok', + sftp_pass = AESEncryptedField( + max_length=63, + null=True, blank=True, - null=True + help_text=_("SFTP password") ) + @cached_property + def provisioned_switchs(self): + """Liste des switches provisionnés""" + from topologie.models import Switch + return Switch.objects.filter(automatic_provision=True).order_by('interface__domain__name') + + @cached_property + def switchs_management_interface(self): + """Return the ip of the interface that the switch have to contact to get it's config""" + if self.switchs_ip_type: + from machines.models import Role, Interface + return Interface.objects.filter(machine__interface__in=Role.interface_for_roletype("switch-conf-server")).filter(type__ip_type=self.switchs_ip_type).first() + else: + return None + + @cached_property + def switchs_management_interface_ip(self): + """Same, but return the ipv4""" + if not self.switchs_management_interface: + return None + return self.switchs_management_interface.ipv4 + + @cached_property + def switchs_management_sftp_creds(self): + """Credentials des switchs pour provion sftp""" + if self.sftp_login and self.sftp_pass: + return {'login' : self.sftp_login, 'pass' : self.sftp_pass} + else: + return None + + @cached_property + def switchs_management_utils(self): + """Used for switch_conf, return a list of ip on vlans""" + from machines.models import Role, Ipv6List, Interface + def return_ips_dict(interfaces): + return {'ipv4' : [str(interface.ipv4) for interface in interfaces], 'ipv6' : Ipv6List.objects.filter(interface__in=interfaces).values_list('ipv6', flat=True)} + + ntp_servers = Role.all_interfaces_for_roletype("ntp-server").filter(type__ip_type=self.switchs_ip_type) + log_servers = Role.all_interfaces_for_roletype("log-server").filter(type__ip_type=self.switchs_ip_type) + radius_servers = Role.all_interfaces_for_roletype("radius-server").filter(type__ip_type=self.switchs_ip_type) + dhcp_servers = Role.all_interfaces_for_roletype("dhcp-server") + dns_recursive_servers = Role.all_interfaces_for_roletype("dns-recursive-server").filter(type__ip_type=self.switchs_ip_type) + subnet = None + subnet6 = None + if self.switchs_ip_type: + subnet = self.switchs_ip_type.ip_set_full_info + subnet6 = self.switchs_ip_type.ip6_set_full_info + return {'ntp_servers': return_ips_dict(ntp_servers), 'log_servers': return_ips_dict(log_servers), 'radius_servers': return_ips_dict(radius_servers), 'dhcp_servers': return_ips_dict(dhcp_servers), 'dns_recursive_servers': return_ips_dict(dns_recursive_servers), 'subnet': subnet, 'subnet6': subnet6} + + @cached_property + def provision_switchs_enabled(self): + """Return true if all settings are ok : switchs on automatic provision, + ip_type""" + return bool(self.provisioned_switchs and self.switchs_ip_type and SwitchManagementCred.objects.filter(default_switch=True).exists() and self.switchs_management_interface_ip and bool(self.switchs_provision != 'sftp' or self.switchs_management_sftp_creds)) class Meta: permissions = ( ("view_optionaltopologie", _("Can view the topology options")), @@ -215,6 +313,96 @@ def optionaltopologie_post_save(**kwargs): topologie_pref.set_in_cache() +class RadiusKey(AclMixin, models.Model): + """Class of a radius key""" + radius_key = AESEncryptedField( + max_length=255, + help_text=_("RADIUS key") + ) + comment = models.CharField( + max_length=255, + null=True, + blank=True, + help_text=_("Comment for this key") + ) + default_switch = models.BooleanField( + default=True, + unique=True, + help_text=_("Default key for switches") + ) + + class Meta: + permissions = ( + ("view_radiuskey", _("Can view a RADIUS key object")), + ) + verbose_name = _("RADIUS key") + verbose_name_plural = _("RADIUS keys") + + def __str__(self): + return _("RADIUS key ") + str(self.id) + " " + str(self.comment) + + +class SwitchManagementCred(AclMixin, models.Model): + """Class of a management creds of a switch, for rest management""" + management_id = models.CharField( + max_length=63, + help_text=_("Switch login") + ) + management_pass = AESEncryptedField( + max_length=63, + help_text=_("Password") + ) + default_switch = models.BooleanField( + default=True, + unique=True, + help_text=_("Default credentials for switches") + ) + + class Meta: + permissions = ( + ("view_switchmanagementcred", _("Can view a switch management" + " credentials object")), + ) + verbose_name = _("switch management credentials") + + def __str__(self): + return _("Switch login ") + str(self.management_id) + + +class Reminder(AclMixin, models.Model): + """Options pour les mails de notification de fin d'adhésion. + Days: liste des nombres de jours pour lesquells un mail est envoyé + optionalMessage: message additionel pour le mail + """ + + days = models.IntegerField( + default=7, + unique=True, + help_text=_("Delay between the email and the membership's end") + ) + message = models.CharField( + max_length=255, + default="", + null=True, + blank=True, + help_text=_("Message displayed specifically for this reminder") + ) + + class Meta: + permissions = ( + ("view_reminder", _("Can view a reminder object")), + ) + verbose_name = _("reminder") + verbose_name_plural = _("reminders") + + def users_to_remind(self): + from re2o.utils import all_has_access + date = timezone.now().replace(minute=0,hour=0) + futur_date = date + timedelta(days=self.days) + users = all_has_access(futur_date).exclude(pk__in = all_has_access(futur_date + timedelta(days=1))) + return users + + class GeneralOption(AclMixin, PreferencesModel): """Options générales : nombre de resultats par page, nom du site, temps où les liens sont valides""" @@ -237,6 +425,7 @@ class GeneralOption(AclMixin, PreferencesModel): req_expire_hrs = models.IntegerField(default=48) site_name = models.CharField(max_length=32, default="Re2o") email_from = models.EmailField(default="www-data@example.com") + main_site_url = models.URLField(max_length=255, default="http://re2o.example.org") GTU_sum_up = models.TextField( default="", blank=True, @@ -291,8 +480,7 @@ class MailContact(AclMixin, models.Model): commentary = models.CharField( blank = True, null = True, - help_text = _( - "Description of the associated email address."), + help_text = _("Description of the associated email address."), max_length = 256 ) @@ -334,6 +522,12 @@ class AssoOption(AclMixin, PreferencesModel): null=True, blank=True, ) + pres_name = models.CharField( + max_length=255, + default="", + verbose_name=_("President of the association"), + help_text=_("Displayed on subscription vouchers") + ) class Meta: permissions = ( @@ -383,8 +577,8 @@ def homeoption_post_save(**kwargs): class MailMessageOption(AclMixin, models.Model): """Reglages, mail de bienvenue et autre""" - welcome_mail_fr = models.TextField(default="") - welcome_mail_en = models.TextField(default="") + welcome_mail_fr = models.TextField(default="", help_text=_("Welcome email in French")) + welcome_mail_en = models.TextField(default="", help_text=_("Welcome email in English")) class Meta: permissions = ( @@ -393,3 +587,202 @@ class MailMessageOption(AclMixin, models.Model): ) verbose_name = _("email message options") + +class RadiusOption(AclMixin, PreferencesModel): + class Meta: + verbose_name = _("RADIUS policy") + verbose_name_plural = _("RADIUS policies") + + MACHINE = 'MACHINE' + DEFINED = 'DEFINED' + CHOICE_RADIUS = ( + (MACHINE, _("On the IP range's VLAN of the machine")), + (DEFINED, _("Preset in 'VLAN for machines accepted by RADIUS'")), + ) + REJECT = 'REJECT' + SET_VLAN = 'SET_VLAN' + CHOICE_POLICY = ( + (REJECT, _("Reject the machine")), + (SET_VLAN, _("Place the machine on the VLAN")) + ) + radius_general_policy = models.CharField( + max_length=32, + choices=CHOICE_RADIUS, + default='DEFINED' + ) + unknown_machine = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, + verbose_name=_("Policy for unknown machines"), + ) + unknown_machine_vlan = models.ForeignKey( + 'machines.Vlan', + on_delete=models.PROTECT, + related_name='unknown_machine_vlan', + blank=True, + null=True, + verbose_name=_("Unknown machines VLAN"), + help_text=_("VLAN for unknown machines if not rejected") + ) + unknown_port = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, + verbose_name=_("Policy for unknown ports"), + ) + unknown_port_vlan = models.ForeignKey( + 'machines.Vlan', + on_delete=models.PROTECT, + related_name='unknown_port_vlan', + blank=True, + null=True, + verbose_name=_("Unknown ports VLAN"), + help_text=_("VLAN for unknown ports if not rejected") + ) + unknown_room = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, + verbose_name=_("Policy for machines connecting from unregistered rooms" + " (relevant on ports with STRICT RADIUS mode)"), + ) + unknown_room_vlan = models.ForeignKey( + 'machines.Vlan', + related_name='unknown_room_vlan', + on_delete=models.PROTECT, + blank=True, + null=True, + verbose_name=_("Unknown rooms VLAN"), + help_text=_("VLAN for unknown rooms if not rejected") + ) + non_member = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, + verbose_name=_("Policy for non members"), + ) + non_member_vlan = models.ForeignKey( + 'machines.Vlan', + related_name='non_member_vlan', + on_delete=models.PROTECT, + blank=True, + null=True, + verbose_name=_("Non members VLAN"), + help_text=_("VLAN for non members if not rejected") + ) + banned = models.CharField( + max_length=32, + choices=CHOICE_POLICY, + default=REJECT, + verbose_name=_("Policy for banned users"), + ) + banned_vlan = models.ForeignKey( + 'machines.Vlan', + related_name='banned_vlan', + on_delete=models.PROTECT, + blank=True, + null=True, + verbose_name=_("Banned users VLAN"), + help_text=_("VLAN for banned users if not rejected") + ) + vlan_decision_ok = models.OneToOneField( + 'machines.Vlan', + on_delete=models.PROTECT, + related_name='vlan_ok_option', + blank=True, + null=True + ) + + +def default_invoice(): + tpl, _ = DocumentTemplate.objects.get_or_create( + name="Re2o default invoice", + template="templates/default_invoice.tex" + ) + return tpl.id + + +def default_voucher(): + tpl, _ = DocumentTemplate.objects.get_or_create( + name="Re2o default voucher", + template="templates/default_voucher.tex" + ) + return tpl.id + + +class CotisationsOption(AclMixin, PreferencesModel): + class Meta: + verbose_name = _("cotisations options") + + invoice_template = models.OneToOneField( + 'preferences.DocumentTemplate', + verbose_name=_("Template for invoices"), + related_name="invoice_template", + on_delete=models.PROTECT, + default=default_invoice, + ) + voucher_template = models.OneToOneField( + 'preferences.DocumentTemplate', + verbose_name=_("Template for subscription voucher"), + related_name="voucher_template", + on_delete=models.PROTECT, + default=default_voucher, + ) + send_voucher_mail = models.BooleanField( + verbose_name=_("Send voucher by email when the invoice is controlled."), + default=False, + ) + + +class DocumentTemplate(RevMixin, AclMixin, models.Model): + """Represent a template in order to create documents such as invoice or + subscription voucher. + """ + template = models.FileField( + upload_to='templates/', + verbose_name=_('template') + ) + name = models.CharField( + max_length=125, + verbose_name=_('name'), + unique=True + ) + + class Meta: + verbose_name = _("document template") + verbose_name_plural = _("document templates") + + def __str__(self): + return str(self.name) + +@receiver(models.signals.post_delete, sender=DocumentTemplate) +def auto_delete_file_on_delete(sender, instance, **kwargs): + """ + Deletes file from filesystem + when corresponding `DocumentTemplate` object is deleted. + """ + if instance.template: + if os.path.isfile(instance.template.path): + os.remove(instance.template.path) + + +@receiver(models.signals.pre_save, sender=DocumentTemplate) +def auto_delete_file_on_change(sender, instance, **kwargs): + """ + Deletes old file from filesystem + when corresponding `DocumentTemplate` object is updated + with new file. + """ + if not instance.pk: + return False + + try: + old_file = DocumentTemplate.objects.get(pk=instance.pk).template + except DocumentTemplate.DoesNotExist: + return False + + new_file = instance.template + if not old_file == new_file: + if os.path.isfile(old_file.path): + os.remove(old_file.path) diff --git a/preferences/templates/preferences/aff_document_template.html b/preferences/templates/preferences/aff_document_template.html new file mode 100644 index 00000000..8d2184b4 --- /dev/null +++ b/preferences/templates/preferences/aff_document_template.html @@ -0,0 +1,50 @@ +{% 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 %} +{% load logs_extra %} + + + + + + + + + + {% for template in document_template_list %} + + + + + + {% endfor %} +
{% trans "Document template" %}{% trans "File" %}
{{ template.name }}{{template.template}} + {% can_edit template %} + {% include 'buttons/edit.html' with href='preferences:edit-document-template' id=template.id %} + {% acl_end %} + {% history_button template %} +
+ diff --git a/preferences/templates/preferences/aff_radiuskey.html b/preferences/templates/preferences/aff_radiuskey.html new file mode 100644 index 00000000..7c972abe --- /dev/null +++ b/preferences/templates/preferences/aff_radiuskey.html @@ -0,0 +1,57 @@ +{% 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 logs_extra %} +{% load i18n %} + + + + + + + + + + + + + {% for radiuskey in radiuskey_list %} + + + + + + + + {% endfor %} +
{% trans "RADIUS key ID" %}{% trans "Comment" %}{% trans "Default RADIUS key for switches" %}{% trans "RADIUS key used by the swithes" %}
{{ radiuskey.id }}{{ radiuskey.comment }}{{ radiuskey.default_switch }}{{ radiuskey.switch_set.all|join:", " }} + {% can_edit radiuskey %} + {% include 'buttons/edit.html' with href='preferences:edit-radiuskey' id=radiuskey.id %} + {% acl_end %} + {% can_delete radiuskey %} + {% include 'buttons/suppr.html' with href='preferences:del-radiuskey' id=radiuskey.id %} + {% acl_end %} + {% history_button radiuskey %} +
+ diff --git a/preferences/templates/preferences/aff_radiusoptions.html b/preferences/templates/preferences/aff_radiusoptions.html new file mode 100644 index 00000000..41cb1846 --- /dev/null +++ b/preferences/templates/preferences/aff_radiusoptions.html @@ -0,0 +1,96 @@ +{% 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 %} + + + + + + + + + + + +
{% trans "General policy for VLAN setting" %}{{ radiusoptions.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" %}{% blocktrans with vlan_decision_ok=radiusoptions.vlan_decision_ok %}VLAN {{ vlan_decision_ok }}{% endblocktrans %}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Situation" %}{% trans "Behaviour" %}
{% trans "Unknown machine" %} + {% if radiusoptions.unknown_machine == 'REJECT' %} + {% trans "Reject" %} + {% else %} + {% blocktrans with unknown_machine_vlan=radiusoptions.unknown_machine_vlan %}VLAN {{ unknown_machine_vlan }}{% endblocktrans %} + {% endif %} +
{% trans "Unknown port" %} + {% if radiusoptions.unknown_port == 'REJECT' %} + {% trans "Reject" %} + {% else %} + {% blocktrans with unknown_port_vlan=radiusoptions.unknown_port_vlan %}VLAN {{ unknown_port_vlan }}{% endblocktrans %} + {% endif %} +
{% trans "Unknown room" %} + {% if radiusoptions.unknown_room == 'REJECT' %} + {% trans "Reject" %} + {% else %} + {% blocktrans with unknown_room_vlan=radiusoptions.unknown_room_vlan %}VLAN {{ unknown_room_vlan }}{% endblocktrans %} + {% endif %} +
{% trans "Non member" %} + {% if radiusoptions.non_member == 'REJECT' %} + {% trans "Reject" %} + {% else %} + {% blocktrans with non_member_vlan=radiusoptions.non_member_vlan %}VLAN {{ non_member_vlan }}{% endblocktrans %} + {% endif %} +
{% trans "Banned user" %} + {% if radiusoptions.unknown_port == 'REJECT' %} + {% trans "Reject" %} + {% else %} + {% blocktrans with banned_vlan=radiusoptions.banned_vlan %}VLAN {{ banned_vlan }}{% endblocktrans %} + {% endif %} +
+ diff --git a/preferences/templates/preferences/aff_reminder.html b/preferences/templates/preferences/aff_reminder.html new file mode 100644 index 00000000..c0586b31 --- /dev/null +++ b/preferences/templates/preferences/aff_reminder.html @@ -0,0 +1,54 @@ +{% 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 logs_extra %} +{% load i18n %} + + + + + + + + + + + {% for reminder in reminder_list %} + + + + + + {% endfor %} +
{% trans "Number of days before the reminder" %}{% trans "Message for this reminder" %}
{{ reminder.days }}{{ reminder.message }} + {% can_edit reminder %} + {% include 'buttons/edit.html' with href='preferences:edit-reminder' id=reminder.id %} + {% acl_end %} + {% can_delete reminder %} + {% include 'buttons/suppr.html' with href='preferences:del-reminder' id=reminder.id %} + {% acl_end %} + {% history_button reminder %} +
+ + diff --git a/preferences/templates/preferences/aff_service.html b/preferences/templates/preferences/aff_service.html index c08e14e0..dce81768 100644 --- a/preferences/templates/preferences/aff_service.html +++ b/preferences/templates/preferences/aff_service.html @@ -42,10 +42,13 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ 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 %} + {% can_delete service %} + {% include 'buttons/suppr.html' with href='preferences:del-service' id=service.id %} + {% acl_end %} + {% history_button service %} {% endfor %} diff --git a/preferences/templates/preferences/aff_switchmanagementcred.html b/preferences/templates/preferences/aff_switchmanagementcred.html new file mode 100644 index 00000000..b45dd356 --- /dev/null +++ b/preferences/templates/preferences/aff_switchmanagementcred.html @@ -0,0 +1,55 @@ +{% 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 logs_extra %} +{% load i18n %} + + + + + + + + + + + + {% for switchmanagementcred in switchmanagementcred_list %} + + + + + + + {% endfor %} +
{% trans "Switch login" %}{% trans "Default switch management credentials" %}{% trans "Management credentials used by the switches" %}
{{ switchmanagementcred.management_id }}{{ switchmanagementcred.default_switch }}{{ switchmanagementcred.switch_set.all|join:", " }} + {% can_edit switchmanagementcred %} + {% include 'buttons/edit.html' with href='preferences:edit-switchmanagementcred' id=switchmanagementcred.id %} + {% acl_end %} + {% can_delete switchmanagementcred %} + {% include 'buttons/suppr.html' with href='preferences:del-switchmanagementcred' id=switchmanagementcred.id %} + {% acl_end %} + {% history_button switchmanagementcred %} +
+ diff --git a/preferences/templates/preferences/delete.html b/preferences/templates/preferences/delete.html new file mode 100644 index 00000000..7897e286 --- /dev/null +++ b/preferences/templates/preferences/delete.html @@ -0,0 +1,42 @@ +{% extends 'preferences/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 i18n %} + +{% block title %}{% trans "Deletion of preferences" %}{% endblock %} + +{% block content %} + +
+ {% csrf_token %} +

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

+ {% trans "Confirm" as tr_confirm %} + {% bootstrap_button tr_confirm button_type="submit" icon="trash" %} +
+
+
+
+{% endblock %} diff --git a/preferences/templates/preferences/display_preferences.html b/preferences/templates/preferences/display_preferences.html index 6a499969..a7d15679 100644 --- a/preferences/templates/preferences/display_preferences.html +++ b/preferences/templates/preferences/display_preferences.html @@ -1,4 +1,4 @@ -{% extends "preferences/sidebar.html" %} +{% extends 'preferences/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 @@ -31,212 +31,477 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block title %}{% trans "Preferences" %}{% endblock %} {% block content %} -

{% 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 "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 "Main site URL" %}{{ generaloptions.main_site_url }}
{% trans "Summary of the General Terms of Use" %}{{ generaloptions.GTU_sum_up }}{% trans "General Terms of Use" %}{{ generaloptions.GTU }} +
+ + + + + + + + + + + +
{% 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 "Edit" %} + +

+ + + + + + + + + + + + + + + + + + +
{% 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 "Self registration" %}{{ useroptions.self_adhesion|tick }}{% trans "Delete not yet active users after" %}{% blocktrans with delete_notyetactive=useroptions.delete_notyetactive %}{{ delete_notyetactive }} days{% endblocktrans %}
{% trans "All users are active by default" %}{{ useroptions.all_users_active|tick }}
+ +

{% trans "Users general permissions" %}

+ + + + + + + + + + + + + + + + + +
{% trans "Default shell for users" %}{{ useroptions.shell_default }}{% trans "Users can edit their shell" %}{{ useroptions.self_change_shell|tick }}
{% trans "Users can edit their room" %}{{ useroptions.self_change_room|tick }}{% trans "Telephone number required" %}{{ useroptions.is_tel_mandatory|tick }}
{% trans "GPG fingerprint field" %}{{ useroptions.gpg_fingerprint|tick }}
+
+
+
+ +
+ + +
+ + + + {% 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 "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 "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 "VLAN for non members machines" %}{{ topologieoptions.vlan_non_member }}
+ +

{% trans "RADIUS keys" %}

+ {% can_create RadiusKey%} +
{% trans " Add a RADIUS key" %} + {% acl_end %} + {% include 'preferences/aff_radiuskey.html' with radiuskey_list=radiuskey_list %} + +
+
+ +
+ +
+ + {% 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 "Web management, activated in case of automatic provision" %}{{ topologieoptions.switchs_web_management }}{% trans "REST management, activated in case of automatic provision" %}{{ topologieoptions.switchs_rest_management }}
+ + + +
{% if topologieoptions.provision_switchs_enabled %}{% trans "Provision of configuration for switches" %}{% else %}{% trans "Provision of configuration for switches" %}{% endif%}
+ + + + + + + + + + + + + + + + + + + + + + + + + +
{% trans "Switches with automatic provision" %}{{ topologieoptions.provisioned_switchs|join:", " }} {% if topologieoptions.provisioned_switchs %}{% trans "OK" %}{% else %}{% trans "Missing" %}{% endif %}
{% trans "IP range for the management of switches" %}{{ topologieoptions.switchs_ip_type }} {% if topologieoptions.switchs_ip_type %}{% trans "OK" %}{% else %}{% trans "Missing" %}{% endif %}
{% trans "Server for the configuration of switches" %}{{ topologieoptions.switchs_management_interface }} {% if topologieoptions.switchs_management_interface %} - {{ topologieoptions.switchs_management_interface_ip }} {% trans "OK" %}{% else %}{% trans "Missing" %}{% endif %}
{% trans "Provision of configuration mode for switches" %}{{ topologieoptions.switchs_provision }}
{% trans "TFTP mode" %}{% trans "OK" %}
{% trans "SFTP mode" %}{% if topologieoptions.switchs_management_sftp_creds %}{% trans "OK" %}{% else %}{% trans "Missing credentials" %}{% endif %}
+ +
{% trans "Switch management credentials" %}
+ {% can_create SwitchManagementCred%} + {% trans " Add switch management credentials" %} + {% acl_end %} +

+

+ {% if switchmanagementcred_list %}{% trans "OK" %}{% else %}{% trans "Missing" %}{% endif %} + {% include 'preferences/aff_switchmanagementcred.html' with switchmanagementcred_list=switchmanagementcred_list %} +
+
+ +
+ +
+ + + {% trans "Edit" %} + + {% include 'preferences/aff_radiusoptions.html' %} +
+
+ +
+ +
{% 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 "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 "President of the association"%}{{ assooptions.pres_name }}
+
+
+
+ +
+ {% can_create DocumentTemplate %} + + {% trans "Add a document template" %} + + {% acl_end %} + + {% trans "Delete one or several document templates" %} + + {% include 'preferences/aff_document_template.html' %} +
+
+ +
+ +
+ + + + + + + + + + + + + + + +
{% trans "Send voucher by email" %}{{ cotisationsoptions.send_voucher_mail | tick }} +
{% trans "Invoices' template" %}{{ cotisationsoptions.invoice_template }}
{% trans "Vouchers' template" %}{{ cotisationsoptions.voucher_template }}
+
+
+ + +
+ +
+ {% 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" %} +

+ + + + + + + + + + +
{% trans "Welcome email (in French)" %}{{ mailmessageoptions.welcome_mail_fr|safe }}
{% trans "Welcome email (in English)" %}{{ mailmessageoptions.welcome_mail_en|safe }}
+
+
+ +
+ +
+ {% can_create preferences.Reminder%} + + {% trans " Add a reminder" %} +

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

+ {% acl_end %} + {% include 'preferences/aff_service.html' with service_list=service_list %} + +
+
+ +
+ +
+ + {% 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 "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 }}
+

+ + + + + + + + + + + +
{% 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 356f2362..b5f8c506 100644 --- a/preferences/templates/preferences/edit_preferences.html +++ b/preferences/templates/preferences/edit_preferences.html @@ -1,4 +1,4 @@ -{% extends "preferences/sidebar.html" %} +{% extends 'preferences/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 @@ -36,9 +36,15 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} - {% massive_bootstrap_form options 'utilisateur_asso' %} + {% massive_bootstrap_form options 'utilisateur_asso,automatic_provision_switchs' %} + {% if formset %} + {{ formset.management_form }} + {% for f in formset %} + {% bootstrap_form f %} + {% endfor %} + {% endif %} {% trans "Edit" as tr_edit %} - {% bootstrap_button tr_edit button_type="submit" icon="star" %} + {% bootstrap_button tr_edit button_type="submit" icon='ok' button_class='btn-success' %}


diff --git a/preferences/templates/preferences/preferences.html b/preferences/templates/preferences/preferences.html index e4ad4ba2..308d121e 100644 --- a/preferences/templates/preferences/preferences.html +++ b/preferences/templates/preferences/preferences.html @@ -1,4 +1,4 @@ -{% extends "preferences/sidebar.html" %} +{% extends 'preferences/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 @@ -25,6 +25,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load bootstrap3 %} {% load i18n %} +{% load massive_bootstrap_form %} {% block title %}{% trans "Preferences" %}{% endblock %} @@ -37,9 +38,9 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% csrf_token %} {% if preferenceform %} - {% bootstrap_form preferenceform %} + {% massive_bootstrap_form preferenceform 'members' %} {% endif %} - {% bootstrap_button action_name button_type="submit" icon="star" %} + {% bootstrap_button action_name button_type="submit" icon='ok' button_class='btn-success' %}


diff --git a/preferences/templates/preferences/sidebar.html b/preferences/templates/preferences/sidebar.html index 4f69298b..2dccf639 100644 --- a/preferences/templates/preferences/sidebar.html +++ b/preferences/templates/preferences/sidebar.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.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 @@ -22,6 +22,8 @@ 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 sidebar %} diff --git a/preferences/urls.py b/preferences/urls.py index a89fcdf3..9bfd67d3 100644 --- a/preferences/urls.py +++ b/preferences/urls.py @@ -66,13 +66,23 @@ urlpatterns = [ views.edit_options, name='edit-options' ), + url( + r'^edit_options/(?P
RadiusOption)$', + views.edit_options, + name='edit-options' + ), + url( + r'^edit_options/(?P
CotisationsOption)$', + views.edit_options, + name='edit-options' + ), url(r'^add_service/$', views.add_service, name='add-service'), url( r'^edit_service/(?P[0-9]+)$', views.edit_service, name='edit-service' ), - url(r'^del_service/$', views.del_service, name='del-service'), + url(r'^del_service/(?P[0-9]+)$', views.del_service, name='del-service'), url(r'^add_mailcontact/$', views.add_mailcontact, name='add-mailcontact'), url( r'^edit_mailcontact/(?P[0-9]+)$', @@ -80,5 +90,41 @@ urlpatterns = [ name='edit-mailcontact' ), url(r'^del_mailcontact/$', views.del_mailcontact, name='del-mailcontact'), + url(r'^add_reminder/$', views.add_reminder, name='add-reminder'), + url( + r'^edit_reminder/(?P[0-9]+)$', + views.edit_reminder, + name='edit-reminder' + ), + url(r'^del_reminder/(?P[0-9]+)$', views.del_reminder, name='del-reminder'), + url(r'^add_radiuskey/$', views.add_radiuskey, name='add-radiuskey'), + url( + r'^edit_radiuskey/(?P[0-9]+)$', + views.edit_radiuskey, + name='edit-radiuskey' + ), + url(r'^del_radiuskey/(?P[0-9]+)$', views.del_radiuskey, name='del-radiuskey'), + url(r'^add_switchmanagementcred/$', views.add_switchmanagementcred, name='add-switchmanagementcred'), + url( + r'^edit_switchmanagementcred/(?P[0-9]+)$', + views.edit_switchmanagementcred, + name='edit-switchmanagementcred' + ), + url(r'^del_switchmanagementcred/(?P[0-9]+)$', views.del_switchmanagementcred, name='del-switchmanagementcred'), + url( + r'^add_document_template/$', + views.add_document_template, + name='add-document-template' + ), + url( + r'^edit_document_template/(?P[0-9]+)$', + views.edit_document_template, + name='edit-document-template' + ), + url( + r'^del_document_template/$', + views.del_document_template, + name='del-document-template' + ), url(r'^$', views.display_options, name='display-options'), ] diff --git a/preferences/views.py b/preferences/views.py index 559cdfef..ecb3826e 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -41,10 +41,16 @@ from django.utils.translation import ugettext as _ from reversion import revisions as reversion from re2o.views import form -from re2o.acl import can_create, can_edit, can_delete_set, can_view_all +from re2o.acl import can_create, can_edit, can_delete_set, can_view_all, can_delete +from .forms import MailContactForm, DelMailContactForm from .forms import ( - ServiceForm, DelServiceForm, MailContactForm, DelMailContactForm + ServiceForm, + ReminderForm, + RadiusKeyForm, + SwitchManagementCredForm, + DocumentTemplateForm, + DelDocumentTemplateForm ) from .models import ( Service, @@ -55,7 +61,13 @@ from .models import ( MailMessageOption, GeneralOption, OptionalTopologie, - HomeOption + HomeOption, + Reminder, + RadiusKey, + SwitchManagementCred, + RadiusOption, + CotisationsOption, + DocumentTemplate ) from . import models from . import forms @@ -76,6 +88,12 @@ def display_options(request): mailmessageoptions, _created = MailMessageOption.objects.get_or_create() service_list = Service.objects.all() mailcontact_list = MailContact.objects.all() + reminder_list = Reminder.objects.all() + radiuskey_list = RadiusKey.objects.all() + switchmanagementcred_list = SwitchManagementCred.objects.all() + radiusoptions, _ = RadiusOption.objects.get_or_create() + cotisationsoptions, _created = CotisationsOption.objects.get_or_create() + document_template_list = DocumentTemplate.objects.order_by('name') return form({ 'useroptions': useroptions, 'machineoptions': machineoptions, @@ -85,7 +103,13 @@ def display_options(request): 'homeoptions': homeoptions, 'mailmessageoptions': mailmessageoptions, 'service_list': service_list, - 'mailcontact_list': mailcontact_list + 'mailcontact_list': mailcontact_list, + 'reminder_list': reminder_list, + 'radiuskey_list' : radiuskey_list, + 'switchmanagementcred_list': switchmanagementcred_list, + 'radiusoptions' : radiusoptions, + 'cotisationsoptions': cotisationsoptions, + 'document_template_list': document_template_list, }, 'preferences/display_preferences.html', request) @@ -95,7 +119,7 @@ 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, _("Unknown object")) + messages.error(request, _("Unknown object.")) return redirect(reverse('preferences:display-options')) options_instance, _created = model.objects.get_or_create() @@ -121,7 +145,9 @@ def edit_options(request, section): messages.success(request, _("The preferences were edited.")) return redirect(reverse('preferences:display-options')) return form( - {'options': options}, + { + 'options': options, + }, 'preferences/edit_preferences.html', request ) @@ -133,10 +159,7 @@ def add_service(request): """Ajout d'un service de la page d'accueil""" service = ServiceForm(request.POST or None, request.FILES or None) if service.is_valid(): - with transaction.atomic(), reversion.create_revision(): - service.save() - reversion.set_user(request.user) - reversion.set_comment("Creation") + service.save() messages.success(request, _("The service was added.")) return redirect(reverse('preferences:display-options')) return form( @@ -156,14 +179,7 @@ def edit_service(request, service_instance, **_kwargs): instance=service_instance ) if service.is_valid(): - with transaction.atomic(), reversion.create_revision(): - service.save() - reversion.set_user(request.user) - reversion.set_comment( - "Field(s) edited: %s" % ', '.join( - field for field in service.changed_data - ) - ) + service.save() messages.success(request, _("The service was edited.")) return redirect(reverse('preferences:display-options')) return form( @@ -172,31 +188,170 @@ def edit_service(request, service_instance, **_kwargs): request ) - @login_required -@can_delete_set(Service) -def del_service(request, instances): +@can_delete(Service) +def del_service(request, service_instance, **_kwargs): """Suppression d'un service de la page d'accueil""" - services = DelServiceForm(request.POST or None, instances=instances) - if services.is_valid(): - services_dels = services.cleaned_data['services'] - for services_del in services_dels: - try: - with transaction.atomic(), reversion.create_revision(): - services_del.delete() - reversion.set_user(request.user) - messages.success(request, _("The service was deleted.")) - except ProtectedError: - messages.error(request, _("Error: the service %s can't be" - " deleted.") % services_del) + if request.method == "POST": + service_instance.delete() + messages.success(request, _("The service was deleted.")) return redirect(reverse('preferences:display-options')) return form( - {'preferenceform': services, 'action_name': _("Delete")}, + {'objet': service_instance, 'objet_name': 'service'}, + 'preferences/delete.html', + request + ) + +@login_required +@can_create(Reminder) +def add_reminder(request): + """Ajout d'un mail de rappel""" + reminder = ReminderForm(request.POST or None, request.FILES or None) + if reminder.is_valid(): + reminder.save() + messages.success(request, _("The reminder was added.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': reminder, 'action_name': _("Add a reminder")}, + 'preferences/preferences.html', + request + ) + +@login_required +@can_edit(Reminder) +def edit_reminder(request, reminder_instance, **_kwargs): + """Edition reminder""" + reminder = ReminderForm( + request.POST or None, + request.FILES or None, + instance=reminder_instance + ) + if reminder.is_valid(): + reminder.save() + messages.success(request, _("The reminder was edited.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': reminder, 'action_name': _("Edit")}, 'preferences/preferences.html', request ) + +@login_required +@can_delete(Reminder) +def del_reminder(request, reminder_instance, **_kwargs): + """Destruction d'un reminder""" + if request.method == "POST": + reminder_instance.delete() + messages.success(request, _("The reminder was deleted.")) + return redirect(reverse('preferences:display-options')) + return form( + {'objet': reminder_instance, 'objet_name': 'reminder'}, + 'preferences/delete.html', + request + ) + + +@login_required +@can_create(RadiusKey) +def add_radiuskey(request): + """Ajout d'une clef radius""" + radiuskey = RadiusKeyForm(request.POST or None) + if radiuskey.is_valid(): + radiuskey.save() + messages.success(request, _("The RADIUS key was added.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': radiuskey, 'action_name': _("Add a RADIUS key")}, + 'preferences/preferences.html', + request + ) + +@can_edit(RadiusKey) +def edit_radiuskey(request, radiuskey_instance, **_kwargs): + """Edition des clefs radius""" + radiuskey = RadiusKeyForm(request.POST or None, instance=radiuskey_instance) + if radiuskey.is_valid(): + radiuskey.save() + messages.success(request, _("The RADIUS key was edited.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': radiuskey, 'action_name': _("Edit")}, + 'preferences/preferences.html', + request + ) + + +@login_required +@can_delete(RadiusKey) +def del_radiuskey(request, radiuskey_instance, **_kwargs): + """Destruction d'un radiuskey""" + if request.method == "POST": + try: + radiuskey_instance.delete() + messages.success(request, _("The RADIUS key was deleted.")) + except ProtectedError: + messages.error(request, _("The RADIUS key is assigned to at least" + " one switch, you can't delete it.")) + return redirect(reverse('preferences:display-options')) + return form( + {'objet': radiuskey_instance, 'objet_name': 'radiuskey'}, + 'preferences/delete.html', + request + ) + + +@login_required +@can_create(SwitchManagementCred) +def add_switchmanagementcred(request): + """Ajout de creds de management""" + switchmanagementcred = SwitchManagementCredForm(request.POST or None) + if switchmanagementcred.is_valid(): + switchmanagementcred.save() + messages.success(request, _("The switch management credentials were added.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': switchmanagementcred, 'action_name': _("Add switch management credentials")}, + 'preferences/preferences.html', + request + ) + +@can_edit(SwitchManagementCred) +def edit_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs): + """Edition des creds de management""" + switchmanagementcred = SwitchManagementCredForm(request.POST or None, instance=switchmanagementcred_instance) + if switchmanagementcred.is_valid(): + switchmanagementcred.save() + messages.success(request, _("The switch management credentials were edited.")) + return redirect(reverse('preferences:display-options')) + return form( + {'preferenceform': switchmanagementcred, 'action_name': _("Edit")}, + 'preferences/preferences.html', + request + ) + + +@login_required +@can_delete(SwitchManagementCred) +def del_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs): + """Destruction d'un switchmanagementcred""" + if request.method == "POST": + try: + switchmanagementcred_instance.delete() + messages.success(request, _("The switch management credentials were deleted.")) + except ProtectedError: + messages.error(request, _("The switch management credentials are" + " assigned to at least one switch, you" + " can't delete them.")) + return redirect(reverse('preferences:display-options')) + return form( + {'objet': switchmanagementcred_instance, 'objet_name': 'switchmanagementcred'}, + 'preferences/delete.html', + request + ) + + @login_required @can_create(MailContact) def add_mailcontact(request): @@ -258,3 +413,86 @@ def del_mailcontact(request, instances): request ) + +@login_required +@can_create(DocumentTemplate) +def add_document_template(request): + """ + View used to add a document template. + """ + document_template = DocumentTemplateForm( + request.POST or None, + request.FILES or None, + ) + if document_template.is_valid(): + document_template.save() + messages.success( + request, + _("The document template was created.") + ) + return redirect(reverse('preferences:display-options')) + return form({ + 'preferenceform': document_template, + 'action_name': _("Add"), + 'title': _("New document template") + }, 'preferences/preferences.html', request) + + +@login_required +@can_edit(DocumentTemplate) +def edit_document_template(request, document_template_instance, **_kwargs): + """ + View used to edit a document_template. + """ + document_template = DocumentTemplateForm( + request.POST or None, + request.FILES or None, + instance=document_template_instance) + if document_template.is_valid(): + if document_template.changed_data: + document_template.save() + messages.success( + request, + _("The document template was edited.") + ) + return redirect(reverse('preferences:display-options')) + return form({ + 'preferenceform': document_template, + 'action_name': _("Edit"), + 'title': _("Edit document template") + }, 'preferences/preferences.html', request) + + +@login_required +@can_delete_set(DocumentTemplate) +def del_document_template(request, instances): + """ + View used to delete a set of document template. + """ + document_template = DelDocumentTemplateForm( + request.POST or None, instances=instances) + if document_template.is_valid(): + document_template_del = document_template.cleaned_data['document_templates'] + for document_template in document_template_del: + try: + document_template.delete() + messages.success( + request, + _("The document template %(document_template)s was deleted.") % { + 'document_template': document_template + } + ) + except ProtectedError: + messages.error( + request, + _("The document template %(document_template)s can't be deleted \ + because it is currently being used.") % { + 'document_template': document_template + } + ) + return redirect(reverse('preferences:display-options')) + return form({ + 'preferenceform': document_template, + 'action_name': _("Delete"), + 'title': _("Delete document template") + }, 'preferences/preferences.html', request) diff --git a/re2o/aes_field.py b/re2o/aes_field.py index 2720f5af..eeab5e12 100644 --- a/re2o/aes_field.py +++ b/re2o/aes_field.py @@ -37,8 +37,8 @@ from django.db import models from django import forms from django.conf import settings -EOD = '`%EofD%`' # This should be something that will not occur in strings - +EOD_asbyte = b'`%EofD%`' # This should be something that will not occur in strings +EOD = EOD_asbyte.decode('utf-8') def genstring(length=16, chars=string.printable): """ Generate a random string of length `length` and composed of @@ -46,23 +46,23 @@ def genstring(length=16, chars=string.printable): return ''.join([choice(chars) for i in range(length)]) -def encrypt(key, s): - """ AES Encrypt a secret `s` with the key `key` """ +def encrypt(key, secret): + """ AES Encrypt a secret with the key `key` """ obj = AES.new(key) - datalength = len(s) + len(EOD) + datalength = len(secret) + len(EOD) if datalength < 16: saltlength = 16 - datalength else: saltlength = 16 - datalength % 16 - ss = ''.join([s, EOD, genstring(saltlength)]) - return obj.encrypt(ss) + encrypted_secret = ''.join([secret, EOD, genstring(saltlength)]) + return obj.encrypt(encrypted_secret) -def decrypt(key, s): - """ AES Decrypt a secret `s` with the key `key` """ +def decrypt(key, secret): + """ AES Decrypt a secret with the key `key` """ obj = AES.new(key) - ss = obj.decrypt(s) - return ss.split(bytes(EOD, 'utf-8'))[0] + uncrypted_secret = obj.decrypt(secret) + return uncrypted_secret.split(EOD_asbyte)[0] class AESEncryptedFormField(forms.CharField): @@ -81,27 +81,28 @@ class AESEncryptedField(models.CharField): if value is None: return None try: - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') - except Exception as e: - raise ValueError(value) + return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8') + except UnicodeDecodeError as e: + raise ValueError( + "Could not decode your field %s, your settings.AES_KEY " + "is probably wrong." % self.name + ) def from_db_value(self, value, *args, **kwargs): if value is None: return value try: - return decrypt(settings.AES_KEY, - binascii.a2b_base64(value)).decode('utf-8') - except Exception as e: - raise ValueError(value) + return decrypt(settings.AES_KEY, binascii.a2b_base64(value)).decode('utf-8') + except UnicodeDecodeError as e: + raise ValueError( + "Could not decode your field %s, your settings.AES_KEY " + "is probably wrong." % self.name + ) def get_prep_value(self, value): if value is None: return value - return binascii.b2a_base64(encrypt( - settings.AES_KEY, - value - )).decode('utf-8') + return binascii.b2a_base64(encrypt(settings.AES_KEY, value)).decode('utf-8') def formfield(self, **kwargs): defaults = {'form_class': AESEncryptedFormField} diff --git a/re2o/base.py b/re2o/base.py new file mode 100644 index 00000000..fff2278c --- /dev/null +++ b/re2o/base.py @@ -0,0 +1,267 @@ +# -*- 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. +# +# Copyright © 2018 Gabriel Détraz +# +# 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. + +# -*- coding: utf-8 -*- +""" +Regroupe les fonctions transversales utiles + +Et non corrélées/dépendantes des autres applications +""" + +import smtplib + +from django.utils.translation import ugettext_lazy as _ +from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger + +from re2o.settings import EMAIL_HOST + + +# Mapping of srtftime format for better understanding +# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior +datetime_mapping={ + '%a': '%a', + '%A': '%A', + '%w': '%w', + '%d': 'dd', + '%b': '%b', + '%B': '%B', + '%m': 'mm', + '%y': 'yy', + '%Y': 'yyyy', + '%H': 'HH', + '%I': 'HH(12h)', + '%p': 'AMPM', + '%M': 'MM', + '%S': 'SS', + '%f': 'µµ', + '%z': 'UTC(+/-HHMM)', + '%Z': 'UTC(TZ)', + '%j': '%j', + '%U': 'ww', + '%W': 'ww', + '%c': '%c', + '%x': '%x', + '%X': '%X', + '%%': '%%', +} + + +def smtp_check(local_part): + """Return True if the local_part is already taken + False if available""" + try: + srv = smtplib.SMTP(EMAIL_HOST) + srv.putcmd("vrfy", local_part) + reply_code = srv.getreply()[0] + srv.close() + if reply_code in [250, 252]: + return True, _("This domain is already taken.") + except: + return True, _("SMTP unreachable.") + return False, None + + +def convert_datetime_format(format): + i=0 + new_format = "" + while i < len(format): + if format[i] == '%': + char = format[i:i+2] + new_format += datetime_mapping.get(char, char) + i += 2 + else: + new_format += format[i] + i += 1 + return new_format + + +def get_input_formats_help_text(input_formats): + """Returns a help text about the possible input formats""" + if len(input_formats) > 1: + help_text_template="Format: {main} {more}" + else: + help_text_template="Format: {main}" + more_text_template="" + help_text = help_text_template.format( + main=convert_datetime_format(input_formats[0]), + more=more_text_template.format( + '\n'.join(map(convert_datetime_format, input_formats)) + ) + ) + return help_text + + +class SortTable: + """ Class gathering uselful stuff to sort the colums of a table, according + to the column and order requested. It's used with a dict of possible + values and associated model_fields """ + + # All the possible possible values + # The naming convention is based on the URL or the views function + # The syntax to describe the sort to apply is a dict where the keys are + # the url value and the values are a list of model field name to use to + # order the request. They are applied in the order they are given. + # A 'default' might be provided to specify what to do if the requested col + # doesn't match any keys. + + USERS_INDEX = { + 'user_name': ['name'], + 'user_surname': ['surname'], + 'user_pseudo': ['pseudo'], + 'user_room': ['room'], + 'default': ['state', 'pseudo'] + } + USERS_INDEX_BAN = { + 'ban_user': ['user__pseudo'], + 'ban_start': ['date_start'], + 'ban_end': ['date_end'], + 'default': ['-date_end'] + } + USERS_INDEX_WHITE = { + 'white_user': ['user__pseudo'], + 'white_start': ['date_start'], + 'white_end': ['date_end'], + 'default': ['-date_end'] + } + USERS_INDEX_SCHOOL = { + 'school_name': ['name'], + 'default': ['name'] + } + MACHINES_INDEX = { + 'machine_name': ['name'], + 'default': ['pk'] + } + COTISATIONS_INDEX = { + 'cotis_user': ['user__pseudo'], + 'cotis_paiement': ['paiement__moyen'], + 'cotis_date': ['date'], + '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'], + 'control_paiement': ['paiement'], + 'control_date': ['date'], + 'control_valid': ['valid'], + 'control_control': ['control'], + 'control_id': ['id'], + 'control_user-id': ['user__id'], + 'default': ['-date'] + } + TOPOLOGIE_INDEX = { + 'switch_dns': ['interface__domain__name'], + 'switch_ip': ['interface__ipv4__ipv4'], + 'switch_loc': ['switchbay__name'], + 'switch_ports': ['number'], + 'switch_stack': ['stack__name'], + 'default': ['switchbay', 'stack', 'stack_member_id'] + } + TOPOLOGIE_INDEX_PORT = { + 'port_port': ['port'], + 'port_room': ['room__name'], + 'port_interface': ['machine_interface__domain__name'], + 'port_related': ['related__switch__name'], + 'port_radius': ['radius'], + 'port_vlan': ['vlan_force__name'], + 'default': ['port'] + } + TOPOLOGIE_INDEX_ROOM = { + 'room_name': ['name'], + 'default': ['name'] + } + TOPOLOGIE_INDEX_BUILDING = { + 'building_name': ['name'], + 'default': ['name'] + } + TOPOLOGIE_INDEX_BORNE = { + 'ap_name': ['interface__domain__name'], + 'ap_ip': ['interface__ipv4__ipv4'], + 'ap_mac': ['interface__mac_address'], + 'default': ['interface__domain__name'] + } + TOPOLOGIE_INDEX_STACK = { + 'stack_name': ['name'], + 'stack_id': ['stack_id'], + 'default': ['stack_id'], + } + TOPOLOGIE_INDEX_MODEL_SWITCH = { + 'model-switch_name': ['reference'], + 'model-switch_contructor': ['constructor__name'], + 'default': ['reference'], + } + TOPOLOGIE_INDEX_SWITCH_BAY = { + 'switch-bay_name': ['name'], + 'switch-bay_building': ['building__name'], + 'default': ['name'], + } + TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = { + 'constructor-switch_name': ['name'], + 'default': ['name'], + } + LOGS_INDEX = { + 'sum_date': ['revision__date_created'], + 'default': ['-revision__date_created'], + } + LOGS_STATS_LOGS = { + 'logs_author': ['user__name'], + 'logs_date': ['date_created'], + 'default': ['-date_created'] + } + + @staticmethod + def sort(request, col, order, values): + """ Check if the given values are possible and add .order_by() and + a .reverse() as specified according to those values """ + fields = values.get(col, None) + if not fields: + fields = values.get('default', []) + request = request.order_by(*fields) + if values.get(col, None) and order == 'desc': + return request.reverse() + else: + return request + + +def re2o_paginator(request, query_set, pagination_number): + """Paginator script for list display in re2o. + :request: + :query_set: Query_set to paginate + :pagination_number: Number of entries to display""" + paginator = Paginator(query_set, pagination_number) + page = request.GET.get('page') + try: + results = paginator.page(page) + except PageNotAnInteger: + # If page is not an integer, deliver first page. + results = paginator.page(1) + except EmptyPage: + # If page is out of range (e.g. 9999), deliver last page of results. + results = paginator.page(paginator.num_pages) + return results diff --git a/re2o/locale/fr/LC_MESSAGES/django.mo b/re2o/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 72cde111..00000000 Binary files a/re2o/locale/fr/LC_MESSAGES/django.mo and /dev/null differ diff --git a/re2o/locale/fr/LC_MESSAGES/django.po b/re2o/locale/fr/LC_MESSAGES/django.po index f54fdcc1..2f634fed 100644 --- a/re2o/locale/fr/LC_MESSAGES/django.po +++ b/re2o/locale/fr/LC_MESSAGES/django.po @@ -21,7 +21,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-23 15:03+0200\n" +"POT-Creation-Date: 2019-01-12 16:48+0100\n" "PO-Revision-Date: 2018-03-31 16:09+0002\n" "Last-Translator: Laouen Fernet \n" "Language-Team: \n" @@ -42,6 +42,14 @@ msgstr "Vous n'avez pas le droit d'accéder à ce menu." msgid "You don't have the right to edit the history." msgstr "Vous n'avez pas le droit de modifier l'historique." +#: base.py:76 +msgid "This domain is already taken." +msgstr "Ce domaine est déjà pris." + +#: base.py:78 +msgid "SMTP unreachable." +msgstr "SMTP injoignable." + #: mixins.py:111 #, python-format msgid "You don't have the right to create a %s object." @@ -87,22 +95,22 @@ msgstr "À propos de %(AssoName)s" #: templates/re2o/about.html:36 msgid "" "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." +"\">Rezo 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 "" "Re2o est un outil d'administration initié par Rézo Supélec Metz et quelques membres d'autres assocations de Rézo Metz et quelques membres d'autres associations 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 " @@ -190,7 +198,7 @@ msgstr "Accueil" msgid "Welcome to %(name_website)s" msgstr "Bienvenue sur %(name_website)s" -#: templates/re2o/index.html:43 templates/re2o/index.html:45 +#: templates/re2o/index.html:43 msgid "Registration" msgstr "Inscription" @@ -202,17 +210,25 @@ 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 +#: templates/re2o/index.html:45 +msgid "Sign up" +msgstr "S'inscrire" + +#: templates/re2o/index.html:55 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 " +"If you already have an account, log in. You can manage your subscriptions 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." +"Si vous avez déjà un compte, identifiez-vous. Vous pouvez gérer vos " +"cotisations à l'association, vos machines et tous vos services." + +#: templates/re2o/index.html:57 +msgid "Log in" +msgstr "Se connecter" #: templates/re2o/index.html:68 msgid "My profile" @@ -220,10 +236,10 @@ msgstr "Mon profil" #: templates/re2o/index.html:69 msgid "" -"To manage your subscription, your machines and all your services, access " +"To manage your subscriptions, your machines and all your services, access " "your profile." msgstr "" -"Pour gérer votre cotisation, vos machines et tous vos services, accéder à " +"Pour gérer vos cotisations, vos machines et tous vos services, accéder à " "votre profil." #: templates/re2o/index.html:70 @@ -232,7 +248,7 @@ msgstr "Accéder à mon profil" #: templates/re2o/index.html:79 msgid "Services of the organisation" -msgstr "Serices de l'association" +msgstr "Services de l'association" #: templates/re2o/index.html:93 msgid "Go there" @@ -249,5 +265,5 @@ 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" +msgid "Unable to get the information." +msgstr "Impossible d'obtenir l'information." diff --git a/re2o/login.py b/re2o/login.py index 471c2e02..0b552239 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -114,9 +114,9 @@ class CryptPasswordHasher(hashers.BasePasswordHasher): 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()) + salt = hash_password_salt(encoded) + return constant_time_compare(crypt.crypt(password, salt), + encoded) def safe_summary(self, encoded): """ diff --git a/re2o/management/commands/gen_contrib.py b/re2o/management/commands/gen_contrib.py index 9951a383..2b0afd39 100644 --- a/re2o/management/commands/gen_contrib.py +++ b/re2o/management/commands/gen_contrib.py @@ -32,16 +32,38 @@ class Command(BaseCommand): """ The command object for `gen_contrib` """ help = 'Update contributors list' + @staticmethod + def _contrib_file_generator(contributors): + """ + Generate the content of contributors.py + """ + buffer = "# -*- mode: python; coding: utf-8 -*-\n" + buffer += "\"\"\"re2o.contributors\n" + buffer += "A list of the proud contributors to Re2o\n" + buffer += "\"\"\"\n" + buffer += "\n" + buffer += "CONTRIBUTORS = [\n" + for name in contributors: + # Split name into parts + names = name.split() + + # Normalize it + names = list(map(str.capitalize, names)) + + # Put it back together + name_text = " ".join(names) + buffer += " '{}',\n".format(name_text) + buffer += "]" + + return buffer + def handle(self, *args, **options): - contributeurs = [ + contributors = [ item.split('\t')[1] for item in os.popen("git shortlog -s -n").read().split("\n") if '\t' in item ] - self.stdout.write(self.style.SUCCESS("Exportation Sucessfull")) + self.stdout.write(self.style.SUCCESS("Exportation Successful")) with open("re2o/contributors.py", "w") as contrib_file: - contrib_file.write("\"\"\"re2o.contributors\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)) + content = self._contrib_file_generator(contributors) + contrib_file.write(content) diff --git a/re2o/settings.py b/re2o/settings.py index 9dd52d1f..f2557b44 100644 --- a/re2o/settings.py +++ b/re2o/settings.py @@ -120,6 +120,7 @@ TEMPLATES = [ 'DIRS': [ # Use only absolute paths with '/' delimiters even on Windows os.path.join(BASE_DIR, 'templates').replace('\\', '/'), + os.path.join(BASE_DIR, 'media', 'templates').replace('\\', '/'), ], 'APP_DIRS': True, 'OPTIONS': { @@ -174,6 +175,7 @@ BOOTSTRAP_BASE_URL = '/javascript/bootstrap/' # Use only absolute paths with '/' delimiters even on Windows STATICFILES_DIRS = ( os.path.join(BASE_DIR, 'static').replace('\\', '/'), + "/usr/share/fonts-font-awesome/", ) # Directory where the static files served by the server are stored STATIC_ROOT = os.path.join(BASE_DIR, 'static_files') diff --git a/re2o/templates/re2o/about.html b/re2o/templates/re2o/about.html index f7afc69c..aab3b620 100644 --- a/re2o/templates/re2o/about.html +++ b/re2o/templates/re2o/about.html @@ -1,4 +1,4 @@ -{% extends "re2o/sidebar.html" %} +{% extends 're2o/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 @@ -35,7 +35,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% trans "About Re2o" %}

{% blocktrans trimmed %} Re2o is an administration tool initiated by - Rezo Supelec Metz and a few + Rezo 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 diff --git a/re2o/templates/re2o/contact.html b/re2o/templates/re2o/contact.html index f26e002b..b20fe875 100644 --- a/re2o/templates/re2o/contact.html +++ b/re2o/templates/re2o/contact.html @@ -1,4 +1,4 @@ -{% extends "re2o/sidebar.html" %} +{% extends 're2o/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 diff --git a/re2o/templates/re2o/history.html b/re2o/templates/re2o/history.html index 945a355e..d991b033 100644 --- a/re2o/templates/re2o/history.html +++ b/re2o/templates/re2o/history.html @@ -1,4 +1,4 @@ -{% extends "re2o/sidebar.html" %} +{% extends 're2o/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 @@ -30,7 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %}

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

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


diff --git a/re2o/templates/re2o/index.html b/re2o/templates/re2o/index.html index f7adacbc..a6b1a8c5 100644 --- a/re2o/templates/re2o/index.html +++ b/re2o/templates/re2o/index.html @@ -1,4 +1,4 @@ -{% extends "re2o/sidebar.html" %} +{% extends 're2o/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 @@ -42,7 +42,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% 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" %}

+

{% trans "Sign up" %}

@@ -53,8 +53,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% 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" %}

+

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

+

{% trans "Log in" %}

@@ -66,7 +66,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,

{% trans "My profile" %}

-

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

+

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

{% trans "Access my profile" %}

diff --git a/re2o/templates/re2o/sidebar.html b/re2o/templates/re2o/sidebar.html index c9202d14..e7e58599 100644 --- a/re2o/templates/re2o/sidebar.html +++ b/re2o/templates/re2o/sidebar.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.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 diff --git a/re2o/templatetags/design.py b/re2o/templatetags/design.py index c64e9b40..9df37c30 100644 --- a/re2o/templatetags/design.py +++ b/re2o/templatetags/design.py @@ -31,9 +31,9 @@ def tick(valeur, autoescape=False): if isinstance(valeur,bool): if valeur == True: - result = '' + result = '' else: - result = '' + result = '' return mark_safe(result) else: # if the value is not a boolean, display it as if tick was not called diff --git a/re2o/utils.py b/re2o/utils.py index 6f7870f0..20218a81 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -38,55 +38,11 @@ from __future__ import unicode_literals from django.utils import timezone from django.db.models import Q -from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger from cotisations.models import Cotisation, Facture, Vente from machines.models import Interface, Machine from users.models import Adherent, User, Ban, Whitelist - -# Mapping of srtftime format for better understanding -# https://docs.python.org/3.6/library/datetime.html#strftime-strptime-behavior -datetime_mapping={ - '%a': '%a', - '%A': '%A', - '%w': '%w', - '%d': 'dd', - '%b': '%b', - '%B': '%B', - '%m': 'mm', - '%y': 'yy', - '%Y': 'yyyy', - '%H': 'HH', - '%I': 'HH(12h)', - '%p': 'AMPM', - '%M': 'MM', - '%S': 'SS', - '%f': 'µµ', - '%z': 'UTC(+/-HHMM)', - '%Z': 'UTC(TZ)', - '%j': '%j', - '%U': 'ww', - '%W': 'ww', - '%c': '%c', - '%x': '%x', - '%X': '%X', - '%%': '%%', -} - - -def convert_datetime_format(format): - i=0 - new_format = "" - while i < len(format): - if format[i] == '%': - char = format[i:i+2] - new_format += datetime_mapping.get(char, char) - i += 2 - else: - new_format += format[i] - i += 1 - return new_format - +from preferences.models import AssoOption def all_adherent(search_time=None): """ Fonction renvoyant tous les users adherents. Optimisee pour n'est @@ -103,7 +59,7 @@ def all_adherent(search_time=None): vente__in=Vente.objects.filter( facture__in=Facture.objects.all().exclude(valid=False) ) - ).filter(date_end__gt=search_time) + ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time)) ) ) ).distinct() @@ -115,7 +71,7 @@ def all_baned(search_time=None): search_time = timezone.now() return User.objects.filter( ban__in=Ban.objects.filter( - date_end__gt=search_time + Q(date_start__lt=search_time) & Q(date_end__gt=search_time) ) ).distinct() @@ -126,20 +82,23 @@ def all_whitelisted(search_time=None): search_time = timezone.now() return User.objects.filter( whitelist__in=Whitelist.objects.filter( - date_end__gt=search_time + Q(date_start__lt=search_time) & Q(date_end__gt=search_time) ) ).distinct() def all_has_access(search_time=None): - """ Renvoie tous les users beneficiant d'une connexion - : user adherent ou whiteliste et non banni """ + """ Return all connected users : active users and whitelisted + + asso_user defined in AssoOption pannel + ---- + Renvoie tous les users beneficiant d'une connexion + : user adherent et whiteliste non banni plus l'utilisateur asso""" if search_time is None: search_time = timezone.now() - return User.objects.filter( + filter_user = ( Q(state=User.STATE_ACTIVE) & - ~Q(ban__in=Ban.objects.filter(date_end__gt=search_time)) & - (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)) | + ~Q(ban__in=Ban.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) & + (Q(whitelist__in=Whitelist.objects.filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time))) | Q(facture__in=Facture.objects.filter( vente__in=Vente.objects.filter( cotisation__in=Cotisation.objects.filter( @@ -148,10 +107,14 @@ def all_has_access(search_time=None): facture__in=Facture.objects.all() .exclude(valid=False) ) - ).filter(date_end__gt=search_time) + ).filter(Q(date_start__lt=search_time) & Q(date_end__gt=search_time)) ) ))) - ).distinct() + ) + asso_user = AssoOption.get_cached_value('utilisateur_asso') + if asso_user: + filter_user |= Q(id=asso_user.id) + return User.objects.filter(filter_user).distinct() def filter_active_interfaces(interface_set): @@ -203,164 +166,6 @@ def all_active_assigned_interfaces_count(): return all_active_interfaces_count().filter(ipv4__isnull=False) -class SortTable: - """ Class gathering uselful stuff to sort the colums of a table, according - to the column and order requested. It's used with a dict of possible - values and associated model_fields """ - - # All the possible possible values - # The naming convention is based on the URL or the views function - # The syntax to describe the sort to apply is a dict where the keys are - # the url value and the values are a list of model field name to use to - # order the request. They are applied in the order they are given. - # A 'default' might be provided to specify what to do if the requested col - # doesn't match any keys. - - USERS_INDEX = { - 'user_name': ['name'], - 'user_surname': ['surname'], - 'user_pseudo': ['pseudo'], - 'user_room': ['room'], - 'default': ['state', 'pseudo'] - } - USERS_INDEX_BAN = { - 'ban_user': ['user__pseudo'], - 'ban_start': ['date_start'], - 'ban_end': ['date_end'], - 'default': ['-date_end'] - } - USERS_INDEX_WHITE = { - 'white_user': ['user__pseudo'], - 'white_start': ['date_start'], - 'white_end': ['date_end'], - 'default': ['-date_end'] - } - USERS_INDEX_SCHOOL = { - 'school_name': ['name'], - 'default': ['name'] - } - MACHINES_INDEX = { - 'machine_name': ['name'], - 'default': ['pk'] - } - COTISATIONS_INDEX = { - 'cotis_user': ['user__pseudo'], - 'cotis_paiement': ['paiement__moyen'], - 'cotis_date': ['date'], - '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'], - 'control_paiement': ['paiement'], - 'control_date': ['date'], - 'control_valid': ['valid'], - 'control_control': ['control'], - 'control_id': ['id'], - 'control_user-id': ['user__id'], - 'default': ['-date'] - } - TOPOLOGIE_INDEX = { - 'switch_dns': ['interface__domain__name'], - 'switch_ip': ['interface__ipv4__ipv4'], - 'switch_loc': ['switchbay__name'], - 'switch_ports': ['number'], - 'switch_stack': ['stack__name'], - 'default': ['switchbay', 'stack', 'stack_member_id'] - } - TOPOLOGIE_INDEX_PORT = { - 'port_port': ['port'], - 'port_room': ['room__name'], - 'port_interface': ['machine_interface__domain__name'], - 'port_related': ['related__switch__name'], - 'port_radius': ['radius'], - 'port_vlan': ['vlan_force__name'], - 'default': ['port'] - } - TOPOLOGIE_INDEX_ROOM = { - 'room_name': ['name'], - 'default': ['name'] - } - TOPOLOGIE_INDEX_BUILDING = { - 'building_name': ['name'], - 'default': ['name'] - } - TOPOLOGIE_INDEX_BORNE = { - 'ap_name': ['interface__domain__name'], - 'ap_ip': ['interface__ipv4__ipv4'], - 'ap_mac': ['interface__mac_address'], - 'default': ['interface__domain__name'] - } - TOPOLOGIE_INDEX_STACK = { - 'stack_name': ['name'], - 'stack_id': ['stack_id'], - 'default': ['stack_id'], - } - TOPOLOGIE_INDEX_MODEL_SWITCH = { - 'model-switch_name': ['reference'], - 'model-switch_contructor': ['constructor__name'], - 'default': ['reference'], - } - TOPOLOGIE_INDEX_SWITCH_BAY = { - 'switch-bay_name': ['name'], - 'switch-bay_building': ['building__name'], - 'default': ['name'], - } - TOPOLOGIE_INDEX_CONSTRUCTOR_SWITCH = { - 'constructor-switch_name': ['name'], - 'default': ['name'], - } - LOGS_INDEX = { - 'sum_date': ['revision__date_created'], - 'default': ['-revision__date_created'], - } - LOGS_STATS_LOGS = { - 'logs_author': ['user__name'], - 'logs_date': ['date_created'], - 'default': ['-date_created'] - } - - @staticmethod - def sort(request, col, order, values): - """ Check if the given values are possible and add .order_by() and - a .reverse() as specified according to those values """ - fields = values.get(col, None) - if not fields: - fields = values.get('default', []) - request = request.order_by(*fields) - if values.get(col, None) and order == 'desc': - return request.reverse() - else: - return request - - -def re2o_paginator(request, query_set, pagination_number): - """Paginator script for list display in re2o. - :request: - :query_set: Query_set to paginate - :pagination_number: Number of entries to display""" - paginator = Paginator(query_set, pagination_number) - page = request.GET.get('page') - try: - results = paginator.page(page) - except PageNotAnInteger: - # If page is not an integer, deliver first page. - results = paginator.page(1) - except EmptyPage: - # If page is out of range (e.g. 9999), deliver last page of results. - results = paginator.page(paginator.num_pages) - return results - - def remove_user_room(room): """ Déménage de force l'ancien locataire de la chambre """ try: @@ -370,18 +175,3 @@ def remove_user_room(room): user.room = None user.save() - -def get_input_formats_help_text(input_formats): - """Returns a help text about the possible input formats""" - if len(input_formats) > 1: - help_text_template="Format: {main} {more}" - else: - help_text_template="Format: {main}" - more_text_template="" - help_text = help_text_template.format( - main=convert_datetime_format(input_formats[0]), - more=more_text_template.format( - '\n'.join(map(convert_datetime_format, input_formats)) - ) - ) - return help_text diff --git a/re2o/views.py b/re2o/views.py index 15becb35..0ff2774b 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -84,7 +84,7 @@ def about_page(request): git_info_commit = last_commit.hexsha git_info_commit_date = last_commit.committed_datetime except: - NO_GIT_MSG = _("Unable to get the information") + NO_GIT_MSG = _("Unable to get the information.") git_info_remote = NO_GIT_MSG git_info_branch = NO_GIT_MSG git_info_commit = NO_GIT_MSG diff --git a/search/forms.py b/search/forms.py index 6065e799..a32c8abc 100644 --- a/search/forms.py +++ b/search/forms.py @@ -27,12 +27,13 @@ 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 +from re2o.base import get_input_formats_help_text CHOICES_USER = ( ('0', _("Active")), ('1', _("Disabled")), ('2', _("Archived")), + ('3', _("Not yet active")), ) CHOICES_AFF = ( diff --git a/search/locale/fr/LC_MESSAGES/django.mo b/search/locale/fr/LC_MESSAGES/django.mo deleted file mode 100644 index 94a44104..00000000 Binary files a/search/locale/fr/LC_MESSAGES/django.mo and /dev/null differ diff --git a/search/locale/fr/LC_MESSAGES/django.po b/search/locale/fr/LC_MESSAGES/django.po index dd0b63a3..be2a79cc 100644 --- a/search/locale/fr/LC_MESSAGES/django.po +++ b/search/locale/fr/LC_MESSAGES/django.po @@ -21,7 +21,7 @@ msgid "" msgstr "" "Project-Id-Version: 2.5\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2018-08-15 18:15+0200\n" +"POT-Creation-Date: 2019-01-08 23:56+0100\n" "PO-Revision-Date: 2018-06-24 20:10+0200\n" "Last-Translator: Laouen Fernet \n" "Language-Team: \n" @@ -42,44 +42,48 @@ msgstr "Désactivés" msgid "Archived" msgstr "Archivés" -#: forms.py:39 +#: forms.py:36 +msgid "Not yet active" +msgstr "Pas encore adhéré" + +#: forms.py:40 msgid "Users" msgstr "Utilisateurs" -#: forms.py:40 +#: forms.py:41 msgid "Machines" msgstr "Machines" -#: forms.py:41 +#: forms.py:42 msgid "Invoices" msgstr "Factures" -#: forms.py:42 +#: forms.py:43 msgid "Bans" msgstr "Bannissements" -#: forms.py:43 +#: forms.py:44 msgid "Whitelists" msgstr "Accès gracieux" -#: forms.py:44 +#: forms.py:45 msgid "Rooms" msgstr "Chambres" -#: forms.py:45 +#: forms.py:46 msgid "Ports" msgstr "Ports" -#: forms.py:46 +#: forms.py:47 msgid "Switches" msgstr "Commutateurs réseau" -#: forms.py:59 forms.py:71 templates/search/search.html:29 +#: forms.py:60 forms.py:72 templates/search/search.html:29 #: templates/search/search.html:48 msgid "Search" msgstr "Rechercher" -#: forms.py:61 forms.py:73 +#: forms.py:62 forms.py:74 msgid "" "Use « » and «,» to specify distinct words, «\"query\"» for an exact search " "and «\\» to escape a character." @@ -87,19 +91,19 @@ msgstr "" "Utilisez « » et «,» pour spécifier différents mots, «\"query\"» pour une " "recherche exacte et «\\» pour échapper un caractère." -#: forms.py:80 +#: forms.py:81 msgid "Users filter" msgstr "Filtre utilisateurs" -#: forms.py:87 +#: forms.py:88 msgid "Display filter" msgstr "Filtre affichage" -#: forms.py:95 +#: forms.py:96 msgid "Start date" msgstr "Date de début" -#: forms.py:99 +#: forms.py:100 msgid "End date" msgstr "Date de fin" @@ -136,11 +140,11 @@ msgid "Results among rooms:" msgstr "Résultats parmi les chambres :" #: templates/search/index.html:61 -msgid "Results among ports" +msgid "Results among ports:" msgstr "Résultats parmi les ports :" #: templates/search/index.html:65 -msgid "Results among switches" +msgid "Results among switches:" msgstr "Résultats parmi les commutateurs réseau :" #: templates/search/index.html:69 diff --git a/search/templates/search/index.html b/search/templates/search/index.html index 23b57cce..4d3e2942 100644 --- a/search/templates/search/index.html +++ b/search/templates/search/index.html @@ -1,4 +1,4 @@ -{% extends "search/sidebar.html" %} +{% extends 'search/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 @@ -31,39 +31,39 @@ with this program; if not, write to the Free Software Foundation, Inc., {% block content %} {% if users %}

{% trans "Results among users:" %}

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

{% trans "Results among clubs:" %}

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

{% trans "Results among machines:" %}

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

{% trans "Results among invoices:" %}

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

{% trans "Results among whitelists:" %}

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

{% trans "Results among bans:" %}

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

{% trans "Results among rooms:" %}

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

{% trans "Results among ports" %}

- {% include "topologie/aff_port.html" with port_list=ports %} +

{% trans "Results among ports:" %}

+ {% include 'topologie/aff_port.html' with port_list=ports search=True %} {% endif %} {% if switches %} -

{% trans "Results among switches" %}

- {% include "topologie/aff_switch.html" with switch_list=switches %} +

{% 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 %}

{% trans "No result" %}

diff --git a/search/templates/search/search.html b/search/templates/search/search.html index 42012339..d957a4cf 100644 --- a/search/templates/search/search.html +++ b/search/templates/search/search.html @@ -1,4 +1,4 @@ -{% extends "search/sidebar.html" %} +{% extends 'search/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 @@ -34,10 +34,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% bootstrap_field search_form.q %} {% if search_form.u %} - {% include "buttons/multiple_checkbox_alt.html" with field=search_form.u %} + {% include 'buttons/multiple_checkbox_alt.html' with field=search_form.u %} {% endif %} {% if search_form.a %} - {% include "buttons/multiple_checkbox_alt.html" with field=search_form.a %} + {% include 'buttons/multiple_checkbox_alt.html' with field=search_form.a %} {% endif %} {% if search_form.s %} {% bootstrap_field search_form.s %} diff --git a/search/templates/search/sidebar.html b/search/templates/search/sidebar.html index a445ef41..28d7a115 100644 --- a/search/templates/search/sidebar.html +++ b/search/templates/search/sidebar.html @@ -1,4 +1,4 @@ -{% extends "base.html" %} +{% extends 'base.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 @@ -26,11 +26,11 @@ with this program; if not, write to the Free Software Foundation, Inc., {% load i18n %} {% block sidebar %} - + {% trans "Simple search" %} - + {% trans "Advanced search" %} diff --git a/search/views.py b/search/views.py index 93aa1d0e..eb0027ec 100644 --- a/search/views.py +++ b/search/views.py @@ -27,6 +27,8 @@ Gplv2""" from __future__ import unicode_literals +from netaddr import EUI, AddrFormatError + from django.shortcuts import render from django.contrib.auth.decorators import login_required @@ -44,7 +46,7 @@ from search.forms import ( CHOICES_AFF, initial_choices ) -from re2o.utils import SortTable +from re2o.base import SortTable from re2o.acl import can_view_all @@ -140,7 +142,9 @@ def search_single_word(word, filters, user, ) | Q( room__name__icontains=word ) | Q( - room__name__icontains=word + email__icontains=word + ) | Q( + telephone__icontains=word ) ) & Q(state__in=user_state) if not User.can_view_all(user)[0]: @@ -169,6 +173,11 @@ def search_single_word(word, filters, user, ) | Q( interface__ipv4__ipv4__icontains=word ) + try: + _mac_addr = EUI(word, 48) + filter_machines |= Q(interface__mac_address=word) + except AddrFormatError: + pass if not Machine.can_view_all(user)[0]: filter_machines &= Q(user__id=user.id) filters['machines'] |= filter_machines diff --git a/static/css/base.css b/static/css/base.css index 5748f538..a13c596f 100644 --- a/static/css/base.css +++ b/static/css/base.css @@ -1,38 +1,22 @@ -/* Sticky footer hacks */ -html, body { - height: 100%; -} - -#wrap { - min-height: 100%; -} - -#main { - overflow: auto; - padding-bottom:60px; /* this needs to be bigger than footer height*/ -} - +/* Footer */ footer { - position: relative; - margin-top: -50px; /* negative value of footer height */ - height: 50px; - clear:both; - padding-top:20px; - background-color: #222222; - /*background: -webkit-linear-gradient(left, red, red 16.6%, orange 16.6%, orange, orange 33.3%, yellow 33.3%, yellow, yellow 50%, green 50%, green, green 66.6%, blue 66.6%, blue, blue 83.3%, violet 83.3%,violet); */ - color: white; - padding: 15px; + padding-top: 3rem; + padding-bottom: 3rem; } -footer a { - color: white; - text-decoration: underline; +footer p { + margin-bottom: .25rem; } -/* Remove the navbar's default margin-bottom and rounded borders */ -.navbar { - margin-bottom: 0; - border-radius: 0; +/* Move the space between navbar and content in the content */ +.navbar { margin-bottom: 0; } +.pt4 { padding-top: 1.5rem!important; } + +/* Reserve space for icons and align */ +a > i.fa { + display: inline-block; + width: 16px; + text-align: center; } /* Reduce the padding for the logo in the navbar-brand so the 32px-high logo @@ -47,6 +31,19 @@ footer a { display: initial; } +/* Make navbar look less Bootstraped */ +.navbar-inverse { + border-color: #f9a01b; +} + +.navbar-inverse .navbar-brand { + color: #ffffff; +} + +.navbar-inverse .navbar-nav > li > a { + color: #d6d6d6; +} + /* Add right colors for buttons in dropdown in navbar-inverse (else it is light * gray on white bg and white when hovered */ .navbar-inverse .dropdown-menu .btn-link { @@ -67,34 +64,21 @@ footer a { } } -/* Set height of the grid so .sidenav can be 100% (adjust as needed) */ +/* Set height of the grid so footer stay on footer */ .row.content { - height: 100%; - overflow: auto; + min-height: 70vh; } -/* Set gray background color and 100% height */ +/* Set gray background color */ .sidenav { - padding-top: 20px; background-color: #f1f1f1; + border: 1px solid #e0e0e0; + border-radius: 0 0 5px 5px; } .table > tbody > tr > td, .table > tbody > tr > th, .table > tfoot > tr > td, .table > tfoot > tr > th, .table > thead > tr > td, .table > thead > tr > th { vertical-align: middle; } -/* Pull sidebars to the bottom */ -@media (min-width: 767px) { - .row { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - } - .row > [class*='col-'] { - flex-direction: column; - } -} - /* On small screens, set height to 'auto' for sidenav and grid */ @media screen and (max-width: 767px) { .sidenav { @@ -105,7 +89,7 @@ footer a { } .table-responsive { - overflow-y: visible; + overflow: visible; } /* Make modal wider on wide screens */ @@ -113,6 +97,7 @@ footer a { .modal-dialog { width: 1000px } +} /* For tables with long text in cells */ @@ -147,3 +132,14 @@ th.long_text{ .dashboard{ text-align: center; } + +/* Detailed information on profile page */ +dl.profile-info { + margin-top: -16px; + margin-bottom: 0; +} + +dl.profile-info > div { + padding: 8px; + border-top: 1px solid #ddd; +} diff --git a/static/images/rj45.gif b/static/images/rj45.gif new file mode 100644 index 00000000..69630b10 Binary files /dev/null and b/static/images/rj45.gif differ diff --git a/static/js/collapse-from-url.js b/static/js/collapse-from-url.js new file mode 100644 index 00000000..6c85762b --- /dev/null +++ b/static/js/collapse-from-url.js @@ -0,0 +1,33 @@ +// 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 Alexandre Iooss +// +// 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. + +// This script makes URL hash controls Bootstrap collapse +// e.g. if there is #information in the URL +// then the collapse with id "information" will be open. + +$(document).ready(function () { + if(location.hash != null && location.hash !== ""){ + // Open the collapse corresponding to URL hash + $(location.hash + '.collapse').collapse('show'); + } else { + // Open default collapse + $('.collapse-default.collapse').collapse('show'); + } +}); diff --git a/static/js/handlebars/LICENSE b/static/js/handlebars/LICENSE deleted file mode 100644 index b802d14e..00000000 --- a/static/js/handlebars/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2011-2017 by Yehuda Katz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/static/js/handlebars/handlebars.js b/static/js/handlebars/handlebars.js deleted file mode 100644 index 5b566151..00000000 --- a/static/js/handlebars/handlebars.js +++ /dev/null @@ -1,4840 +0,0 @@ -/**! - - @license - handlebars v4.0.10 - -Copyright (C) 2011-2016 by Yehuda Katz - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -*/ -(function webpackUniversalModuleDefinition(root, factory) { - if(typeof exports === 'object' && typeof module === 'object') - module.exports = factory(); - else if(typeof define === 'function' && define.amd) - define([], factory); - else if(typeof exports === 'object') - exports["Handlebars"] = factory(); - else - root["Handlebars"] = factory(); -})(this, function() { -return /******/ (function(modules) { // webpackBootstrap -/******/ // The module cache -/******/ var installedModules = {}; - -/******/ // The require function -/******/ function __webpack_require__(moduleId) { - -/******/ // Check if module is in cache -/******/ if(installedModules[moduleId]) -/******/ return installedModules[moduleId].exports; - -/******/ // Create a new module (and put it into the cache) -/******/ var module = installedModules[moduleId] = { -/******/ exports: {}, -/******/ id: moduleId, -/******/ loaded: false -/******/ }; - -/******/ // Execute the module function -/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); - -/******/ // Flag the module as loaded -/******/ module.loaded = true; - -/******/ // Return the exports of the module -/******/ return module.exports; -/******/ } - - -/******/ // expose the modules object (__webpack_modules__) -/******/ __webpack_require__.m = modules; - -/******/ // expose the module cache -/******/ __webpack_require__.c = installedModules; - -/******/ // __webpack_public_path__ -/******/ __webpack_require__.p = ""; - -/******/ // Load entry module and return exports -/******/ return __webpack_require__(0); -/******/ }) -/************************************************************************/ -/******/ ([ -/* 0 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - - var _handlebarsRuntime = __webpack_require__(2); - - var _handlebarsRuntime2 = _interopRequireDefault(_handlebarsRuntime); - - // Compiler imports - - var _handlebarsCompilerAst = __webpack_require__(35); - - var _handlebarsCompilerAst2 = _interopRequireDefault(_handlebarsCompilerAst); - - var _handlebarsCompilerBase = __webpack_require__(36); - - var _handlebarsCompilerCompiler = __webpack_require__(41); - - var _handlebarsCompilerJavascriptCompiler = __webpack_require__(42); - - var _handlebarsCompilerJavascriptCompiler2 = _interopRequireDefault(_handlebarsCompilerJavascriptCompiler); - - var _handlebarsCompilerVisitor = __webpack_require__(39); - - var _handlebarsCompilerVisitor2 = _interopRequireDefault(_handlebarsCompilerVisitor); - - var _handlebarsNoConflict = __webpack_require__(34); - - var _handlebarsNoConflict2 = _interopRequireDefault(_handlebarsNoConflict); - - var _create = _handlebarsRuntime2['default'].create; - function create() { - var hb = _create(); - - hb.compile = function (input, options) { - return _handlebarsCompilerCompiler.compile(input, options, hb); - }; - hb.precompile = function (input, options) { - return _handlebarsCompilerCompiler.precompile(input, options, hb); - }; - - hb.AST = _handlebarsCompilerAst2['default']; - hb.Compiler = _handlebarsCompilerCompiler.Compiler; - hb.JavaScriptCompiler = _handlebarsCompilerJavascriptCompiler2['default']; - hb.Parser = _handlebarsCompilerBase.parser; - hb.parse = _handlebarsCompilerBase.parse; - - return hb; - } - - var inst = create(); - inst.create = create; - - _handlebarsNoConflict2['default'](inst); - - inst.Visitor = _handlebarsCompilerVisitor2['default']; - - inst['default'] = inst; - - exports['default'] = inst; - module.exports = exports['default']; - -/***/ }), -/* 1 */ -/***/ (function(module, exports) { - - "use strict"; - - exports["default"] = function (obj) { - return obj && obj.__esModule ? obj : { - "default": obj - }; - }; - - exports.__esModule = true; - -/***/ }), -/* 2 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireWildcard = __webpack_require__(3)['default']; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - - var _handlebarsBase = __webpack_require__(4); - - var base = _interopRequireWildcard(_handlebarsBase); - - // Each of these augment the Handlebars object. No need to setup here. - // (This is done to easily share code between commonjs and browse envs) - - var _handlebarsSafeString = __webpack_require__(21); - - var _handlebarsSafeString2 = _interopRequireDefault(_handlebarsSafeString); - - var _handlebarsException = __webpack_require__(6); - - var _handlebarsException2 = _interopRequireDefault(_handlebarsException); - - var _handlebarsUtils = __webpack_require__(5); - - var Utils = _interopRequireWildcard(_handlebarsUtils); - - var _handlebarsRuntime = __webpack_require__(22); - - var runtime = _interopRequireWildcard(_handlebarsRuntime); - - var _handlebarsNoConflict = __webpack_require__(34); - - var _handlebarsNoConflict2 = _interopRequireDefault(_handlebarsNoConflict); - - // For compatibility and usage outside of module systems, make the Handlebars object a namespace - function create() { - var hb = new base.HandlebarsEnvironment(); - - Utils.extend(hb, base); - hb.SafeString = _handlebarsSafeString2['default']; - hb.Exception = _handlebarsException2['default']; - hb.Utils = Utils; - hb.escapeExpression = Utils.escapeExpression; - - hb.VM = runtime; - hb.template = function (spec) { - return runtime.template(spec, hb); - }; - - return hb; - } - - var inst = create(); - inst.create = create; - - _handlebarsNoConflict2['default'](inst); - - inst['default'] = inst; - - exports['default'] = inst; - module.exports = exports['default']; - -/***/ }), -/* 3 */ -/***/ (function(module, exports) { - - "use strict"; - - exports["default"] = function (obj) { - if (obj && obj.__esModule) { - return obj; - } else { - var newObj = {}; - - if (obj != null) { - for (var key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; - } - } - - newObj["default"] = obj; - return newObj; - } - }; - - exports.__esModule = true; - -/***/ }), -/* 4 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - exports.HandlebarsEnvironment = HandlebarsEnvironment; - - var _utils = __webpack_require__(5); - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - var _helpers = __webpack_require__(10); - - var _decorators = __webpack_require__(18); - - var _logger = __webpack_require__(20); - - var _logger2 = _interopRequireDefault(_logger); - - var VERSION = '4.0.10'; - exports.VERSION = VERSION; - var COMPILER_REVISION = 7; - - exports.COMPILER_REVISION = COMPILER_REVISION; - var REVISION_CHANGES = { - 1: '<= 1.0.rc.2', // 1.0.rc.2 is actually rev2 but doesn't report it - 2: '== 1.0.0-rc.3', - 3: '== 1.0.0-rc.4', - 4: '== 1.x.x', - 5: '== 2.0.0-alpha.x', - 6: '>= 2.0.0-beta.1', - 7: '>= 4.0.0' - }; - - exports.REVISION_CHANGES = REVISION_CHANGES; - var objectType = '[object Object]'; - - function HandlebarsEnvironment(helpers, partials, decorators) { - this.helpers = helpers || {}; - this.partials = partials || {}; - this.decorators = decorators || {}; - - _helpers.registerDefaultHelpers(this); - _decorators.registerDefaultDecorators(this); - } - - HandlebarsEnvironment.prototype = { - constructor: HandlebarsEnvironment, - - logger: _logger2['default'], - log: _logger2['default'].log, - - registerHelper: function registerHelper(name, fn) { - if (_utils.toString.call(name) === objectType) { - if (fn) { - throw new _exception2['default']('Arg not supported with multiple helpers'); - } - _utils.extend(this.helpers, name); - } else { - this.helpers[name] = fn; - } - }, - unregisterHelper: function unregisterHelper(name) { - delete this.helpers[name]; - }, - - registerPartial: function registerPartial(name, partial) { - if (_utils.toString.call(name) === objectType) { - _utils.extend(this.partials, name); - } else { - if (typeof partial === 'undefined') { - throw new _exception2['default']('Attempting to register a partial called "' + name + '" as undefined'); - } - this.partials[name] = partial; - } - }, - unregisterPartial: function unregisterPartial(name) { - delete this.partials[name]; - }, - - registerDecorator: function registerDecorator(name, fn) { - if (_utils.toString.call(name) === objectType) { - if (fn) { - throw new _exception2['default']('Arg not supported with multiple decorators'); - } - _utils.extend(this.decorators, name); - } else { - this.decorators[name] = fn; - } - }, - unregisterDecorator: function unregisterDecorator(name) { - delete this.decorators[name]; - } - }; - - var log = _logger2['default'].log; - - exports.log = log; - exports.createFrame = _utils.createFrame; - exports.logger = _logger2['default']; - -/***/ }), -/* 5 */ -/***/ (function(module, exports) { - - 'use strict'; - - exports.__esModule = true; - exports.extend = extend; - exports.indexOf = indexOf; - exports.escapeExpression = escapeExpression; - exports.isEmpty = isEmpty; - exports.createFrame = createFrame; - exports.blockParams = blockParams; - exports.appendContextPath = appendContextPath; - var escape = { - '&': '&', - '<': '<', - '>': '>', - '"': '"', - "'": ''', - '`': '`', - '=': '=' - }; - - var badChars = /[&<>"'`=]/g, - possible = /[&<>"'`=]/; - - function escapeChar(chr) { - return escape[chr]; - } - - function extend(obj /* , ...source */) { - for (var i = 1; i < arguments.length; i++) { - for (var key in arguments[i]) { - if (Object.prototype.hasOwnProperty.call(arguments[i], key)) { - obj[key] = arguments[i][key]; - } - } - } - - return obj; - } - - var toString = Object.prototype.toString; - - exports.toString = toString; - // Sourced from lodash - // https://github.com/bestiejs/lodash/blob/master/LICENSE.txt - /* eslint-disable func-style */ - var isFunction = function isFunction(value) { - return typeof value === 'function'; - }; - // fallback for older versions of Chrome and Safari - /* istanbul ignore next */ - if (isFunction(/x/)) { - exports.isFunction = isFunction = function (value) { - return typeof value === 'function' && toString.call(value) === '[object Function]'; - }; - } - exports.isFunction = isFunction; - - /* eslint-enable func-style */ - - /* istanbul ignore next */ - var isArray = Array.isArray || function (value) { - return value && typeof value === 'object' ? toString.call(value) === '[object Array]' : false; - }; - - exports.isArray = isArray; - // Older IE versions do not directly support indexOf so we must implement our own, sadly. - - function indexOf(array, value) { - for (var i = 0, len = array.length; i < len; i++) { - if (array[i] === value) { - return i; - } - } - return -1; - } - - function escapeExpression(string) { - if (typeof string !== 'string') { - // don't escape SafeStrings, since they're already safe - if (string && string.toHTML) { - return string.toHTML(); - } else if (string == null) { - return ''; - } else if (!string) { - return string + ''; - } - - // Force a string conversion as this will be done by the append regardless and - // the regex test will do this transparently behind the scenes, causing issues if - // an object's to string has escaped characters in it. - string = '' + string; - } - - if (!possible.test(string)) { - return string; - } - return string.replace(badChars, escapeChar); - } - - function isEmpty(value) { - if (!value && value !== 0) { - return true; - } else if (isArray(value) && value.length === 0) { - return true; - } else { - return false; - } - } - - function createFrame(object) { - var frame = extend({}, object); - frame._parent = object; - return frame; - } - - function blockParams(params, ids) { - params.path = ids; - return params; - } - - function appendContextPath(contextPath, id) { - return (contextPath ? contextPath + '.' : '') + id; - } - -/***/ }), -/* 6 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _Object$defineProperty = __webpack_require__(7)['default']; - - exports.__esModule = true; - - var errorProps = ['description', 'fileName', 'lineNumber', 'message', 'name', 'number', 'stack']; - - function Exception(message, node) { - var loc = node && node.loc, - line = undefined, - column = undefined; - if (loc) { - line = loc.start.line; - column = loc.start.column; - - message += ' - ' + line + ':' + column; - } - - var tmp = Error.prototype.constructor.call(this, message); - - // Unfortunately errors are not enumerable in Chrome (at least), so `for prop in tmp` doesn't work. - for (var idx = 0; idx < errorProps.length; idx++) { - this[errorProps[idx]] = tmp[errorProps[idx]]; - } - - /* istanbul ignore else */ - if (Error.captureStackTrace) { - Error.captureStackTrace(this, Exception); - } - - try { - if (loc) { - this.lineNumber = line; - - // Work around issue under safari where we can't directly set the column value - /* istanbul ignore next */ - if (_Object$defineProperty) { - Object.defineProperty(this, 'column', { - value: column, - enumerable: true - }); - } else { - this.column = column; - } - } - } catch (nop) { - /* Ignore if the browser is very particular */ - } - } - - Exception.prototype = new Error(); - - exports['default'] = Exception; - module.exports = exports['default']; - -/***/ }), -/* 7 */ -/***/ (function(module, exports, __webpack_require__) { - - module.exports = { "default": __webpack_require__(8), __esModule: true }; - -/***/ }), -/* 8 */ -/***/ (function(module, exports, __webpack_require__) { - - var $ = __webpack_require__(9); - module.exports = function defineProperty(it, key, desc){ - return $.setDesc(it, key, desc); - }; - -/***/ }), -/* 9 */ -/***/ (function(module, exports) { - - var $Object = Object; - module.exports = { - create: $Object.create, - getProto: $Object.getPrototypeOf, - isEnum: {}.propertyIsEnumerable, - getDesc: $Object.getOwnPropertyDescriptor, - setDesc: $Object.defineProperty, - setDescs: $Object.defineProperties, - getKeys: $Object.keys, - getNames: $Object.getOwnPropertyNames, - getSymbols: $Object.getOwnPropertySymbols, - each: [].forEach - }; - -/***/ }), -/* 10 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - exports.registerDefaultHelpers = registerDefaultHelpers; - - var _helpersBlockHelperMissing = __webpack_require__(11); - - var _helpersBlockHelperMissing2 = _interopRequireDefault(_helpersBlockHelperMissing); - - var _helpersEach = __webpack_require__(12); - - var _helpersEach2 = _interopRequireDefault(_helpersEach); - - var _helpersHelperMissing = __webpack_require__(13); - - var _helpersHelperMissing2 = _interopRequireDefault(_helpersHelperMissing); - - var _helpersIf = __webpack_require__(14); - - var _helpersIf2 = _interopRequireDefault(_helpersIf); - - var _helpersLog = __webpack_require__(15); - - var _helpersLog2 = _interopRequireDefault(_helpersLog); - - var _helpersLookup = __webpack_require__(16); - - var _helpersLookup2 = _interopRequireDefault(_helpersLookup); - - var _helpersWith = __webpack_require__(17); - - var _helpersWith2 = _interopRequireDefault(_helpersWith); - - function registerDefaultHelpers(instance) { - _helpersBlockHelperMissing2['default'](instance); - _helpersEach2['default'](instance); - _helpersHelperMissing2['default'](instance); - _helpersIf2['default'](instance); - _helpersLog2['default'](instance); - _helpersLookup2['default'](instance); - _helpersWith2['default'](instance); - } - -/***/ }), -/* 11 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - exports.__esModule = true; - - var _utils = __webpack_require__(5); - - exports['default'] = function (instance) { - instance.registerHelper('blockHelperMissing', function (context, options) { - var inverse = options.inverse, - fn = options.fn; - - if (context === true) { - return fn(this); - } else if (context === false || context == null) { - return inverse(this); - } else if (_utils.isArray(context)) { - if (context.length > 0) { - if (options.ids) { - options.ids = [options.name]; - } - - return instance.helpers.each(context, options); - } else { - return inverse(this); - } - } else { - if (options.data && options.ids) { - var data = _utils.createFrame(options.data); - data.contextPath = _utils.appendContextPath(options.data.contextPath, options.name); - options = { data: data }; - } - - return fn(context, options); - } - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 12 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - - var _utils = __webpack_require__(5); - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - exports['default'] = function (instance) { - instance.registerHelper('each', function (context, options) { - if (!options) { - throw new _exception2['default']('Must pass iterator to #each'); - } - - var fn = options.fn, - inverse = options.inverse, - i = 0, - ret = '', - data = undefined, - contextPath = undefined; - - if (options.data && options.ids) { - contextPath = _utils.appendContextPath(options.data.contextPath, options.ids[0]) + '.'; - } - - if (_utils.isFunction(context)) { - context = context.call(this); - } - - if (options.data) { - data = _utils.createFrame(options.data); - } - - function execIteration(field, index, last) { - if (data) { - data.key = field; - data.index = index; - data.first = index === 0; - data.last = !!last; - - if (contextPath) { - data.contextPath = contextPath + field; - } - } - - ret = ret + fn(context[field], { - data: data, - blockParams: _utils.blockParams([context[field], field], [contextPath + field, null]) - }); - } - - if (context && typeof context === 'object') { - if (_utils.isArray(context)) { - for (var j = context.length; i < j; i++) { - if (i in context) { - execIteration(i, i, i === context.length - 1); - } - } - } else { - var priorKey = undefined; - - for (var key in context) { - if (context.hasOwnProperty(key)) { - // We're running the iterations one step out of sync so we can detect - // the last iteration without have to scan the object twice and create - // an itermediate keys array. - if (priorKey !== undefined) { - execIteration(priorKey, i - 1); - } - priorKey = key; - i++; - } - } - if (priorKey !== undefined) { - execIteration(priorKey, i - 1, true); - } - } - } - - if (i === 0) { - ret = inverse(this); - } - - return ret; - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 13 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - exports['default'] = function (instance) { - instance.registerHelper('helperMissing', function () /* [args, ]options */{ - if (arguments.length === 1) { - // A missing field in a {{foo}} construct. - return undefined; - } else { - // Someone is actually trying to call something, blow up. - throw new _exception2['default']('Missing helper: "' + arguments[arguments.length - 1].name + '"'); - } - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 14 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - exports.__esModule = true; - - var _utils = __webpack_require__(5); - - exports['default'] = function (instance) { - instance.registerHelper('if', function (conditional, options) { - if (_utils.isFunction(conditional)) { - conditional = conditional.call(this); - } - - // Default behavior is to render the positive path if the value is truthy and not empty. - // The `includeZero` option may be set to treat the condtional as purely not empty based on the - // behavior of isEmpty. Effectively this determines if 0 is handled by the positive path or negative. - if (!options.hash.includeZero && !conditional || _utils.isEmpty(conditional)) { - return options.inverse(this); - } else { - return options.fn(this); - } - }); - - instance.registerHelper('unless', function (conditional, options) { - return instance.helpers['if'].call(this, conditional, { fn: options.inverse, inverse: options.fn, hash: options.hash }); - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 15 */ -/***/ (function(module, exports) { - - 'use strict'; - - exports.__esModule = true; - - exports['default'] = function (instance) { - instance.registerHelper('log', function () /* message, options */{ - var args = [undefined], - options = arguments[arguments.length - 1]; - for (var i = 0; i < arguments.length - 1; i++) { - args.push(arguments[i]); - } - - var level = 1; - if (options.hash.level != null) { - level = options.hash.level; - } else if (options.data && options.data.level != null) { - level = options.data.level; - } - args[0] = level; - - instance.log.apply(instance, args); - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 16 */ -/***/ (function(module, exports) { - - 'use strict'; - - exports.__esModule = true; - - exports['default'] = function (instance) { - instance.registerHelper('lookup', function (obj, field) { - return obj && obj[field]; - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 17 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - exports.__esModule = true; - - var _utils = __webpack_require__(5); - - exports['default'] = function (instance) { - instance.registerHelper('with', function (context, options) { - if (_utils.isFunction(context)) { - context = context.call(this); - } - - var fn = options.fn; - - if (!_utils.isEmpty(context)) { - var data = options.data; - if (options.data && options.ids) { - data = _utils.createFrame(options.data); - data.contextPath = _utils.appendContextPath(options.data.contextPath, options.ids[0]); - } - - return fn(context, { - data: data, - blockParams: _utils.blockParams([context], [data && data.contextPath]) - }); - } else { - return options.inverse(this); - } - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 18 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - exports.registerDefaultDecorators = registerDefaultDecorators; - - var _decoratorsInline = __webpack_require__(19); - - var _decoratorsInline2 = _interopRequireDefault(_decoratorsInline); - - function registerDefaultDecorators(instance) { - _decoratorsInline2['default'](instance); - } - -/***/ }), -/* 19 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - exports.__esModule = true; - - var _utils = __webpack_require__(5); - - exports['default'] = function (instance) { - instance.registerDecorator('inline', function (fn, props, container, options) { - var ret = fn; - if (!props.partials) { - props.partials = {}; - ret = function (context, options) { - // Create a new partials stack frame prior to exec. - var original = container.partials; - container.partials = _utils.extend({}, original, props.partials); - var ret = fn(context, options); - container.partials = original; - return ret; - }; - } - - props.partials[options.args[0]] = options.fn; - - return ret; - }); - }; - - module.exports = exports['default']; - -/***/ }), -/* 20 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - exports.__esModule = true; - - var _utils = __webpack_require__(5); - - var logger = { - methodMap: ['debug', 'info', 'warn', 'error'], - level: 'info', - - // Maps a given level value to the `methodMap` indexes above. - lookupLevel: function lookupLevel(level) { - if (typeof level === 'string') { - var levelMap = _utils.indexOf(logger.methodMap, level.toLowerCase()); - if (levelMap >= 0) { - level = levelMap; - } else { - level = parseInt(level, 10); - } - } - - return level; - }, - - // Can be overridden in the host environment - log: function log(level) { - level = logger.lookupLevel(level); - - if (typeof console !== 'undefined' && logger.lookupLevel(logger.level) <= level) { - var method = logger.methodMap[level]; - if (!console[method]) { - // eslint-disable-line no-console - method = 'log'; - } - - for (var _len = arguments.length, message = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - message[_key - 1] = arguments[_key]; - } - - console[method].apply(console, message); // eslint-disable-line no-console - } - } - }; - - exports['default'] = logger; - module.exports = exports['default']; - -/***/ }), -/* 21 */ -/***/ (function(module, exports) { - - // Build out our basic SafeString type - 'use strict'; - - exports.__esModule = true; - function SafeString(string) { - this.string = string; - } - - SafeString.prototype.toString = SafeString.prototype.toHTML = function () { - return '' + this.string; - }; - - exports['default'] = SafeString; - module.exports = exports['default']; - -/***/ }), -/* 22 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _Object$seal = __webpack_require__(23)['default']; - - var _interopRequireWildcard = __webpack_require__(3)['default']; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - exports.checkRevision = checkRevision; - exports.template = template; - exports.wrapProgram = wrapProgram; - exports.resolvePartial = resolvePartial; - exports.invokePartial = invokePartial; - exports.noop = noop; - - var _utils = __webpack_require__(5); - - var Utils = _interopRequireWildcard(_utils); - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - var _base = __webpack_require__(4); - - function checkRevision(compilerInfo) { - var compilerRevision = compilerInfo && compilerInfo[0] || 1, - currentRevision = _base.COMPILER_REVISION; - - if (compilerRevision !== currentRevision) { - if (compilerRevision < currentRevision) { - var runtimeVersions = _base.REVISION_CHANGES[currentRevision], - compilerVersions = _base.REVISION_CHANGES[compilerRevision]; - throw new _exception2['default']('Template was precompiled with an older version of Handlebars than the current runtime. ' + 'Please update your precompiler to a newer version (' + runtimeVersions + ') or downgrade your runtime to an older version (' + compilerVersions + ').'); - } else { - // Use the embedded version info since the runtime doesn't know about this revision yet - throw new _exception2['default']('Template was precompiled with a newer version of Handlebars than the current runtime. ' + 'Please update your runtime to a newer version (' + compilerInfo[1] + ').'); - } - } - } - - function template(templateSpec, env) { - /* istanbul ignore next */ - if (!env) { - throw new _exception2['default']('No environment passed to template'); - } - if (!templateSpec || !templateSpec.main) { - throw new _exception2['default']('Unknown template object: ' + typeof templateSpec); - } - - templateSpec.main.decorator = templateSpec.main_d; - - // Note: Using env.VM references rather than local var references throughout this section to allow - // for external users to override these as psuedo-supported APIs. - env.VM.checkRevision(templateSpec.compiler); - - function invokePartialWrapper(partial, context, options) { - if (options.hash) { - context = Utils.extend({}, context, options.hash); - if (options.ids) { - options.ids[0] = true; - } - } - - partial = env.VM.resolvePartial.call(this, partial, context, options); - var result = env.VM.invokePartial.call(this, partial, context, options); - - if (result == null && env.compile) { - options.partials[options.name] = env.compile(partial, templateSpec.compilerOptions, env); - result = options.partials[options.name](context, options); - } - if (result != null) { - if (options.indent) { - var lines = result.split('\n'); - for (var i = 0, l = lines.length; i < l; i++) { - if (!lines[i] && i + 1 === l) { - break; - } - - lines[i] = options.indent + lines[i]; - } - result = lines.join('\n'); - } - return result; - } else { - throw new _exception2['default']('The partial ' + options.name + ' could not be compiled when running in runtime-only mode'); - } - } - - // Just add water - var container = { - strict: function strict(obj, name) { - if (!(name in obj)) { - throw new _exception2['default']('"' + name + '" not defined in ' + obj); - } - return obj[name]; - }, - lookup: function lookup(depths, name) { - var len = depths.length; - for (var i = 0; i < len; i++) { - if (depths[i] && depths[i][name] != null) { - return depths[i][name]; - } - } - }, - lambda: function lambda(current, context) { - return typeof current === 'function' ? current.call(context) : current; - }, - - escapeExpression: Utils.escapeExpression, - invokePartial: invokePartialWrapper, - - fn: function fn(i) { - var ret = templateSpec[i]; - ret.decorator = templateSpec[i + '_d']; - return ret; - }, - - programs: [], - program: function program(i, data, declaredBlockParams, blockParams, depths) { - var programWrapper = this.programs[i], - fn = this.fn(i); - if (data || depths || blockParams || declaredBlockParams) { - programWrapper = wrapProgram(this, i, fn, data, declaredBlockParams, blockParams, depths); - } else if (!programWrapper) { - programWrapper = this.programs[i] = wrapProgram(this, i, fn); - } - return programWrapper; - }, - - data: function data(value, depth) { - while (value && depth--) { - value = value._parent; - } - return value; - }, - merge: function merge(param, common) { - var obj = param || common; - - if (param && common && param !== common) { - obj = Utils.extend({}, common, param); - } - - return obj; - }, - // An empty object to use as replacement for null-contexts - nullContext: _Object$seal({}), - - noop: env.VM.noop, - compilerInfo: templateSpec.compiler - }; - - function ret(context) { - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var data = options.data; - - ret._setup(options); - if (!options.partial && templateSpec.useData) { - data = initData(context, data); - } - var depths = undefined, - blockParams = templateSpec.useBlockParams ? [] : undefined; - if (templateSpec.useDepths) { - if (options.depths) { - depths = context != options.depths[0] ? [context].concat(options.depths) : options.depths; - } else { - depths = [context]; - } - } - - function main(context /*, options*/) { - return '' + templateSpec.main(container, context, container.helpers, container.partials, data, blockParams, depths); - } - main = executeDecorators(templateSpec.main, main, container, options.depths || [], data, blockParams); - return main(context, options); - } - ret.isTop = true; - - ret._setup = function (options) { - if (!options.partial) { - container.helpers = container.merge(options.helpers, env.helpers); - - if (templateSpec.usePartial) { - container.partials = container.merge(options.partials, env.partials); - } - if (templateSpec.usePartial || templateSpec.useDecorators) { - container.decorators = container.merge(options.decorators, env.decorators); - } - } else { - container.helpers = options.helpers; - container.partials = options.partials; - container.decorators = options.decorators; - } - }; - - ret._child = function (i, data, blockParams, depths) { - if (templateSpec.useBlockParams && !blockParams) { - throw new _exception2['default']('must pass block params'); - } - if (templateSpec.useDepths && !depths) { - throw new _exception2['default']('must pass parent depths'); - } - - return wrapProgram(container, i, templateSpec[i], data, 0, blockParams, depths); - }; - return ret; - } - - function wrapProgram(container, i, fn, data, declaredBlockParams, blockParams, depths) { - function prog(context) { - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - var currentDepths = depths; - if (depths && context != depths[0] && !(context === container.nullContext && depths[0] === null)) { - currentDepths = [context].concat(depths); - } - - return fn(container, context, container.helpers, container.partials, options.data || data, blockParams && [options.blockParams].concat(blockParams), currentDepths); - } - - prog = executeDecorators(fn, prog, container, depths, data, blockParams); - - prog.program = i; - prog.depth = depths ? depths.length : 0; - prog.blockParams = declaredBlockParams || 0; - return prog; - } - - function resolvePartial(partial, context, options) { - if (!partial) { - if (options.name === '@partial-block') { - partial = options.data['partial-block']; - } else { - partial = options.partials[options.name]; - } - } else if (!partial.call && !options.name) { - // This is a dynamic partial that returned a string - options.name = partial; - partial = options.partials[partial]; - } - return partial; - } - - function invokePartial(partial, context, options) { - // Use the current closure context to save the partial-block if this partial - var currentPartialBlock = options.data && options.data['partial-block']; - options.partial = true; - if (options.ids) { - options.data.contextPath = options.ids[0] || options.data.contextPath; - } - - var partialBlock = undefined; - if (options.fn && options.fn !== noop) { - (function () { - options.data = _base.createFrame(options.data); - // Wrapper function to get access to currentPartialBlock from the closure - var fn = options.fn; - partialBlock = options.data['partial-block'] = function partialBlockWrapper(context) { - var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1]; - - // Restore the partial-block from the closure for the execution of the block - // i.e. the part inside the block of the partial call. - options.data = _base.createFrame(options.data); - options.data['partial-block'] = currentPartialBlock; - return fn(context, options); - }; - if (fn.partials) { - options.partials = Utils.extend({}, options.partials, fn.partials); - } - })(); - } - - if (partial === undefined && partialBlock) { - partial = partialBlock; - } - - if (partial === undefined) { - throw new _exception2['default']('The partial ' + options.name + ' could not be found'); - } else if (partial instanceof Function) { - return partial(context, options); - } - } - - function noop() { - return ''; - } - - function initData(context, data) { - if (!data || !('root' in data)) { - data = data ? _base.createFrame(data) : {}; - data.root = context; - } - return data; - } - - function executeDecorators(fn, prog, container, depths, data, blockParams) { - if (fn.decorator) { - var props = {}; - prog = fn.decorator(prog, props, container, depths && depths[0], data, blockParams, depths); - Utils.extend(prog, props); - } - return prog; - } - -/***/ }), -/* 23 */ -/***/ (function(module, exports, __webpack_require__) { - - module.exports = { "default": __webpack_require__(24), __esModule: true }; - -/***/ }), -/* 24 */ -/***/ (function(module, exports, __webpack_require__) { - - __webpack_require__(25); - module.exports = __webpack_require__(30).Object.seal; - -/***/ }), -/* 25 */ -/***/ (function(module, exports, __webpack_require__) { - - // 19.1.2.17 Object.seal(O) - var isObject = __webpack_require__(26); - - __webpack_require__(27)('seal', function($seal){ - return function seal(it){ - return $seal && isObject(it) ? $seal(it) : it; - }; - }); - -/***/ }), -/* 26 */ -/***/ (function(module, exports) { - - module.exports = function(it){ - return typeof it === 'object' ? it !== null : typeof it === 'function'; - }; - -/***/ }), -/* 27 */ -/***/ (function(module, exports, __webpack_require__) { - - // most Object methods by ES6 should accept primitives - var $export = __webpack_require__(28) - , core = __webpack_require__(30) - , fails = __webpack_require__(33); - module.exports = function(KEY, exec){ - var fn = (core.Object || {})[KEY] || Object[KEY] - , exp = {}; - exp[KEY] = exec(fn); - $export($export.S + $export.F * fails(function(){ fn(1); }), 'Object', exp); - }; - -/***/ }), -/* 28 */ -/***/ (function(module, exports, __webpack_require__) { - - var global = __webpack_require__(29) - , core = __webpack_require__(30) - , ctx = __webpack_require__(31) - , PROTOTYPE = 'prototype'; - - var $export = function(type, name, source){ - var IS_FORCED = type & $export.F - , IS_GLOBAL = type & $export.G - , IS_STATIC = type & $export.S - , IS_PROTO = type & $export.P - , IS_BIND = type & $export.B - , IS_WRAP = type & $export.W - , exports = IS_GLOBAL ? core : core[name] || (core[name] = {}) - , target = IS_GLOBAL ? global : IS_STATIC ? global[name] : (global[name] || {})[PROTOTYPE] - , key, own, out; - if(IS_GLOBAL)source = name; - for(key in source){ - // contains in native - own = !IS_FORCED && target && key in target; - if(own && key in exports)continue; - // export native or passed - out = own ? target[key] : source[key]; - // prevent global pollution for namespaces - exports[key] = IS_GLOBAL && typeof target[key] != 'function' ? source[key] - // bind timers to global for call from export context - : IS_BIND && own ? ctx(out, global) - // wrap global constructors for prevent change them in library - : IS_WRAP && target[key] == out ? (function(C){ - var F = function(param){ - return this instanceof C ? new C(param) : C(param); - }; - F[PROTOTYPE] = C[PROTOTYPE]; - return F; - // make static versions for prototype methods - })(out) : IS_PROTO && typeof out == 'function' ? ctx(Function.call, out) : out; - if(IS_PROTO)(exports[PROTOTYPE] || (exports[PROTOTYPE] = {}))[key] = out; - } - }; - // type bitmap - $export.F = 1; // forced - $export.G = 2; // global - $export.S = 4; // static - $export.P = 8; // proto - $export.B = 16; // bind - $export.W = 32; // wrap - module.exports = $export; - -/***/ }), -/* 29 */ -/***/ (function(module, exports) { - - // https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 - var global = module.exports = typeof window != 'undefined' && window.Math == Math - ? window : typeof self != 'undefined' && self.Math == Math ? self : Function('return this')(); - if(typeof __g == 'number')__g = global; // eslint-disable-line no-undef - -/***/ }), -/* 30 */ -/***/ (function(module, exports) { - - var core = module.exports = {version: '1.2.6'}; - if(typeof __e == 'number')__e = core; // eslint-disable-line no-undef - -/***/ }), -/* 31 */ -/***/ (function(module, exports, __webpack_require__) { - - // optional / simple context binding - var aFunction = __webpack_require__(32); - module.exports = function(fn, that, length){ - aFunction(fn); - if(that === undefined)return fn; - switch(length){ - case 1: return function(a){ - return fn.call(that, a); - }; - case 2: return function(a, b){ - return fn.call(that, a, b); - }; - case 3: return function(a, b, c){ - return fn.call(that, a, b, c); - }; - } - return function(/* ...args */){ - return fn.apply(that, arguments); - }; - }; - -/***/ }), -/* 32 */ -/***/ (function(module, exports) { - - module.exports = function(it){ - if(typeof it != 'function')throw TypeError(it + ' is not a function!'); - return it; - }; - -/***/ }), -/* 33 */ -/***/ (function(module, exports) { - - module.exports = function(exec){ - try { - return !!exec(); - } catch(e){ - return true; - } - }; - -/***/ }), -/* 34 */ -/***/ (function(module, exports) { - - /* WEBPACK VAR INJECTION */(function(global) {/* global window */ - 'use strict'; - - exports.__esModule = true; - - exports['default'] = function (Handlebars) { - /* istanbul ignore next */ - var root = typeof global !== 'undefined' ? global : window, - $Handlebars = root.Handlebars; - /* istanbul ignore next */ - Handlebars.noConflict = function () { - if (root.Handlebars === Handlebars) { - root.Handlebars = $Handlebars; - } - return Handlebars; - }; - }; - - module.exports = exports['default']; - /* WEBPACK VAR INJECTION */}.call(exports, (function() { return this; }()))) - -/***/ }), -/* 35 */ -/***/ (function(module, exports) { - - 'use strict'; - - exports.__esModule = true; - var AST = { - // Public API used to evaluate derived attributes regarding AST nodes - helpers: { - // a mustache is definitely a helper if: - // * it is an eligible helper, and - // * it has at least one parameter or hash segment - helperExpression: function helperExpression(node) { - return node.type === 'SubExpression' || (node.type === 'MustacheStatement' || node.type === 'BlockStatement') && !!(node.params && node.params.length || node.hash); - }, - - scopedId: function scopedId(path) { - return (/^\.|this\b/.test(path.original) - ); - }, - - // an ID is simple if it only has one part, and that part is not - // `..` or `this`. - simpleId: function simpleId(path) { - return path.parts.length === 1 && !AST.helpers.scopedId(path) && !path.depth; - } - } - }; - - // Must be exported as an object rather than the root of the module as the jison lexer - // must modify the object to operate properly. - exports['default'] = AST; - module.exports = exports['default']; - -/***/ }), -/* 36 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - var _interopRequireWildcard = __webpack_require__(3)['default']; - - exports.__esModule = true; - exports.parse = parse; - - var _parser = __webpack_require__(37); - - var _parser2 = _interopRequireDefault(_parser); - - var _whitespaceControl = __webpack_require__(38); - - var _whitespaceControl2 = _interopRequireDefault(_whitespaceControl); - - var _helpers = __webpack_require__(40); - - var Helpers = _interopRequireWildcard(_helpers); - - var _utils = __webpack_require__(5); - - exports.parser = _parser2['default']; - - var yy = {}; - _utils.extend(yy, Helpers); - - function parse(input, options) { - // Just return if an already-compiled AST was passed in. - if (input.type === 'Program') { - return input; - } - - _parser2['default'].yy = yy; - - // Altering the shared object here, but this is ok as parser is a sync operation - yy.locInfo = function (locInfo) { - return new yy.SourceLocation(options && options.srcName, locInfo); - }; - - var strip = new _whitespaceControl2['default'](options); - return strip.accept(_parser2['default'].parse(input)); - } - -/***/ }), -/* 37 */ -/***/ (function(module, exports) { - - // File ignored in coverage tests via setting in .istanbul.yml - /* Jison generated parser */ - "use strict"; - - exports.__esModule = true; - var handlebars = (function () { - var parser = { trace: function trace() {}, - yy: {}, - symbols_: { "error": 2, "root": 3, "program": 4, "EOF": 5, "program_repetition0": 6, "statement": 7, "mustache": 8, "block": 9, "rawBlock": 10, "partial": 11, "partialBlock": 12, "content": 13, "COMMENT": 14, "CONTENT": 15, "openRawBlock": 16, "rawBlock_repetition_plus0": 17, "END_RAW_BLOCK": 18, "OPEN_RAW_BLOCK": 19, "helperName": 20, "openRawBlock_repetition0": 21, "openRawBlock_option0": 22, "CLOSE_RAW_BLOCK": 23, "openBlock": 24, "block_option0": 25, "closeBlock": 26, "openInverse": 27, "block_option1": 28, "OPEN_BLOCK": 29, "openBlock_repetition0": 30, "openBlock_option0": 31, "openBlock_option1": 32, "CLOSE": 33, "OPEN_INVERSE": 34, "openInverse_repetition0": 35, "openInverse_option0": 36, "openInverse_option1": 37, "openInverseChain": 38, "OPEN_INVERSE_CHAIN": 39, "openInverseChain_repetition0": 40, "openInverseChain_option0": 41, "openInverseChain_option1": 42, "inverseAndProgram": 43, "INVERSE": 44, "inverseChain": 45, "inverseChain_option0": 46, "OPEN_ENDBLOCK": 47, "OPEN": 48, "mustache_repetition0": 49, "mustache_option0": 50, "OPEN_UNESCAPED": 51, "mustache_repetition1": 52, "mustache_option1": 53, "CLOSE_UNESCAPED": 54, "OPEN_PARTIAL": 55, "partialName": 56, "partial_repetition0": 57, "partial_option0": 58, "openPartialBlock": 59, "OPEN_PARTIAL_BLOCK": 60, "openPartialBlock_repetition0": 61, "openPartialBlock_option0": 62, "param": 63, "sexpr": 64, "OPEN_SEXPR": 65, "sexpr_repetition0": 66, "sexpr_option0": 67, "CLOSE_SEXPR": 68, "hash": 69, "hash_repetition_plus0": 70, "hashSegment": 71, "ID": 72, "EQUALS": 73, "blockParams": 74, "OPEN_BLOCK_PARAMS": 75, "blockParams_repetition_plus0": 76, "CLOSE_BLOCK_PARAMS": 77, "path": 78, "dataName": 79, "STRING": 80, "NUMBER": 81, "BOOLEAN": 82, "UNDEFINED": 83, "NULL": 84, "DATA": 85, "pathSegments": 86, "SEP": 87, "$accept": 0, "$end": 1 }, - terminals_: { 2: "error", 5: "EOF", 14: "COMMENT", 15: "CONTENT", 18: "END_RAW_BLOCK", 19: "OPEN_RAW_BLOCK", 23: "CLOSE_RAW_BLOCK", 29: "OPEN_BLOCK", 33: "CLOSE", 34: "OPEN_INVERSE", 39: "OPEN_INVERSE_CHAIN", 44: "INVERSE", 47: "OPEN_ENDBLOCK", 48: "OPEN", 51: "OPEN_UNESCAPED", 54: "CLOSE_UNESCAPED", 55: "OPEN_PARTIAL", 60: "OPEN_PARTIAL_BLOCK", 65: "OPEN_SEXPR", 68: "CLOSE_SEXPR", 72: "ID", 73: "EQUALS", 75: "OPEN_BLOCK_PARAMS", 77: "CLOSE_BLOCK_PARAMS", 80: "STRING", 81: "NUMBER", 82: "BOOLEAN", 83: "UNDEFINED", 84: "NULL", 85: "DATA", 87: "SEP" }, - productions_: [0, [3, 2], [4, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [7, 1], [13, 1], [10, 3], [16, 5], [9, 4], [9, 4], [24, 6], [27, 6], [38, 6], [43, 2], [45, 3], [45, 1], [26, 3], [8, 5], [8, 5], [11, 5], [12, 3], [59, 5], [63, 1], [63, 1], [64, 5], [69, 1], [71, 3], [74, 3], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [20, 1], [56, 1], [56, 1], [79, 2], [78, 1], [86, 3], [86, 1], [6, 0], [6, 2], [17, 1], [17, 2], [21, 0], [21, 2], [22, 0], [22, 1], [25, 0], [25, 1], [28, 0], [28, 1], [30, 0], [30, 2], [31, 0], [31, 1], [32, 0], [32, 1], [35, 0], [35, 2], [36, 0], [36, 1], [37, 0], [37, 1], [40, 0], [40, 2], [41, 0], [41, 1], [42, 0], [42, 1], [46, 0], [46, 1], [49, 0], [49, 2], [50, 0], [50, 1], [52, 0], [52, 2], [53, 0], [53, 1], [57, 0], [57, 2], [58, 0], [58, 1], [61, 0], [61, 2], [62, 0], [62, 1], [66, 0], [66, 2], [67, 0], [67, 1], [70, 1], [70, 2], [76, 1], [76, 2]], - performAction: function anonymous(yytext, yyleng, yylineno, yy, yystate, $$, _$ - /**/) { - - var $0 = $$.length - 1; - switch (yystate) { - case 1: - return $$[$0 - 1]; - break; - case 2: - this.$ = yy.prepareProgram($$[$0]); - break; - case 3: - this.$ = $$[$0]; - break; - case 4: - this.$ = $$[$0]; - break; - case 5: - this.$ = $$[$0]; - break; - case 6: - this.$ = $$[$0]; - break; - case 7: - this.$ = $$[$0]; - break; - case 8: - this.$ = $$[$0]; - break; - case 9: - this.$ = { - type: 'CommentStatement', - value: yy.stripComment($$[$0]), - strip: yy.stripFlags($$[$0], $$[$0]), - loc: yy.locInfo(this._$) - }; - - break; - case 10: - this.$ = { - type: 'ContentStatement', - original: $$[$0], - value: $$[$0], - loc: yy.locInfo(this._$) - }; - - break; - case 11: - this.$ = yy.prepareRawBlock($$[$0 - 2], $$[$0 - 1], $$[$0], this._$); - break; - case 12: - this.$ = { path: $$[$0 - 3], params: $$[$0 - 2], hash: $$[$0 - 1] }; - break; - case 13: - this.$ = yy.prepareBlock($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0], false, this._$); - break; - case 14: - this.$ = yy.prepareBlock($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0], true, this._$); - break; - case 15: - this.$ = { open: $$[$0 - 5], path: $$[$0 - 4], params: $$[$0 - 3], hash: $$[$0 - 2], blockParams: $$[$0 - 1], strip: yy.stripFlags($$[$0 - 5], $$[$0]) }; - break; - case 16: - this.$ = { path: $$[$0 - 4], params: $$[$0 - 3], hash: $$[$0 - 2], blockParams: $$[$0 - 1], strip: yy.stripFlags($$[$0 - 5], $$[$0]) }; - break; - case 17: - this.$ = { path: $$[$0 - 4], params: $$[$0 - 3], hash: $$[$0 - 2], blockParams: $$[$0 - 1], strip: yy.stripFlags($$[$0 - 5], $$[$0]) }; - break; - case 18: - this.$ = { strip: yy.stripFlags($$[$0 - 1], $$[$0 - 1]), program: $$[$0] }; - break; - case 19: - var inverse = yy.prepareBlock($$[$0 - 2], $$[$0 - 1], $$[$0], $$[$0], false, this._$), - program = yy.prepareProgram([inverse], $$[$0 - 1].loc); - program.chained = true; - - this.$ = { strip: $$[$0 - 2].strip, program: program, chain: true }; - - break; - case 20: - this.$ = $$[$0]; - break; - case 21: - this.$ = { path: $$[$0 - 1], strip: yy.stripFlags($$[$0 - 2], $$[$0]) }; - break; - case 22: - this.$ = yy.prepareMustache($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0 - 4], yy.stripFlags($$[$0 - 4], $$[$0]), this._$); - break; - case 23: - this.$ = yy.prepareMustache($$[$0 - 3], $$[$0 - 2], $$[$0 - 1], $$[$0 - 4], yy.stripFlags($$[$0 - 4], $$[$0]), this._$); - break; - case 24: - this.$ = { - type: 'PartialStatement', - name: $$[$0 - 3], - params: $$[$0 - 2], - hash: $$[$0 - 1], - indent: '', - strip: yy.stripFlags($$[$0 - 4], $$[$0]), - loc: yy.locInfo(this._$) - }; - - break; - case 25: - this.$ = yy.preparePartialBlock($$[$0 - 2], $$[$0 - 1], $$[$0], this._$); - break; - case 26: - this.$ = { path: $$[$0 - 3], params: $$[$0 - 2], hash: $$[$0 - 1], strip: yy.stripFlags($$[$0 - 4], $$[$0]) }; - break; - case 27: - this.$ = $$[$0]; - break; - case 28: - this.$ = $$[$0]; - break; - case 29: - this.$ = { - type: 'SubExpression', - path: $$[$0 - 3], - params: $$[$0 - 2], - hash: $$[$0 - 1], - loc: yy.locInfo(this._$) - }; - - break; - case 30: - this.$ = { type: 'Hash', pairs: $$[$0], loc: yy.locInfo(this._$) }; - break; - case 31: - this.$ = { type: 'HashPair', key: yy.id($$[$0 - 2]), value: $$[$0], loc: yy.locInfo(this._$) }; - break; - case 32: - this.$ = yy.id($$[$0 - 1]); - break; - case 33: - this.$ = $$[$0]; - break; - case 34: - this.$ = $$[$0]; - break; - case 35: - this.$ = { type: 'StringLiteral', value: $$[$0], original: $$[$0], loc: yy.locInfo(this._$) }; - break; - case 36: - this.$ = { type: 'NumberLiteral', value: Number($$[$0]), original: Number($$[$0]), loc: yy.locInfo(this._$) }; - break; - case 37: - this.$ = { type: 'BooleanLiteral', value: $$[$0] === 'true', original: $$[$0] === 'true', loc: yy.locInfo(this._$) }; - break; - case 38: - this.$ = { type: 'UndefinedLiteral', original: undefined, value: undefined, loc: yy.locInfo(this._$) }; - break; - case 39: - this.$ = { type: 'NullLiteral', original: null, value: null, loc: yy.locInfo(this._$) }; - break; - case 40: - this.$ = $$[$0]; - break; - case 41: - this.$ = $$[$0]; - break; - case 42: - this.$ = yy.preparePath(true, $$[$0], this._$); - break; - case 43: - this.$ = yy.preparePath(false, $$[$0], this._$); - break; - case 44: - $$[$0 - 2].push({ part: yy.id($$[$0]), original: $$[$0], separator: $$[$0 - 1] });this.$ = $$[$0 - 2]; - break; - case 45: - this.$ = [{ part: yy.id($$[$0]), original: $$[$0] }]; - break; - case 46: - this.$ = []; - break; - case 47: - $$[$0 - 1].push($$[$0]); - break; - case 48: - this.$ = [$$[$0]]; - break; - case 49: - $$[$0 - 1].push($$[$0]); - break; - case 50: - this.$ = []; - break; - case 51: - $$[$0 - 1].push($$[$0]); - break; - case 58: - this.$ = []; - break; - case 59: - $$[$0 - 1].push($$[$0]); - break; - case 64: - this.$ = []; - break; - case 65: - $$[$0 - 1].push($$[$0]); - break; - case 70: - this.$ = []; - break; - case 71: - $$[$0 - 1].push($$[$0]); - break; - case 78: - this.$ = []; - break; - case 79: - $$[$0 - 1].push($$[$0]); - break; - case 82: - this.$ = []; - break; - case 83: - $$[$0 - 1].push($$[$0]); - break; - case 86: - this.$ = []; - break; - case 87: - $$[$0 - 1].push($$[$0]); - break; - case 90: - this.$ = []; - break; - case 91: - $$[$0 - 1].push($$[$0]); - break; - case 94: - this.$ = []; - break; - case 95: - $$[$0 - 1].push($$[$0]); - break; - case 98: - this.$ = [$$[$0]]; - break; - case 99: - $$[$0 - 1].push($$[$0]); - break; - case 100: - this.$ = [$$[$0]]; - break; - case 101: - $$[$0 - 1].push($$[$0]); - break; - } - }, - table: [{ 3: 1, 4: 2, 5: [2, 46], 6: 3, 14: [2, 46], 15: [2, 46], 19: [2, 46], 29: [2, 46], 34: [2, 46], 48: [2, 46], 51: [2, 46], 55: [2, 46], 60: [2, 46] }, { 1: [3] }, { 5: [1, 4] }, { 5: [2, 2], 7: 5, 8: 6, 9: 7, 10: 8, 11: 9, 12: 10, 13: 11, 14: [1, 12], 15: [1, 20], 16: 17, 19: [1, 23], 24: 15, 27: 16, 29: [1, 21], 34: [1, 22], 39: [2, 2], 44: [2, 2], 47: [2, 2], 48: [1, 13], 51: [1, 14], 55: [1, 18], 59: 19, 60: [1, 24] }, { 1: [2, 1] }, { 5: [2, 47], 14: [2, 47], 15: [2, 47], 19: [2, 47], 29: [2, 47], 34: [2, 47], 39: [2, 47], 44: [2, 47], 47: [2, 47], 48: [2, 47], 51: [2, 47], 55: [2, 47], 60: [2, 47] }, { 5: [2, 3], 14: [2, 3], 15: [2, 3], 19: [2, 3], 29: [2, 3], 34: [2, 3], 39: [2, 3], 44: [2, 3], 47: [2, 3], 48: [2, 3], 51: [2, 3], 55: [2, 3], 60: [2, 3] }, { 5: [2, 4], 14: [2, 4], 15: [2, 4], 19: [2, 4], 29: [2, 4], 34: [2, 4], 39: [2, 4], 44: [2, 4], 47: [2, 4], 48: [2, 4], 51: [2, 4], 55: [2, 4], 60: [2, 4] }, { 5: [2, 5], 14: [2, 5], 15: [2, 5], 19: [2, 5], 29: [2, 5], 34: [2, 5], 39: [2, 5], 44: [2, 5], 47: [2, 5], 48: [2, 5], 51: [2, 5], 55: [2, 5], 60: [2, 5] }, { 5: [2, 6], 14: [2, 6], 15: [2, 6], 19: [2, 6], 29: [2, 6], 34: [2, 6], 39: [2, 6], 44: [2, 6], 47: [2, 6], 48: [2, 6], 51: [2, 6], 55: [2, 6], 60: [2, 6] }, { 5: [2, 7], 14: [2, 7], 15: [2, 7], 19: [2, 7], 29: [2, 7], 34: [2, 7], 39: [2, 7], 44: [2, 7], 47: [2, 7], 48: [2, 7], 51: [2, 7], 55: [2, 7], 60: [2, 7] }, { 5: [2, 8], 14: [2, 8], 15: [2, 8], 19: [2, 8], 29: [2, 8], 34: [2, 8], 39: [2, 8], 44: [2, 8], 47: [2, 8], 48: [2, 8], 51: [2, 8], 55: [2, 8], 60: [2, 8] }, { 5: [2, 9], 14: [2, 9], 15: [2, 9], 19: [2, 9], 29: [2, 9], 34: [2, 9], 39: [2, 9], 44: [2, 9], 47: [2, 9], 48: [2, 9], 51: [2, 9], 55: [2, 9], 60: [2, 9] }, { 20: 25, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 36, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 4: 37, 6: 3, 14: [2, 46], 15: [2, 46], 19: [2, 46], 29: [2, 46], 34: [2, 46], 39: [2, 46], 44: [2, 46], 47: [2, 46], 48: [2, 46], 51: [2, 46], 55: [2, 46], 60: [2, 46] }, { 4: 38, 6: 3, 14: [2, 46], 15: [2, 46], 19: [2, 46], 29: [2, 46], 34: [2, 46], 44: [2, 46], 47: [2, 46], 48: [2, 46], 51: [2, 46], 55: [2, 46], 60: [2, 46] }, { 13: 40, 15: [1, 20], 17: 39 }, { 20: 42, 56: 41, 64: 43, 65: [1, 44], 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 4: 45, 6: 3, 14: [2, 46], 15: [2, 46], 19: [2, 46], 29: [2, 46], 34: [2, 46], 47: [2, 46], 48: [2, 46], 51: [2, 46], 55: [2, 46], 60: [2, 46] }, { 5: [2, 10], 14: [2, 10], 15: [2, 10], 18: [2, 10], 19: [2, 10], 29: [2, 10], 34: [2, 10], 39: [2, 10], 44: [2, 10], 47: [2, 10], 48: [2, 10], 51: [2, 10], 55: [2, 10], 60: [2, 10] }, { 20: 46, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 47, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 48, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 42, 56: 49, 64: 43, 65: [1, 44], 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 33: [2, 78], 49: 50, 65: [2, 78], 72: [2, 78], 80: [2, 78], 81: [2, 78], 82: [2, 78], 83: [2, 78], 84: [2, 78], 85: [2, 78] }, { 23: [2, 33], 33: [2, 33], 54: [2, 33], 65: [2, 33], 68: [2, 33], 72: [2, 33], 75: [2, 33], 80: [2, 33], 81: [2, 33], 82: [2, 33], 83: [2, 33], 84: [2, 33], 85: [2, 33] }, { 23: [2, 34], 33: [2, 34], 54: [2, 34], 65: [2, 34], 68: [2, 34], 72: [2, 34], 75: [2, 34], 80: [2, 34], 81: [2, 34], 82: [2, 34], 83: [2, 34], 84: [2, 34], 85: [2, 34] }, { 23: [2, 35], 33: [2, 35], 54: [2, 35], 65: [2, 35], 68: [2, 35], 72: [2, 35], 75: [2, 35], 80: [2, 35], 81: [2, 35], 82: [2, 35], 83: [2, 35], 84: [2, 35], 85: [2, 35] }, { 23: [2, 36], 33: [2, 36], 54: [2, 36], 65: [2, 36], 68: [2, 36], 72: [2, 36], 75: [2, 36], 80: [2, 36], 81: [2, 36], 82: [2, 36], 83: [2, 36], 84: [2, 36], 85: [2, 36] }, { 23: [2, 37], 33: [2, 37], 54: [2, 37], 65: [2, 37], 68: [2, 37], 72: [2, 37], 75: [2, 37], 80: [2, 37], 81: [2, 37], 82: [2, 37], 83: [2, 37], 84: [2, 37], 85: [2, 37] }, { 23: [2, 38], 33: [2, 38], 54: [2, 38], 65: [2, 38], 68: [2, 38], 72: [2, 38], 75: [2, 38], 80: [2, 38], 81: [2, 38], 82: [2, 38], 83: [2, 38], 84: [2, 38], 85: [2, 38] }, { 23: [2, 39], 33: [2, 39], 54: [2, 39], 65: [2, 39], 68: [2, 39], 72: [2, 39], 75: [2, 39], 80: [2, 39], 81: [2, 39], 82: [2, 39], 83: [2, 39], 84: [2, 39], 85: [2, 39] }, { 23: [2, 43], 33: [2, 43], 54: [2, 43], 65: [2, 43], 68: [2, 43], 72: [2, 43], 75: [2, 43], 80: [2, 43], 81: [2, 43], 82: [2, 43], 83: [2, 43], 84: [2, 43], 85: [2, 43], 87: [1, 51] }, { 72: [1, 35], 86: 52 }, { 23: [2, 45], 33: [2, 45], 54: [2, 45], 65: [2, 45], 68: [2, 45], 72: [2, 45], 75: [2, 45], 80: [2, 45], 81: [2, 45], 82: [2, 45], 83: [2, 45], 84: [2, 45], 85: [2, 45], 87: [2, 45] }, { 52: 53, 54: [2, 82], 65: [2, 82], 72: [2, 82], 80: [2, 82], 81: [2, 82], 82: [2, 82], 83: [2, 82], 84: [2, 82], 85: [2, 82] }, { 25: 54, 38: 56, 39: [1, 58], 43: 57, 44: [1, 59], 45: 55, 47: [2, 54] }, { 28: 60, 43: 61, 44: [1, 59], 47: [2, 56] }, { 13: 63, 15: [1, 20], 18: [1, 62] }, { 15: [2, 48], 18: [2, 48] }, { 33: [2, 86], 57: 64, 65: [2, 86], 72: [2, 86], 80: [2, 86], 81: [2, 86], 82: [2, 86], 83: [2, 86], 84: [2, 86], 85: [2, 86] }, { 33: [2, 40], 65: [2, 40], 72: [2, 40], 80: [2, 40], 81: [2, 40], 82: [2, 40], 83: [2, 40], 84: [2, 40], 85: [2, 40] }, { 33: [2, 41], 65: [2, 41], 72: [2, 41], 80: [2, 41], 81: [2, 41], 82: [2, 41], 83: [2, 41], 84: [2, 41], 85: [2, 41] }, { 20: 65, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 26: 66, 47: [1, 67] }, { 30: 68, 33: [2, 58], 65: [2, 58], 72: [2, 58], 75: [2, 58], 80: [2, 58], 81: [2, 58], 82: [2, 58], 83: [2, 58], 84: [2, 58], 85: [2, 58] }, { 33: [2, 64], 35: 69, 65: [2, 64], 72: [2, 64], 75: [2, 64], 80: [2, 64], 81: [2, 64], 82: [2, 64], 83: [2, 64], 84: [2, 64], 85: [2, 64] }, { 21: 70, 23: [2, 50], 65: [2, 50], 72: [2, 50], 80: [2, 50], 81: [2, 50], 82: [2, 50], 83: [2, 50], 84: [2, 50], 85: [2, 50] }, { 33: [2, 90], 61: 71, 65: [2, 90], 72: [2, 90], 80: [2, 90], 81: [2, 90], 82: [2, 90], 83: [2, 90], 84: [2, 90], 85: [2, 90] }, { 20: 75, 33: [2, 80], 50: 72, 63: 73, 64: 76, 65: [1, 44], 69: 74, 70: 77, 71: 78, 72: [1, 79], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 72: [1, 80] }, { 23: [2, 42], 33: [2, 42], 54: [2, 42], 65: [2, 42], 68: [2, 42], 72: [2, 42], 75: [2, 42], 80: [2, 42], 81: [2, 42], 82: [2, 42], 83: [2, 42], 84: [2, 42], 85: [2, 42], 87: [1, 51] }, { 20: 75, 53: 81, 54: [2, 84], 63: 82, 64: 76, 65: [1, 44], 69: 83, 70: 77, 71: 78, 72: [1, 79], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 26: 84, 47: [1, 67] }, { 47: [2, 55] }, { 4: 85, 6: 3, 14: [2, 46], 15: [2, 46], 19: [2, 46], 29: [2, 46], 34: [2, 46], 39: [2, 46], 44: [2, 46], 47: [2, 46], 48: [2, 46], 51: [2, 46], 55: [2, 46], 60: [2, 46] }, { 47: [2, 20] }, { 20: 86, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 4: 87, 6: 3, 14: [2, 46], 15: [2, 46], 19: [2, 46], 29: [2, 46], 34: [2, 46], 47: [2, 46], 48: [2, 46], 51: [2, 46], 55: [2, 46], 60: [2, 46] }, { 26: 88, 47: [1, 67] }, { 47: [2, 57] }, { 5: [2, 11], 14: [2, 11], 15: [2, 11], 19: [2, 11], 29: [2, 11], 34: [2, 11], 39: [2, 11], 44: [2, 11], 47: [2, 11], 48: [2, 11], 51: [2, 11], 55: [2, 11], 60: [2, 11] }, { 15: [2, 49], 18: [2, 49] }, { 20: 75, 33: [2, 88], 58: 89, 63: 90, 64: 76, 65: [1, 44], 69: 91, 70: 77, 71: 78, 72: [1, 79], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 65: [2, 94], 66: 92, 68: [2, 94], 72: [2, 94], 80: [2, 94], 81: [2, 94], 82: [2, 94], 83: [2, 94], 84: [2, 94], 85: [2, 94] }, { 5: [2, 25], 14: [2, 25], 15: [2, 25], 19: [2, 25], 29: [2, 25], 34: [2, 25], 39: [2, 25], 44: [2, 25], 47: [2, 25], 48: [2, 25], 51: [2, 25], 55: [2, 25], 60: [2, 25] }, { 20: 93, 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 75, 31: 94, 33: [2, 60], 63: 95, 64: 76, 65: [1, 44], 69: 96, 70: 77, 71: 78, 72: [1, 79], 75: [2, 60], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 75, 33: [2, 66], 36: 97, 63: 98, 64: 76, 65: [1, 44], 69: 99, 70: 77, 71: 78, 72: [1, 79], 75: [2, 66], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 75, 22: 100, 23: [2, 52], 63: 101, 64: 76, 65: [1, 44], 69: 102, 70: 77, 71: 78, 72: [1, 79], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 20: 75, 33: [2, 92], 62: 103, 63: 104, 64: 76, 65: [1, 44], 69: 105, 70: 77, 71: 78, 72: [1, 79], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 33: [1, 106] }, { 33: [2, 79], 65: [2, 79], 72: [2, 79], 80: [2, 79], 81: [2, 79], 82: [2, 79], 83: [2, 79], 84: [2, 79], 85: [2, 79] }, { 33: [2, 81] }, { 23: [2, 27], 33: [2, 27], 54: [2, 27], 65: [2, 27], 68: [2, 27], 72: [2, 27], 75: [2, 27], 80: [2, 27], 81: [2, 27], 82: [2, 27], 83: [2, 27], 84: [2, 27], 85: [2, 27] }, { 23: [2, 28], 33: [2, 28], 54: [2, 28], 65: [2, 28], 68: [2, 28], 72: [2, 28], 75: [2, 28], 80: [2, 28], 81: [2, 28], 82: [2, 28], 83: [2, 28], 84: [2, 28], 85: [2, 28] }, { 23: [2, 30], 33: [2, 30], 54: [2, 30], 68: [2, 30], 71: 107, 72: [1, 108], 75: [2, 30] }, { 23: [2, 98], 33: [2, 98], 54: [2, 98], 68: [2, 98], 72: [2, 98], 75: [2, 98] }, { 23: [2, 45], 33: [2, 45], 54: [2, 45], 65: [2, 45], 68: [2, 45], 72: [2, 45], 73: [1, 109], 75: [2, 45], 80: [2, 45], 81: [2, 45], 82: [2, 45], 83: [2, 45], 84: [2, 45], 85: [2, 45], 87: [2, 45] }, { 23: [2, 44], 33: [2, 44], 54: [2, 44], 65: [2, 44], 68: [2, 44], 72: [2, 44], 75: [2, 44], 80: [2, 44], 81: [2, 44], 82: [2, 44], 83: [2, 44], 84: [2, 44], 85: [2, 44], 87: [2, 44] }, { 54: [1, 110] }, { 54: [2, 83], 65: [2, 83], 72: [2, 83], 80: [2, 83], 81: [2, 83], 82: [2, 83], 83: [2, 83], 84: [2, 83], 85: [2, 83] }, { 54: [2, 85] }, { 5: [2, 13], 14: [2, 13], 15: [2, 13], 19: [2, 13], 29: [2, 13], 34: [2, 13], 39: [2, 13], 44: [2, 13], 47: [2, 13], 48: [2, 13], 51: [2, 13], 55: [2, 13], 60: [2, 13] }, { 38: 56, 39: [1, 58], 43: 57, 44: [1, 59], 45: 112, 46: 111, 47: [2, 76] }, { 33: [2, 70], 40: 113, 65: [2, 70], 72: [2, 70], 75: [2, 70], 80: [2, 70], 81: [2, 70], 82: [2, 70], 83: [2, 70], 84: [2, 70], 85: [2, 70] }, { 47: [2, 18] }, { 5: [2, 14], 14: [2, 14], 15: [2, 14], 19: [2, 14], 29: [2, 14], 34: [2, 14], 39: [2, 14], 44: [2, 14], 47: [2, 14], 48: [2, 14], 51: [2, 14], 55: [2, 14], 60: [2, 14] }, { 33: [1, 114] }, { 33: [2, 87], 65: [2, 87], 72: [2, 87], 80: [2, 87], 81: [2, 87], 82: [2, 87], 83: [2, 87], 84: [2, 87], 85: [2, 87] }, { 33: [2, 89] }, { 20: 75, 63: 116, 64: 76, 65: [1, 44], 67: 115, 68: [2, 96], 69: 117, 70: 77, 71: 78, 72: [1, 79], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 33: [1, 118] }, { 32: 119, 33: [2, 62], 74: 120, 75: [1, 121] }, { 33: [2, 59], 65: [2, 59], 72: [2, 59], 75: [2, 59], 80: [2, 59], 81: [2, 59], 82: [2, 59], 83: [2, 59], 84: [2, 59], 85: [2, 59] }, { 33: [2, 61], 75: [2, 61] }, { 33: [2, 68], 37: 122, 74: 123, 75: [1, 121] }, { 33: [2, 65], 65: [2, 65], 72: [2, 65], 75: [2, 65], 80: [2, 65], 81: [2, 65], 82: [2, 65], 83: [2, 65], 84: [2, 65], 85: [2, 65] }, { 33: [2, 67], 75: [2, 67] }, { 23: [1, 124] }, { 23: [2, 51], 65: [2, 51], 72: [2, 51], 80: [2, 51], 81: [2, 51], 82: [2, 51], 83: [2, 51], 84: [2, 51], 85: [2, 51] }, { 23: [2, 53] }, { 33: [1, 125] }, { 33: [2, 91], 65: [2, 91], 72: [2, 91], 80: [2, 91], 81: [2, 91], 82: [2, 91], 83: [2, 91], 84: [2, 91], 85: [2, 91] }, { 33: [2, 93] }, { 5: [2, 22], 14: [2, 22], 15: [2, 22], 19: [2, 22], 29: [2, 22], 34: [2, 22], 39: [2, 22], 44: [2, 22], 47: [2, 22], 48: [2, 22], 51: [2, 22], 55: [2, 22], 60: [2, 22] }, { 23: [2, 99], 33: [2, 99], 54: [2, 99], 68: [2, 99], 72: [2, 99], 75: [2, 99] }, { 73: [1, 109] }, { 20: 75, 63: 126, 64: 76, 65: [1, 44], 72: [1, 35], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 5: [2, 23], 14: [2, 23], 15: [2, 23], 19: [2, 23], 29: [2, 23], 34: [2, 23], 39: [2, 23], 44: [2, 23], 47: [2, 23], 48: [2, 23], 51: [2, 23], 55: [2, 23], 60: [2, 23] }, { 47: [2, 19] }, { 47: [2, 77] }, { 20: 75, 33: [2, 72], 41: 127, 63: 128, 64: 76, 65: [1, 44], 69: 129, 70: 77, 71: 78, 72: [1, 79], 75: [2, 72], 78: 26, 79: 27, 80: [1, 28], 81: [1, 29], 82: [1, 30], 83: [1, 31], 84: [1, 32], 85: [1, 34], 86: 33 }, { 5: [2, 24], 14: [2, 24], 15: [2, 24], 19: [2, 24], 29: [2, 24], 34: [2, 24], 39: [2, 24], 44: [2, 24], 47: [2, 24], 48: [2, 24], 51: [2, 24], 55: [2, 24], 60: [2, 24] }, { 68: [1, 130] }, { 65: [2, 95], 68: [2, 95], 72: [2, 95], 80: [2, 95], 81: [2, 95], 82: [2, 95], 83: [2, 95], 84: [2, 95], 85: [2, 95] }, { 68: [2, 97] }, { 5: [2, 21], 14: [2, 21], 15: [2, 21], 19: [2, 21], 29: [2, 21], 34: [2, 21], 39: [2, 21], 44: [2, 21], 47: [2, 21], 48: [2, 21], 51: [2, 21], 55: [2, 21], 60: [2, 21] }, { 33: [1, 131] }, { 33: [2, 63] }, { 72: [1, 133], 76: 132 }, { 33: [1, 134] }, { 33: [2, 69] }, { 15: [2, 12] }, { 14: [2, 26], 15: [2, 26], 19: [2, 26], 29: [2, 26], 34: [2, 26], 47: [2, 26], 48: [2, 26], 51: [2, 26], 55: [2, 26], 60: [2, 26] }, { 23: [2, 31], 33: [2, 31], 54: [2, 31], 68: [2, 31], 72: [2, 31], 75: [2, 31] }, { 33: [2, 74], 42: 135, 74: 136, 75: [1, 121] }, { 33: [2, 71], 65: [2, 71], 72: [2, 71], 75: [2, 71], 80: [2, 71], 81: [2, 71], 82: [2, 71], 83: [2, 71], 84: [2, 71], 85: [2, 71] }, { 33: [2, 73], 75: [2, 73] }, { 23: [2, 29], 33: [2, 29], 54: [2, 29], 65: [2, 29], 68: [2, 29], 72: [2, 29], 75: [2, 29], 80: [2, 29], 81: [2, 29], 82: [2, 29], 83: [2, 29], 84: [2, 29], 85: [2, 29] }, { 14: [2, 15], 15: [2, 15], 19: [2, 15], 29: [2, 15], 34: [2, 15], 39: [2, 15], 44: [2, 15], 47: [2, 15], 48: [2, 15], 51: [2, 15], 55: [2, 15], 60: [2, 15] }, { 72: [1, 138], 77: [1, 137] }, { 72: [2, 100], 77: [2, 100] }, { 14: [2, 16], 15: [2, 16], 19: [2, 16], 29: [2, 16], 34: [2, 16], 44: [2, 16], 47: [2, 16], 48: [2, 16], 51: [2, 16], 55: [2, 16], 60: [2, 16] }, { 33: [1, 139] }, { 33: [2, 75] }, { 33: [2, 32] }, { 72: [2, 101], 77: [2, 101] }, { 14: [2, 17], 15: [2, 17], 19: [2, 17], 29: [2, 17], 34: [2, 17], 39: [2, 17], 44: [2, 17], 47: [2, 17], 48: [2, 17], 51: [2, 17], 55: [2, 17], 60: [2, 17] }], - defaultActions: { 4: [2, 1], 55: [2, 55], 57: [2, 20], 61: [2, 57], 74: [2, 81], 83: [2, 85], 87: [2, 18], 91: [2, 89], 102: [2, 53], 105: [2, 93], 111: [2, 19], 112: [2, 77], 117: [2, 97], 120: [2, 63], 123: [2, 69], 124: [2, 12], 136: [2, 75], 137: [2, 32] }, - parseError: function parseError(str, hash) { - throw new Error(str); - }, - parse: function parse(input) { - var self = this, - stack = [0], - vstack = [null], - lstack = [], - table = this.table, - yytext = "", - yylineno = 0, - yyleng = 0, - recovering = 0, - TERROR = 2, - EOF = 1; - this.lexer.setInput(input); - this.lexer.yy = this.yy; - this.yy.lexer = this.lexer; - this.yy.parser = this; - if (typeof this.lexer.yylloc == "undefined") this.lexer.yylloc = {}; - var yyloc = this.lexer.yylloc; - lstack.push(yyloc); - var ranges = this.lexer.options && this.lexer.options.ranges; - if (typeof this.yy.parseError === "function") this.parseError = this.yy.parseError; - function popStack(n) { - stack.length = stack.length - 2 * n; - vstack.length = vstack.length - n; - lstack.length = lstack.length - n; - } - function lex() { - var token; - token = self.lexer.lex() || 1; - if (typeof token !== "number") { - token = self.symbols_[token] || token; - } - return token; - } - var symbol, - preErrorSymbol, - state, - action, - a, - r, - yyval = {}, - p, - len, - newState, - expected; - while (true) { - state = stack[stack.length - 1]; - if (this.defaultActions[state]) { - action = this.defaultActions[state]; - } else { - if (symbol === null || typeof symbol == "undefined") { - symbol = lex(); - } - action = table[state] && table[state][symbol]; - } - if (typeof action === "undefined" || !action.length || !action[0]) { - var errStr = ""; - if (!recovering) { - expected = []; - for (p in table[state]) if (this.terminals_[p] && p > 2) { - expected.push("'" + this.terminals_[p] + "'"); - } - if (this.lexer.showPosition) { - errStr = "Parse error on line " + (yylineno + 1) + ":\n" + this.lexer.showPosition() + "\nExpecting " + expected.join(", ") + ", got '" + (this.terminals_[symbol] || symbol) + "'"; - } else { - errStr = "Parse error on line " + (yylineno + 1) + ": Unexpected " + (symbol == 1 ? "end of input" : "'" + (this.terminals_[symbol] || symbol) + "'"); - } - this.parseError(errStr, { text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected }); - } - } - if (action[0] instanceof Array && action.length > 1) { - throw new Error("Parse Error: multiple actions possible at state: " + state + ", token: " + symbol); - } - switch (action[0]) { - case 1: - stack.push(symbol); - vstack.push(this.lexer.yytext); - lstack.push(this.lexer.yylloc); - stack.push(action[1]); - symbol = null; - if (!preErrorSymbol) { - yyleng = this.lexer.yyleng; - yytext = this.lexer.yytext; - yylineno = this.lexer.yylineno; - yyloc = this.lexer.yylloc; - if (recovering > 0) recovering--; - } else { - symbol = preErrorSymbol; - preErrorSymbol = null; - } - break; - case 2: - len = this.productions_[action[1]][1]; - yyval.$ = vstack[vstack.length - len]; - yyval._$ = { first_line: lstack[lstack.length - (len || 1)].first_line, last_line: lstack[lstack.length - 1].last_line, first_column: lstack[lstack.length - (len || 1)].first_column, last_column: lstack[lstack.length - 1].last_column }; - if (ranges) { - yyval._$.range = [lstack[lstack.length - (len || 1)].range[0], lstack[lstack.length - 1].range[1]]; - } - r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack); - if (typeof r !== "undefined") { - return r; - } - if (len) { - stack = stack.slice(0, -1 * len * 2); - vstack = vstack.slice(0, -1 * len); - lstack = lstack.slice(0, -1 * len); - } - stack.push(this.productions_[action[1]][0]); - vstack.push(yyval.$); - lstack.push(yyval._$); - newState = table[stack[stack.length - 2]][stack[stack.length - 1]]; - stack.push(newState); - break; - case 3: - return true; - } - } - return true; - } - }; - /* Jison generated lexer */ - var lexer = (function () { - var lexer = { EOF: 1, - parseError: function parseError(str, hash) { - if (this.yy.parser) { - this.yy.parser.parseError(str, hash); - } else { - throw new Error(str); - } - }, - setInput: function setInput(input) { - this._input = input; - this._more = this._less = this.done = false; - this.yylineno = this.yyleng = 0; - this.yytext = this.matched = this.match = ''; - this.conditionStack = ['INITIAL']; - this.yylloc = { first_line: 1, first_column: 0, last_line: 1, last_column: 0 }; - if (this.options.ranges) this.yylloc.range = [0, 0]; - this.offset = 0; - return this; - }, - input: function input() { - var ch = this._input[0]; - this.yytext += ch; - this.yyleng++; - this.offset++; - this.match += ch; - this.matched += ch; - var lines = ch.match(/(?:\r\n?|\n).*/g); - if (lines) { - this.yylineno++; - this.yylloc.last_line++; - } else { - this.yylloc.last_column++; - } - if (this.options.ranges) this.yylloc.range[1]++; - - this._input = this._input.slice(1); - return ch; - }, - unput: function unput(ch) { - var len = ch.length; - var lines = ch.split(/(?:\r\n?|\n)/g); - - this._input = ch + this._input; - this.yytext = this.yytext.substr(0, this.yytext.length - len - 1); - //this.yyleng -= len; - this.offset -= len; - var oldLines = this.match.split(/(?:\r\n?|\n)/g); - this.match = this.match.substr(0, this.match.length - 1); - this.matched = this.matched.substr(0, this.matched.length - 1); - - if (lines.length - 1) this.yylineno -= lines.length - 1; - var r = this.yylloc.range; - - this.yylloc = { first_line: this.yylloc.first_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.first_column, - last_column: lines ? (lines.length === oldLines.length ? this.yylloc.first_column : 0) + oldLines[oldLines.length - lines.length].length - lines[0].length : this.yylloc.first_column - len - }; - - if (this.options.ranges) { - this.yylloc.range = [r[0], r[0] + this.yyleng - len]; - } - return this; - }, - more: function more() { - this._more = true; - return this; - }, - less: function less(n) { - this.unput(this.match.slice(n)); - }, - pastInput: function pastInput() { - var past = this.matched.substr(0, this.matched.length - this.match.length); - return (past.length > 20 ? '...' : '') + past.substr(-20).replace(/\n/g, ""); - }, - upcomingInput: function upcomingInput() { - var next = this.match; - if (next.length < 20) { - next += this._input.substr(0, 20 - next.length); - } - return (next.substr(0, 20) + (next.length > 20 ? '...' : '')).replace(/\n/g, ""); - }, - showPosition: function showPosition() { - var pre = this.pastInput(); - var c = new Array(pre.length + 1).join("-"); - return pre + this.upcomingInput() + "\n" + c + "^"; - }, - next: function next() { - if (this.done) { - return this.EOF; - } - if (!this._input) this.done = true; - - var token, match, tempMatch, index, col, lines; - if (!this._more) { - this.yytext = ''; - this.match = ''; - } - var rules = this._currentRules(); - for (var i = 0; i < rules.length; i++) { - tempMatch = this._input.match(this.rules[rules[i]]); - if (tempMatch && (!match || tempMatch[0].length > match[0].length)) { - match = tempMatch; - index = i; - if (!this.options.flex) break; - } - } - if (match) { - lines = match[0].match(/(?:\r\n?|\n).*/g); - if (lines) this.yylineno += lines.length; - this.yylloc = { first_line: this.yylloc.last_line, - last_line: this.yylineno + 1, - first_column: this.yylloc.last_column, - last_column: lines ? lines[lines.length - 1].length - lines[lines.length - 1].match(/\r?\n?/)[0].length : this.yylloc.last_column + match[0].length }; - this.yytext += match[0]; - this.match += match[0]; - this.matches = match; - this.yyleng = this.yytext.length; - if (this.options.ranges) { - this.yylloc.range = [this.offset, this.offset += this.yyleng]; - } - this._more = false; - this._input = this._input.slice(match[0].length); - this.matched += match[0]; - token = this.performAction.call(this, this.yy, this, rules[index], this.conditionStack[this.conditionStack.length - 1]); - if (this.done && this._input) this.done = false; - if (token) return token;else return; - } - if (this._input === "") { - return this.EOF; - } else { - return this.parseError('Lexical error on line ' + (this.yylineno + 1) + '. Unrecognized text.\n' + this.showPosition(), { text: "", token: null, line: this.yylineno }); - } - }, - lex: function lex() { - var r = this.next(); - if (typeof r !== 'undefined') { - return r; - } else { - return this.lex(); - } - }, - begin: function begin(condition) { - this.conditionStack.push(condition); - }, - popState: function popState() { - return this.conditionStack.pop(); - }, - _currentRules: function _currentRules() { - return this.conditions[this.conditionStack[this.conditionStack.length - 1]].rules; - }, - topState: function topState() { - return this.conditionStack[this.conditionStack.length - 2]; - }, - pushState: function begin(condition) { - this.begin(condition); - } }; - lexer.options = {}; - lexer.performAction = function anonymous(yy, yy_, $avoiding_name_collisions, YY_START - /**/) { - - function strip(start, end) { - return yy_.yytext = yy_.yytext.substr(start, yy_.yyleng - end); - } - - var YYSTATE = YY_START; - switch ($avoiding_name_collisions) { - case 0: - if (yy_.yytext.slice(-2) === "\\\\") { - strip(0, 1); - this.begin("mu"); - } else if (yy_.yytext.slice(-1) === "\\") { - strip(0, 1); - this.begin("emu"); - } else { - this.begin("mu"); - } - if (yy_.yytext) return 15; - - break; - case 1: - return 15; - break; - case 2: - this.popState(); - return 15; - - break; - case 3: - this.begin('raw');return 15; - break; - case 4: - this.popState(); - // Should be using `this.topState()` below, but it currently - // returns the second top instead of the first top. Opened an - // issue about it at https://github.com/zaach/jison/issues/291 - if (this.conditionStack[this.conditionStack.length - 1] === 'raw') { - return 15; - } else { - yy_.yytext = yy_.yytext.substr(5, yy_.yyleng - 9); - return 'END_RAW_BLOCK'; - } - - break; - case 5: - return 15; - break; - case 6: - this.popState(); - return 14; - - break; - case 7: - return 65; - break; - case 8: - return 68; - break; - case 9: - return 19; - break; - case 10: - this.popState(); - this.begin('raw'); - return 23; - - break; - case 11: - return 55; - break; - case 12: - return 60; - break; - case 13: - return 29; - break; - case 14: - return 47; - break; - case 15: - this.popState();return 44; - break; - case 16: - this.popState();return 44; - break; - case 17: - return 34; - break; - case 18: - return 39; - break; - case 19: - return 51; - break; - case 20: - return 48; - break; - case 21: - this.unput(yy_.yytext); - this.popState(); - this.begin('com'); - - break; - case 22: - this.popState(); - return 14; - - break; - case 23: - return 48; - break; - case 24: - return 73; - break; - case 25: - return 72; - break; - case 26: - return 72; - break; - case 27: - return 87; - break; - case 28: - // ignore whitespace - break; - case 29: - this.popState();return 54; - break; - case 30: - this.popState();return 33; - break; - case 31: - yy_.yytext = strip(1, 2).replace(/\\"/g, '"');return 80; - break; - case 32: - yy_.yytext = strip(1, 2).replace(/\\'/g, "'");return 80; - break; - case 33: - return 85; - break; - case 34: - return 82; - break; - case 35: - return 82; - break; - case 36: - return 83; - break; - case 37: - return 84; - break; - case 38: - return 81; - break; - case 39: - return 75; - break; - case 40: - return 77; - break; - case 41: - return 72; - break; - case 42: - yy_.yytext = yy_.yytext.replace(/\\([\\\]])/g, '$1');return 72; - break; - case 43: - return 'INVALID'; - break; - case 44: - return 5; - break; - } - }; - lexer.rules = [/^(?:[^\x00]*?(?=(\{\{)))/, /^(?:[^\x00]+)/, /^(?:[^\x00]{2,}?(?=(\{\{|\\\{\{|\\\\\{\{|$)))/, /^(?:\{\{\{\{(?=[^\/]))/, /^(?:\{\{\{\{\/[^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=[=}\s\/.])\}\}\}\})/, /^(?:[^\x00]*?(?=(\{\{\{\{)))/, /^(?:[\s\S]*?--(~)?\}\})/, /^(?:\()/, /^(?:\))/, /^(?:\{\{\{\{)/, /^(?:\}\}\}\})/, /^(?:\{\{(~)?>)/, /^(?:\{\{(~)?#>)/, /^(?:\{\{(~)?#\*?)/, /^(?:\{\{(~)?\/)/, /^(?:\{\{(~)?\^\s*(~)?\}\})/, /^(?:\{\{(~)?\s*else\s*(~)?\}\})/, /^(?:\{\{(~)?\^)/, /^(?:\{\{(~)?\s*else\b)/, /^(?:\{\{(~)?\{)/, /^(?:\{\{(~)?&)/, /^(?:\{\{(~)?!--)/, /^(?:\{\{(~)?![\s\S]*?\}\})/, /^(?:\{\{(~)?\*?)/, /^(?:=)/, /^(?:\.\.)/, /^(?:\.(?=([=~}\s\/.)|])))/, /^(?:[\/.])/, /^(?:\s+)/, /^(?:\}(~)?\}\})/, /^(?:(~)?\}\})/, /^(?:"(\\["]|[^"])*")/, /^(?:'(\\[']|[^'])*')/, /^(?:@)/, /^(?:true(?=([~}\s)])))/, /^(?:false(?=([~}\s)])))/, /^(?:undefined(?=([~}\s)])))/, /^(?:null(?=([~}\s)])))/, /^(?:-?[0-9]+(?:\.[0-9]+)?(?=([~}\s)])))/, /^(?:as\s+\|)/, /^(?:\|)/, /^(?:([^\s!"#%-,\.\/;->@\[-\^`\{-~]+(?=([=~}\s\/.)|]))))/, /^(?:\[(\\\]|[^\]])*\])/, /^(?:.)/, /^(?:$)/]; - lexer.conditions = { "mu": { "rules": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44], "inclusive": false }, "emu": { "rules": [2], "inclusive": false }, "com": { "rules": [6], "inclusive": false }, "raw": { "rules": [3, 4, 5], "inclusive": false }, "INITIAL": { "rules": [0, 1, 44], "inclusive": true } }; - return lexer; - })(); - parser.lexer = lexer; - function Parser() { - this.yy = {}; - }Parser.prototype = parser;parser.Parser = Parser; - return new Parser(); - })();exports["default"] = handlebars; - module.exports = exports["default"]; - -/***/ }), -/* 38 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - - var _visitor = __webpack_require__(39); - - var _visitor2 = _interopRequireDefault(_visitor); - - function WhitespaceControl() { - var options = arguments.length <= 0 || arguments[0] === undefined ? {} : arguments[0]; - - this.options = options; - } - WhitespaceControl.prototype = new _visitor2['default'](); - - WhitespaceControl.prototype.Program = function (program) { - var doStandalone = !this.options.ignoreStandalone; - - var isRoot = !this.isRootSeen; - this.isRootSeen = true; - - var body = program.body; - for (var i = 0, l = body.length; i < l; i++) { - var current = body[i], - strip = this.accept(current); - - if (!strip) { - continue; - } - - var _isPrevWhitespace = isPrevWhitespace(body, i, isRoot), - _isNextWhitespace = isNextWhitespace(body, i, isRoot), - openStandalone = strip.openStandalone && _isPrevWhitespace, - closeStandalone = strip.closeStandalone && _isNextWhitespace, - inlineStandalone = strip.inlineStandalone && _isPrevWhitespace && _isNextWhitespace; - - if (strip.close) { - omitRight(body, i, true); - } - if (strip.open) { - omitLeft(body, i, true); - } - - if (doStandalone && inlineStandalone) { - omitRight(body, i); - - if (omitLeft(body, i)) { - // If we are on a standalone node, save the indent info for partials - if (current.type === 'PartialStatement') { - // Pull out the whitespace from the final line - current.indent = /([ \t]+$)/.exec(body[i - 1].original)[1]; - } - } - } - if (doStandalone && openStandalone) { - omitRight((current.program || current.inverse).body); - - // Strip out the previous content node if it's whitespace only - omitLeft(body, i); - } - if (doStandalone && closeStandalone) { - // Always strip the next node - omitRight(body, i); - - omitLeft((current.inverse || current.program).body); - } - } - - return program; - }; - - WhitespaceControl.prototype.BlockStatement = WhitespaceControl.prototype.DecoratorBlock = WhitespaceControl.prototype.PartialBlockStatement = function (block) { - this.accept(block.program); - this.accept(block.inverse); - - // Find the inverse program that is involed with whitespace stripping. - var program = block.program || block.inverse, - inverse = block.program && block.inverse, - firstInverse = inverse, - lastInverse = inverse; - - if (inverse && inverse.chained) { - firstInverse = inverse.body[0].program; - - // Walk the inverse chain to find the last inverse that is actually in the chain. - while (lastInverse.chained) { - lastInverse = lastInverse.body[lastInverse.body.length - 1].program; - } - } - - var strip = { - open: block.openStrip.open, - close: block.closeStrip.close, - - // Determine the standalone candiacy. Basically flag our content as being possibly standalone - // so our parent can determine if we actually are standalone - openStandalone: isNextWhitespace(program.body), - closeStandalone: isPrevWhitespace((firstInverse || program).body) - }; - - if (block.openStrip.close) { - omitRight(program.body, null, true); - } - - if (inverse) { - var inverseStrip = block.inverseStrip; - - if (inverseStrip.open) { - omitLeft(program.body, null, true); - } - - if (inverseStrip.close) { - omitRight(firstInverse.body, null, true); - } - if (block.closeStrip.open) { - omitLeft(lastInverse.body, null, true); - } - - // Find standalone else statments - if (!this.options.ignoreStandalone && isPrevWhitespace(program.body) && isNextWhitespace(firstInverse.body)) { - omitLeft(program.body); - omitRight(firstInverse.body); - } - } else if (block.closeStrip.open) { - omitLeft(program.body, null, true); - } - - return strip; - }; - - WhitespaceControl.prototype.Decorator = WhitespaceControl.prototype.MustacheStatement = function (mustache) { - return mustache.strip; - }; - - WhitespaceControl.prototype.PartialStatement = WhitespaceControl.prototype.CommentStatement = function (node) { - /* istanbul ignore next */ - var strip = node.strip || {}; - return { - inlineStandalone: true, - open: strip.open, - close: strip.close - }; - }; - - function isPrevWhitespace(body, i, isRoot) { - if (i === undefined) { - i = body.length; - } - - // Nodes that end with newlines are considered whitespace (but are special - // cased for strip operations) - var prev = body[i - 1], - sibling = body[i - 2]; - if (!prev) { - return isRoot; - } - - if (prev.type === 'ContentStatement') { - return (sibling || !isRoot ? /\r?\n\s*?$/ : /(^|\r?\n)\s*?$/).test(prev.original); - } - } - function isNextWhitespace(body, i, isRoot) { - if (i === undefined) { - i = -1; - } - - var next = body[i + 1], - sibling = body[i + 2]; - if (!next) { - return isRoot; - } - - if (next.type === 'ContentStatement') { - return (sibling || !isRoot ? /^\s*?\r?\n/ : /^\s*?(\r?\n|$)/).test(next.original); - } - } - - // Marks the node to the right of the position as omitted. - // I.e. {{foo}}' ' will mark the ' ' node as omitted. - // - // If i is undefined, then the first child will be marked as such. - // - // If mulitple is truthy then all whitespace will be stripped out until non-whitespace - // content is met. - function omitRight(body, i, multiple) { - var current = body[i == null ? 0 : i + 1]; - if (!current || current.type !== 'ContentStatement' || !multiple && current.rightStripped) { - return; - } - - var original = current.value; - current.value = current.value.replace(multiple ? /^\s+/ : /^[ \t]*\r?\n?/, ''); - current.rightStripped = current.value !== original; - } - - // Marks the node to the left of the position as omitted. - // I.e. ' '{{foo}} will mark the ' ' node as omitted. - // - // If i is undefined then the last child will be marked as such. - // - // If mulitple is truthy then all whitespace will be stripped out until non-whitespace - // content is met. - function omitLeft(body, i, multiple) { - var current = body[i == null ? body.length - 1 : i - 1]; - if (!current || current.type !== 'ContentStatement' || !multiple && current.leftStripped) { - return; - } - - // We omit the last node if it's whitespace only and not preceeded by a non-content node. - var original = current.value; - current.value = current.value.replace(multiple ? /\s+$/ : /[ \t]+$/, ''); - current.leftStripped = current.value !== original; - return current.leftStripped; - } - - exports['default'] = WhitespaceControl; - module.exports = exports['default']; - -/***/ }), -/* 39 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - function Visitor() { - this.parents = []; - } - - Visitor.prototype = { - constructor: Visitor, - mutating: false, - - // Visits a given value. If mutating, will replace the value if necessary. - acceptKey: function acceptKey(node, name) { - var value = this.accept(node[name]); - if (this.mutating) { - // Hacky sanity check: This may have a few false positives for type for the helper - // methods but will generally do the right thing without a lot of overhead. - if (value && !Visitor.prototype[value.type]) { - throw new _exception2['default']('Unexpected node type "' + value.type + '" found when accepting ' + name + ' on ' + node.type); - } - node[name] = value; - } - }, - - // Performs an accept operation with added sanity check to ensure - // required keys are not removed. - acceptRequired: function acceptRequired(node, name) { - this.acceptKey(node, name); - - if (!node[name]) { - throw new _exception2['default'](node.type + ' requires ' + name); - } - }, - - // Traverses a given array. If mutating, empty respnses will be removed - // for child elements. - acceptArray: function acceptArray(array) { - for (var i = 0, l = array.length; i < l; i++) { - this.acceptKey(array, i); - - if (!array[i]) { - array.splice(i, 1); - i--; - l--; - } - } - }, - - accept: function accept(object) { - if (!object) { - return; - } - - /* istanbul ignore next: Sanity code */ - if (!this[object.type]) { - throw new _exception2['default']('Unknown type: ' + object.type, object); - } - - if (this.current) { - this.parents.unshift(this.current); - } - this.current = object; - - var ret = this[object.type](object); - - this.current = this.parents.shift(); - - if (!this.mutating || ret) { - return ret; - } else if (ret !== false) { - return object; - } - }, - - Program: function Program(program) { - this.acceptArray(program.body); - }, - - MustacheStatement: visitSubExpression, - Decorator: visitSubExpression, - - BlockStatement: visitBlock, - DecoratorBlock: visitBlock, - - PartialStatement: visitPartial, - PartialBlockStatement: function PartialBlockStatement(partial) { - visitPartial.call(this, partial); - - this.acceptKey(partial, 'program'); - }, - - ContentStatement: function ContentStatement() /* content */{}, - CommentStatement: function CommentStatement() /* comment */{}, - - SubExpression: visitSubExpression, - - PathExpression: function PathExpression() /* path */{}, - - StringLiteral: function StringLiteral() /* string */{}, - NumberLiteral: function NumberLiteral() /* number */{}, - BooleanLiteral: function BooleanLiteral() /* bool */{}, - UndefinedLiteral: function UndefinedLiteral() /* literal */{}, - NullLiteral: function NullLiteral() /* literal */{}, - - Hash: function Hash(hash) { - this.acceptArray(hash.pairs); - }, - HashPair: function HashPair(pair) { - this.acceptRequired(pair, 'value'); - } - }; - - function visitSubExpression(mustache) { - this.acceptRequired(mustache, 'path'); - this.acceptArray(mustache.params); - this.acceptKey(mustache, 'hash'); - } - function visitBlock(block) { - visitSubExpression.call(this, block); - - this.acceptKey(block, 'program'); - this.acceptKey(block, 'inverse'); - } - function visitPartial(partial) { - this.acceptRequired(partial, 'name'); - this.acceptArray(partial.params); - this.acceptKey(partial, 'hash'); - } - - exports['default'] = Visitor; - module.exports = exports['default']; - -/***/ }), -/* 40 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - exports.SourceLocation = SourceLocation; - exports.id = id; - exports.stripFlags = stripFlags; - exports.stripComment = stripComment; - exports.preparePath = preparePath; - exports.prepareMustache = prepareMustache; - exports.prepareRawBlock = prepareRawBlock; - exports.prepareBlock = prepareBlock; - exports.prepareProgram = prepareProgram; - exports.preparePartialBlock = preparePartialBlock; - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - function validateClose(open, close) { - close = close.path ? close.path.original : close; - - if (open.path.original !== close) { - var errorNode = { loc: open.path.loc }; - - throw new _exception2['default'](open.path.original + " doesn't match " + close, errorNode); - } - } - - function SourceLocation(source, locInfo) { - this.source = source; - this.start = { - line: locInfo.first_line, - column: locInfo.first_column - }; - this.end = { - line: locInfo.last_line, - column: locInfo.last_column - }; - } - - function id(token) { - if (/^\[.*\]$/.test(token)) { - return token.substr(1, token.length - 2); - } else { - return token; - } - } - - function stripFlags(open, close) { - return { - open: open.charAt(2) === '~', - close: close.charAt(close.length - 3) === '~' - }; - } - - function stripComment(comment) { - return comment.replace(/^\{\{~?\!-?-?/, '').replace(/-?-?~?\}\}$/, ''); - } - - function preparePath(data, parts, loc) { - loc = this.locInfo(loc); - - var original = data ? '@' : '', - dig = [], - depth = 0, - depthString = ''; - - for (var i = 0, l = parts.length; i < l; i++) { - var part = parts[i].part, - - // If we have [] syntax then we do not treat path references as operators, - // i.e. foo.[this] resolves to approximately context.foo['this'] - isLiteral = parts[i].original !== part; - original += (parts[i].separator || '') + part; - - if (!isLiteral && (part === '..' || part === '.' || part === 'this')) { - if (dig.length > 0) { - throw new _exception2['default']('Invalid path: ' + original, { loc: loc }); - } else if (part === '..') { - depth++; - depthString += '../'; - } - } else { - dig.push(part); - } - } - - return { - type: 'PathExpression', - data: data, - depth: depth, - parts: dig, - original: original, - loc: loc - }; - } - - function prepareMustache(path, params, hash, open, strip, locInfo) { - // Must use charAt to support IE pre-10 - var escapeFlag = open.charAt(3) || open.charAt(2), - escaped = escapeFlag !== '{' && escapeFlag !== '&'; - - var decorator = /\*/.test(open); - return { - type: decorator ? 'Decorator' : 'MustacheStatement', - path: path, - params: params, - hash: hash, - escaped: escaped, - strip: strip, - loc: this.locInfo(locInfo) - }; - } - - function prepareRawBlock(openRawBlock, contents, close, locInfo) { - validateClose(openRawBlock, close); - - locInfo = this.locInfo(locInfo); - var program = { - type: 'Program', - body: contents, - strip: {}, - loc: locInfo - }; - - return { - type: 'BlockStatement', - path: openRawBlock.path, - params: openRawBlock.params, - hash: openRawBlock.hash, - program: program, - openStrip: {}, - inverseStrip: {}, - closeStrip: {}, - loc: locInfo - }; - } - - function prepareBlock(openBlock, program, inverseAndProgram, close, inverted, locInfo) { - if (close && close.path) { - validateClose(openBlock, close); - } - - var decorator = /\*/.test(openBlock.open); - - program.blockParams = openBlock.blockParams; - - var inverse = undefined, - inverseStrip = undefined; - - if (inverseAndProgram) { - if (decorator) { - throw new _exception2['default']('Unexpected inverse block on decorator', inverseAndProgram); - } - - if (inverseAndProgram.chain) { - inverseAndProgram.program.body[0].closeStrip = close.strip; - } - - inverseStrip = inverseAndProgram.strip; - inverse = inverseAndProgram.program; - } - - if (inverted) { - inverted = inverse; - inverse = program; - program = inverted; - } - - return { - type: decorator ? 'DecoratorBlock' : 'BlockStatement', - path: openBlock.path, - params: openBlock.params, - hash: openBlock.hash, - program: program, - inverse: inverse, - openStrip: openBlock.strip, - inverseStrip: inverseStrip, - closeStrip: close && close.strip, - loc: this.locInfo(locInfo) - }; - } - - function prepareProgram(statements, loc) { - if (!loc && statements.length) { - var firstLoc = statements[0].loc, - lastLoc = statements[statements.length - 1].loc; - - /* istanbul ignore else */ - if (firstLoc && lastLoc) { - loc = { - source: firstLoc.source, - start: { - line: firstLoc.start.line, - column: firstLoc.start.column - }, - end: { - line: lastLoc.end.line, - column: lastLoc.end.column - } - }; - } - } - - return { - type: 'Program', - body: statements, - strip: {}, - loc: loc - }; - } - - function preparePartialBlock(open, program, close, locInfo) { - validateClose(open, close); - - return { - type: 'PartialBlockStatement', - name: open.path, - params: open.params, - hash: open.hash, - program: program, - openStrip: open.strip, - closeStrip: close && close.strip, - loc: this.locInfo(locInfo) - }; - } - -/***/ }), -/* 41 */ -/***/ (function(module, exports, __webpack_require__) { - - /* eslint-disable new-cap */ - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - exports.Compiler = Compiler; - exports.precompile = precompile; - exports.compile = compile; - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - var _utils = __webpack_require__(5); - - var _ast = __webpack_require__(35); - - var _ast2 = _interopRequireDefault(_ast); - - var slice = [].slice; - - function Compiler() {} - - // the foundHelper register will disambiguate helper lookup from finding a - // function in a context. This is necessary for mustache compatibility, which - // requires that context functions in blocks are evaluated by blockHelperMissing, - // and then proceed as if the resulting value was provided to blockHelperMissing. - - Compiler.prototype = { - compiler: Compiler, - - equals: function equals(other) { - var len = this.opcodes.length; - if (other.opcodes.length !== len) { - return false; - } - - for (var i = 0; i < len; i++) { - var opcode = this.opcodes[i], - otherOpcode = other.opcodes[i]; - if (opcode.opcode !== otherOpcode.opcode || !argEquals(opcode.args, otherOpcode.args)) { - return false; - } - } - - // We know that length is the same between the two arrays because they are directly tied - // to the opcode behavior above. - len = this.children.length; - for (var i = 0; i < len; i++) { - if (!this.children[i].equals(other.children[i])) { - return false; - } - } - - return true; - }, - - guid: 0, - - compile: function compile(program, options) { - this.sourceNode = []; - this.opcodes = []; - this.children = []; - this.options = options; - this.stringParams = options.stringParams; - this.trackIds = options.trackIds; - - options.blockParams = options.blockParams || []; - - // These changes will propagate to the other compiler components - var knownHelpers = options.knownHelpers; - options.knownHelpers = { - 'helperMissing': true, - 'blockHelperMissing': true, - 'each': true, - 'if': true, - 'unless': true, - 'with': true, - 'log': true, - 'lookup': true - }; - if (knownHelpers) { - for (var _name in knownHelpers) { - /* istanbul ignore else */ - if (_name in knownHelpers) { - this.options.knownHelpers[_name] = knownHelpers[_name]; - } - } - } - - return this.accept(program); - }, - - compileProgram: function compileProgram(program) { - var childCompiler = new this.compiler(), - // eslint-disable-line new-cap - result = childCompiler.compile(program, this.options), - guid = this.guid++; - - this.usePartial = this.usePartial || result.usePartial; - - this.children[guid] = result; - this.useDepths = this.useDepths || result.useDepths; - - return guid; - }, - - accept: function accept(node) { - /* istanbul ignore next: Sanity code */ - if (!this[node.type]) { - throw new _exception2['default']('Unknown type: ' + node.type, node); - } - - this.sourceNode.unshift(node); - var ret = this[node.type](node); - this.sourceNode.shift(); - return ret; - }, - - Program: function Program(program) { - this.options.blockParams.unshift(program.blockParams); - - var body = program.body, - bodyLength = body.length; - for (var i = 0; i < bodyLength; i++) { - this.accept(body[i]); - } - - this.options.blockParams.shift(); - - this.isSimple = bodyLength === 1; - this.blockParams = program.blockParams ? program.blockParams.length : 0; - - return this; - }, - - BlockStatement: function BlockStatement(block) { - transformLiteralToPath(block); - - var program = block.program, - inverse = block.inverse; - - program = program && this.compileProgram(program); - inverse = inverse && this.compileProgram(inverse); - - var type = this.classifySexpr(block); - - if (type === 'helper') { - this.helperSexpr(block, program, inverse); - } else if (type === 'simple') { - this.simpleSexpr(block); - - // now that the simple mustache is resolved, we need to - // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('blockValue', block.path.original); - } else { - this.ambiguousSexpr(block, program, inverse); - - // now that the simple mustache is resolved, we need to - // evaluate it by executing `blockHelperMissing` - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - this.opcode('emptyHash'); - this.opcode('ambiguousBlockValue'); - } - - this.opcode('append'); - }, - - DecoratorBlock: function DecoratorBlock(decorator) { - var program = decorator.program && this.compileProgram(decorator.program); - var params = this.setupFullMustacheParams(decorator, program, undefined), - path = decorator.path; - - this.useDecorators = true; - this.opcode('registerDecorator', params.length, path.original); - }, - - PartialStatement: function PartialStatement(partial) { - this.usePartial = true; - - var program = partial.program; - if (program) { - program = this.compileProgram(partial.program); - } - - var params = partial.params; - if (params.length > 1) { - throw new _exception2['default']('Unsupported number of partial arguments: ' + params.length, partial); - } else if (!params.length) { - if (this.options.explicitPartialContext) { - this.opcode('pushLiteral', 'undefined'); - } else { - params.push({ type: 'PathExpression', parts: [], depth: 0 }); - } - } - - var partialName = partial.name.original, - isDynamic = partial.name.type === 'SubExpression'; - if (isDynamic) { - this.accept(partial.name); - } - - this.setupFullMustacheParams(partial, program, undefined, true); - - var indent = partial.indent || ''; - if (this.options.preventIndent && indent) { - this.opcode('appendContent', indent); - indent = ''; - } - - this.opcode('invokePartial', isDynamic, partialName, indent); - this.opcode('append'); - }, - PartialBlockStatement: function PartialBlockStatement(partialBlock) { - this.PartialStatement(partialBlock); - }, - - MustacheStatement: function MustacheStatement(mustache) { - this.SubExpression(mustache); - - if (mustache.escaped && !this.options.noEscape) { - this.opcode('appendEscaped'); - } else { - this.opcode('append'); - } - }, - Decorator: function Decorator(decorator) { - this.DecoratorBlock(decorator); - }, - - ContentStatement: function ContentStatement(content) { - if (content.value) { - this.opcode('appendContent', content.value); - } - }, - - CommentStatement: function CommentStatement() {}, - - SubExpression: function SubExpression(sexpr) { - transformLiteralToPath(sexpr); - var type = this.classifySexpr(sexpr); - - if (type === 'simple') { - this.simpleSexpr(sexpr); - } else if (type === 'helper') { - this.helperSexpr(sexpr); - } else { - this.ambiguousSexpr(sexpr); - } - }, - ambiguousSexpr: function ambiguousSexpr(sexpr, program, inverse) { - var path = sexpr.path, - name = path.parts[0], - isBlock = program != null || inverse != null; - - this.opcode('getContext', path.depth); - - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - - path.strict = true; - this.accept(path); - - this.opcode('invokeAmbiguous', name, isBlock); - }, - - simpleSexpr: function simpleSexpr(sexpr) { - var path = sexpr.path; - path.strict = true; - this.accept(path); - this.opcode('resolvePossibleLambda'); - }, - - helperSexpr: function helperSexpr(sexpr, program, inverse) { - var params = this.setupFullMustacheParams(sexpr, program, inverse), - path = sexpr.path, - name = path.parts[0]; - - if (this.options.knownHelpers[name]) { - this.opcode('invokeKnownHelper', params.length, name); - } else if (this.options.knownHelpersOnly) { - throw new _exception2['default']('You specified knownHelpersOnly, but used the unknown helper ' + name, sexpr); - } else { - path.strict = true; - path.falsy = true; - - this.accept(path); - this.opcode('invokeHelper', params.length, path.original, _ast2['default'].helpers.simpleId(path)); - } - }, - - PathExpression: function PathExpression(path) { - this.addDepth(path.depth); - this.opcode('getContext', path.depth); - - var name = path.parts[0], - scoped = _ast2['default'].helpers.scopedId(path), - blockParamId = !path.depth && !scoped && this.blockParamIndex(name); - - if (blockParamId) { - this.opcode('lookupBlockParam', blockParamId, path.parts); - } else if (!name) { - // Context reference, i.e. `{{foo .}}` or `{{foo ..}}` - this.opcode('pushContext'); - } else if (path.data) { - this.options.data = true; - this.opcode('lookupData', path.depth, path.parts, path.strict); - } else { - this.opcode('lookupOnContext', path.parts, path.falsy, path.strict, scoped); - } - }, - - StringLiteral: function StringLiteral(string) { - this.opcode('pushString', string.value); - }, - - NumberLiteral: function NumberLiteral(number) { - this.opcode('pushLiteral', number.value); - }, - - BooleanLiteral: function BooleanLiteral(bool) { - this.opcode('pushLiteral', bool.value); - }, - - UndefinedLiteral: function UndefinedLiteral() { - this.opcode('pushLiteral', 'undefined'); - }, - - NullLiteral: function NullLiteral() { - this.opcode('pushLiteral', 'null'); - }, - - Hash: function Hash(hash) { - var pairs = hash.pairs, - i = 0, - l = pairs.length; - - this.opcode('pushHash'); - - for (; i < l; i++) { - this.pushParam(pairs[i].value); - } - while (i--) { - this.opcode('assignToHash', pairs[i].key); - } - this.opcode('popHash'); - }, - - // HELPERS - opcode: function opcode(name) { - this.opcodes.push({ opcode: name, args: slice.call(arguments, 1), loc: this.sourceNode[0].loc }); - }, - - addDepth: function addDepth(depth) { - if (!depth) { - return; - } - - this.useDepths = true; - }, - - classifySexpr: function classifySexpr(sexpr) { - var isSimple = _ast2['default'].helpers.simpleId(sexpr.path); - - var isBlockParam = isSimple && !!this.blockParamIndex(sexpr.path.parts[0]); - - // a mustache is an eligible helper if: - // * its id is simple (a single part, not `this` or `..`) - var isHelper = !isBlockParam && _ast2['default'].helpers.helperExpression(sexpr); - - // if a mustache is an eligible helper but not a definite - // helper, it is ambiguous, and will be resolved in a later - // pass or at runtime. - var isEligible = !isBlockParam && (isHelper || isSimple); - - // if ambiguous, we can possibly resolve the ambiguity now - // An eligible helper is one that does not have a complex path, i.e. `this.foo`, `../foo` etc. - if (isEligible && !isHelper) { - var _name2 = sexpr.path.parts[0], - options = this.options; - - if (options.knownHelpers[_name2]) { - isHelper = true; - } else if (options.knownHelpersOnly) { - isEligible = false; - } - } - - if (isHelper) { - return 'helper'; - } else if (isEligible) { - return 'ambiguous'; - } else { - return 'simple'; - } - }, - - pushParams: function pushParams(params) { - for (var i = 0, l = params.length; i < l; i++) { - this.pushParam(params[i]); - } - }, - - pushParam: function pushParam(val) { - var value = val.value != null ? val.value : val.original || ''; - - if (this.stringParams) { - if (value.replace) { - value = value.replace(/^(\.?\.\/)*/g, '').replace(/\//g, '.'); - } - - if (val.depth) { - this.addDepth(val.depth); - } - this.opcode('getContext', val.depth || 0); - this.opcode('pushStringParam', value, val.type); - - if (val.type === 'SubExpression') { - // SubExpressions get evaluated and passed in - // in string params mode. - this.accept(val); - } - } else { - if (this.trackIds) { - var blockParamIndex = undefined; - if (val.parts && !_ast2['default'].helpers.scopedId(val) && !val.depth) { - blockParamIndex = this.blockParamIndex(val.parts[0]); - } - if (blockParamIndex) { - var blockParamChild = val.parts.slice(1).join('.'); - this.opcode('pushId', 'BlockParam', blockParamIndex, blockParamChild); - } else { - value = val.original || value; - if (value.replace) { - value = value.replace(/^this(?:\.|$)/, '').replace(/^\.\//, '').replace(/^\.$/, ''); - } - - this.opcode('pushId', val.type, value); - } - } - this.accept(val); - } - }, - - setupFullMustacheParams: function setupFullMustacheParams(sexpr, program, inverse, omitEmpty) { - var params = sexpr.params; - this.pushParams(params); - - this.opcode('pushProgram', program); - this.opcode('pushProgram', inverse); - - if (sexpr.hash) { - this.accept(sexpr.hash); - } else { - this.opcode('emptyHash', omitEmpty); - } - - return params; - }, - - blockParamIndex: function blockParamIndex(name) { - for (var depth = 0, len = this.options.blockParams.length; depth < len; depth++) { - var blockParams = this.options.blockParams[depth], - param = blockParams && _utils.indexOf(blockParams, name); - if (blockParams && param >= 0) { - return [depth, param]; - } - } - } - }; - - function precompile(input, options, env) { - if (input == null || typeof input !== 'string' && input.type !== 'Program') { - throw new _exception2['default']('You must pass a string or Handlebars AST to Handlebars.precompile. You passed ' + input); - } - - options = options || {}; - if (!('data' in options)) { - options.data = true; - } - if (options.compat) { - options.useDepths = true; - } - - var ast = env.parse(input, options), - environment = new env.Compiler().compile(ast, options); - return new env.JavaScriptCompiler().compile(environment, options); - } - - function compile(input, options, env) { - if (options === undefined) options = {}; - - if (input == null || typeof input !== 'string' && input.type !== 'Program') { - throw new _exception2['default']('You must pass a string or Handlebars AST to Handlebars.compile. You passed ' + input); - } - - options = _utils.extend({}, options); - if (!('data' in options)) { - options.data = true; - } - if (options.compat) { - options.useDepths = true; - } - - var compiled = undefined; - - function compileInput() { - var ast = env.parse(input, options), - environment = new env.Compiler().compile(ast, options), - templateSpec = new env.JavaScriptCompiler().compile(environment, options, undefined, true); - return env.template(templateSpec); - } - - // Template is only compiled on first use and cached after that point. - function ret(context, execOptions) { - if (!compiled) { - compiled = compileInput(); - } - return compiled.call(this, context, execOptions); - } - ret._setup = function (setupOptions) { - if (!compiled) { - compiled = compileInput(); - } - return compiled._setup(setupOptions); - }; - ret._child = function (i, data, blockParams, depths) { - if (!compiled) { - compiled = compileInput(); - } - return compiled._child(i, data, blockParams, depths); - }; - return ret; - } - - function argEquals(a, b) { - if (a === b) { - return true; - } - - if (_utils.isArray(a) && _utils.isArray(b) && a.length === b.length) { - for (var i = 0; i < a.length; i++) { - if (!argEquals(a[i], b[i])) { - return false; - } - } - return true; - } - } - - function transformLiteralToPath(sexpr) { - if (!sexpr.path.parts) { - var literal = sexpr.path; - // Casting to string here to make false and 0 literal values play nicely with the rest - // of the system. - sexpr.path = { - type: 'PathExpression', - data: false, - depth: 0, - parts: [literal.original + ''], - original: literal.original + '', - loc: literal.loc - }; - } - } - -/***/ }), -/* 42 */ -/***/ (function(module, exports, __webpack_require__) { - - 'use strict'; - - var _interopRequireDefault = __webpack_require__(1)['default']; - - exports.__esModule = true; - - var _base = __webpack_require__(4); - - var _exception = __webpack_require__(6); - - var _exception2 = _interopRequireDefault(_exception); - - var _utils = __webpack_require__(5); - - var _codeGen = __webpack_require__(43); - - var _codeGen2 = _interopRequireDefault(_codeGen); - - function Literal(value) { - this.value = value; - } - - function JavaScriptCompiler() {} - - JavaScriptCompiler.prototype = { - // PUBLIC API: You can override these methods in a subclass to provide - // alternative compiled forms for name lookup and buffering semantics - nameLookup: function nameLookup(parent, name /* , type*/) { - if (JavaScriptCompiler.isValidJavaScriptVariableName(name)) { - return [parent, '.', name]; - } else { - return [parent, '[', JSON.stringify(name), ']']; - } - }, - depthedLookup: function depthedLookup(name) { - return [this.aliasable('container.lookup'), '(depths, "', name, '")']; - }, - - compilerInfo: function compilerInfo() { - var revision = _base.COMPILER_REVISION, - versions = _base.REVISION_CHANGES[revision]; - return [revision, versions]; - }, - - appendToBuffer: function appendToBuffer(source, location, explicit) { - // Force a source as this simplifies the merge logic. - if (!_utils.isArray(source)) { - source = [source]; - } - source = this.source.wrap(source, location); - - if (this.environment.isSimple) { - return ['return ', source, ';']; - } else if (explicit) { - // This is a case where the buffer operation occurs as a child of another - // construct, generally braces. We have to explicitly output these buffer - // operations to ensure that the emitted code goes in the correct location. - return ['buffer += ', source, ';']; - } else { - source.appendToBuffer = true; - return source; - } - }, - - initializeBuffer: function initializeBuffer() { - return this.quotedString(''); - }, - // END PUBLIC API - - compile: function compile(environment, options, context, asObject) { - this.environment = environment; - this.options = options; - this.stringParams = this.options.stringParams; - this.trackIds = this.options.trackIds; - this.precompile = !asObject; - - this.name = this.environment.name; - this.isChild = !!context; - this.context = context || { - decorators: [], - programs: [], - environments: [] - }; - - this.preamble(); - - this.stackSlot = 0; - this.stackVars = []; - this.aliases = {}; - this.registers = { list: [] }; - this.hashes = []; - this.compileStack = []; - this.inlineStack = []; - this.blockParams = []; - - this.compileChildren(environment, options); - - this.useDepths = this.useDepths || environment.useDepths || environment.useDecorators || this.options.compat; - this.useBlockParams = this.useBlockParams || environment.useBlockParams; - - var opcodes = environment.opcodes, - opcode = undefined, - firstLoc = undefined, - i = undefined, - l = undefined; - - for (i = 0, l = opcodes.length; i < l; i++) { - opcode = opcodes[i]; - - this.source.currentLocation = opcode.loc; - firstLoc = firstLoc || opcode.loc; - this[opcode.opcode].apply(this, opcode.args); - } - - // Flush any trailing content that might be pending. - this.source.currentLocation = firstLoc; - this.pushSource(''); - - /* istanbul ignore next */ - if (this.stackSlot || this.inlineStack.length || this.compileStack.length) { - throw new _exception2['default']('Compile completed with content left on stack'); - } - - if (!this.decorators.isEmpty()) { - this.useDecorators = true; - - this.decorators.prepend('var decorators = container.decorators;\n'); - this.decorators.push('return fn;'); - - if (asObject) { - this.decorators = Function.apply(this, ['fn', 'props', 'container', 'depth0', 'data', 'blockParams', 'depths', this.decorators.merge()]); - } else { - this.decorators.prepend('function(fn, props, container, depth0, data, blockParams, depths) {\n'); - this.decorators.push('}\n'); - this.decorators = this.decorators.merge(); - } - } else { - this.decorators = undefined; - } - - var fn = this.createFunctionContext(asObject); - if (!this.isChild) { - var ret = { - compiler: this.compilerInfo(), - main: fn - }; - - if (this.decorators) { - ret.main_d = this.decorators; // eslint-disable-line camelcase - ret.useDecorators = true; - } - - var _context = this.context; - var programs = _context.programs; - var decorators = _context.decorators; - - for (i = 0, l = programs.length; i < l; i++) { - if (programs[i]) { - ret[i] = programs[i]; - if (decorators[i]) { - ret[i + '_d'] = decorators[i]; - ret.useDecorators = true; - } - } - } - - if (this.environment.usePartial) { - ret.usePartial = true; - } - if (this.options.data) { - ret.useData = true; - } - if (this.useDepths) { - ret.useDepths = true; - } - if (this.useBlockParams) { - ret.useBlockParams = true; - } - if (this.options.compat) { - ret.compat = true; - } - - if (!asObject) { - ret.compiler = JSON.stringify(ret.compiler); - - this.source.currentLocation = { start: { line: 1, column: 0 } }; - ret = this.objectLiteral(ret); - - if (options.srcName) { - ret = ret.toStringWithSourceMap({ file: options.destName }); - ret.map = ret.map && ret.map.toString(); - } else { - ret = ret.toString(); - } - } else { - ret.compilerOptions = this.options; - } - - return ret; - } else { - return fn; - } - }, - - preamble: function preamble() { - // track the last context pushed into place to allow skipping the - // getContext opcode when it would be a noop - this.lastContext = 0; - this.source = new _codeGen2['default'](this.options.srcName); - this.decorators = new _codeGen2['default'](this.options.srcName); - }, - - createFunctionContext: function createFunctionContext(asObject) { - var varDeclarations = ''; - - var locals = this.stackVars.concat(this.registers.list); - if (locals.length > 0) { - varDeclarations += ', ' + locals.join(', '); - } - - // Generate minimizer alias mappings - // - // When using true SourceNodes, this will update all references to the given alias - // as the source nodes are reused in situ. For the non-source node compilation mode, - // aliases will not be used, but this case is already being run on the client and - // we aren't concern about minimizing the template size. - var aliasCount = 0; - for (var alias in this.aliases) { - // eslint-disable-line guard-for-in - var node = this.aliases[alias]; - - if (this.aliases.hasOwnProperty(alias) && node.children && node.referenceCount > 1) { - varDeclarations += ', alias' + ++aliasCount + '=' + alias; - node.children[0] = 'alias' + aliasCount; - } - } - - var params = ['container', 'depth0', 'helpers', 'partials', 'data']; - - if (this.useBlockParams || this.useDepths) { - params.push('blockParams'); - } - if (this.useDepths) { - params.push('depths'); - } - - // Perform a second pass over the output to merge content when possible - var source = this.mergeSource(varDeclarations); - - if (asObject) { - params.push(source); - - return Function.apply(this, params); - } else { - return this.source.wrap(['function(', params.join(','), ') {\n ', source, '}']); - } - }, - mergeSource: function mergeSource(varDeclarations) { - var isSimple = this.environment.isSimple, - appendOnly = !this.forceBuffer, - appendFirst = undefined, - sourceSeen = undefined, - bufferStart = undefined, - bufferEnd = undefined; - this.source.each(function (line) { - if (line.appendToBuffer) { - if (bufferStart) { - line.prepend(' + '); - } else { - bufferStart = line; - } - bufferEnd = line; - } else { - if (bufferStart) { - if (!sourceSeen) { - appendFirst = true; - } else { - bufferStart.prepend('buffer += '); - } - bufferEnd.add(';'); - bufferStart = bufferEnd = undefined; - } - - sourceSeen = true; - if (!isSimple) { - appendOnly = false; - } - } - }); - - if (appendOnly) { - if (bufferStart) { - bufferStart.prepend('return '); - bufferEnd.add(';'); - } else if (!sourceSeen) { - this.source.push('return "";'); - } - } else { - varDeclarations += ', buffer = ' + (appendFirst ? '' : this.initializeBuffer()); - - if (bufferStart) { - bufferStart.prepend('return buffer + '); - bufferEnd.add(';'); - } else { - this.source.push('return buffer;'); - } - } - - if (varDeclarations) { - this.source.prepend('var ' + varDeclarations.substring(2) + (appendFirst ? '' : ';\n')); - } - - return this.source.merge(); - }, - - // [blockValue] - // - // On stack, before: hash, inverse, program, value - // On stack, after: return value of blockHelperMissing - // - // The purpose of this opcode is to take a block of the form - // `{{#this.foo}}...{{/this.foo}}`, resolve the value of `foo`, and - // replace it on the stack with the result of properly - // invoking blockHelperMissing. - blockValue: function blockValue(name) { - var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), - params = [this.contextName(0)]; - this.setupHelperArgs(name, 0, params); - - var blockName = this.popStack(); - params.splice(1, 0, blockName); - - this.push(this.source.functionCall(blockHelperMissing, 'call', params)); - }, - - // [ambiguousBlockValue] - // - // On stack, before: hash, inverse, program, value - // Compiler value, before: lastHelper=value of last found helper, if any - // On stack, after, if no lastHelper: same as [blockValue] - // On stack, after, if lastHelper: value - ambiguousBlockValue: function ambiguousBlockValue() { - // We're being a bit cheeky and reusing the options value from the prior exec - var blockHelperMissing = this.aliasable('helpers.blockHelperMissing'), - params = [this.contextName(0)]; - this.setupHelperArgs('', 0, params, true); - - this.flushInline(); - - var current = this.topStack(); - params.splice(1, 0, current); - - this.pushSource(['if (!', this.lastHelper, ') { ', current, ' = ', this.source.functionCall(blockHelperMissing, 'call', params), '}']); - }, - - // [appendContent] - // - // On stack, before: ... - // On stack, after: ... - // - // Appends the string value of `content` to the current buffer - appendContent: function appendContent(content) { - if (this.pendingContent) { - content = this.pendingContent + content; - } else { - this.pendingLocation = this.source.currentLocation; - } - - this.pendingContent = content; - }, - - // [append] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Coerces `value` to a String and appends it to the current buffer. - // - // If `value` is truthy, or 0, it is coerced into a string and appended - // Otherwise, the empty string is appended - append: function append() { - if (this.isInline()) { - this.replaceStack(function (current) { - return [' != null ? ', current, ' : ""']; - }); - - this.pushSource(this.appendToBuffer(this.popStack())); - } else { - var local = this.popStack(); - this.pushSource(['if (', local, ' != null) { ', this.appendToBuffer(local, undefined, true), ' }']); - if (this.environment.isSimple) { - this.pushSource(['else { ', this.appendToBuffer("''", undefined, true), ' }']); - } - } - }, - - // [appendEscaped] - // - // On stack, before: value, ... - // On stack, after: ... - // - // Escape `value` and append it to the buffer - appendEscaped: function appendEscaped() { - this.pushSource(this.appendToBuffer([this.aliasable('container.escapeExpression'), '(', this.popStack(), ')'])); - }, - - // [getContext] - // - // On stack, before: ... - // On stack, after: ... - // Compiler value, after: lastContext=depth - // - // Set the value of the `lastContext` compiler value to the depth - getContext: function getContext(depth) { - this.lastContext = depth; - }, - - // [pushContext] - // - // On stack, before: ... - // On stack, after: currentContext, ... - // - // Pushes the value of the current context onto the stack. - pushContext: function pushContext() { - this.pushStackLiteral(this.contextName(this.lastContext)); - }, - - // [lookupOnContext] - // - // On stack, before: ... - // On stack, after: currentContext[name], ... - // - // Looks up the value of `name` on the current context and pushes - // it onto the stack. - lookupOnContext: function lookupOnContext(parts, falsy, strict, scoped) { - var i = 0; - - if (!scoped && this.options.compat && !this.lastContext) { - // The depthed query is expected to handle the undefined logic for the root level that - // is implemented below, so we evaluate that directly in compat mode - this.push(this.depthedLookup(parts[i++])); - } else { - this.pushContext(); - } - - this.resolvePath('context', parts, i, falsy, strict); - }, - - // [lookupBlockParam] - // - // On stack, before: ... - // On stack, after: blockParam[name], ... - // - // Looks up the value of `parts` on the given block param and pushes - // it onto the stack. - lookupBlockParam: function lookupBlockParam(blockParamId, parts) { - this.useBlockParams = true; - - this.push(['blockParams[', blockParamId[0], '][', blockParamId[1], ']']); - this.resolvePath('context', parts, 1); - }, - - // [lookupData] - // - // On stack, before: ... - // On stack, after: data, ... - // - // Push the data lookup operator - lookupData: function lookupData(depth, parts, strict) { - if (!depth) { - this.pushStackLiteral('data'); - } else { - this.pushStackLiteral('container.data(data, ' + depth + ')'); - } - - this.resolvePath('data', parts, 0, true, strict); - }, - - resolvePath: function resolvePath(type, parts, i, falsy, strict) { - // istanbul ignore next - - var _this = this; - - if (this.options.strict || this.options.assumeObjects) { - this.push(strictLookup(this.options.strict && strict, this, parts, type)); - return; - } - - var len = parts.length; - for (; i < len; i++) { - /* eslint-disable no-loop-func */ - this.replaceStack(function (current) { - var lookup = _this.nameLookup(current, parts[i], type); - // We want to ensure that zero and false are handled properly if the context (falsy flag) - // needs to have the special handling for these values. - if (!falsy) { - return [' != null ? ', lookup, ' : ', current]; - } else { - // Otherwise we can use generic falsy handling - return [' && ', lookup]; - } - }); - /* eslint-enable no-loop-func */ - } - }, - - // [resolvePossibleLambda] - // - // On stack, before: value, ... - // On stack, after: resolved value, ... - // - // If the `value` is a lambda, replace it on the stack by - // the return value of the lambda - resolvePossibleLambda: function resolvePossibleLambda() { - this.push([this.aliasable('container.lambda'), '(', this.popStack(), ', ', this.contextName(0), ')']); - }, - - // [pushStringParam] - // - // On stack, before: ... - // On stack, after: string, currentContext, ... - // - // This opcode is designed for use in string mode, which - // provides the string value of a parameter along with its - // depth rather than resolving it immediately. - pushStringParam: function pushStringParam(string, type) { - this.pushContext(); - this.pushString(type); - - // If it's a subexpression, the string result - // will be pushed after this opcode. - if (type !== 'SubExpression') { - if (typeof string === 'string') { - this.pushString(string); - } else { - this.pushStackLiteral(string); - } - } - }, - - emptyHash: function emptyHash(omitEmpty) { - if (this.trackIds) { - this.push('{}'); // hashIds - } - if (this.stringParams) { - this.push('{}'); // hashContexts - this.push('{}'); // hashTypes - } - this.pushStackLiteral(omitEmpty ? 'undefined' : '{}'); - }, - pushHash: function pushHash() { - if (this.hash) { - this.hashes.push(this.hash); - } - this.hash = { values: [], types: [], contexts: [], ids: [] }; - }, - popHash: function popHash() { - var hash = this.hash; - this.hash = this.hashes.pop(); - - if (this.trackIds) { - this.push(this.objectLiteral(hash.ids)); - } - if (this.stringParams) { - this.push(this.objectLiteral(hash.contexts)); - this.push(this.objectLiteral(hash.types)); - } - - this.push(this.objectLiteral(hash.values)); - }, - - // [pushString] - // - // On stack, before: ... - // On stack, after: quotedString(string), ... - // - // Push a quoted version of `string` onto the stack - pushString: function pushString(string) { - this.pushStackLiteral(this.quotedString(string)); - }, - - // [pushLiteral] - // - // On stack, before: ... - // On stack, after: value, ... - // - // Pushes a value onto the stack. This operation prevents - // the compiler from creating a temporary variable to hold - // it. - pushLiteral: function pushLiteral(value) { - this.pushStackLiteral(value); - }, - - // [pushProgram] - // - // On stack, before: ... - // On stack, after: program(guid), ... - // - // Push a program expression onto the stack. This takes - // a compile-time guid and converts it into a runtime-accessible - // expression. - pushProgram: function pushProgram(guid) { - if (guid != null) { - this.pushStackLiteral(this.programExpression(guid)); - } else { - this.pushStackLiteral(null); - } - }, - - // [registerDecorator] - // - // On stack, before: hash, program, params..., ... - // On stack, after: ... - // - // Pops off the decorator's parameters, invokes the decorator, - // and inserts the decorator into the decorators list. - registerDecorator: function registerDecorator(paramSize, name) { - var foundDecorator = this.nameLookup('decorators', name, 'decorator'), - options = this.setupHelperArgs(name, paramSize); - - this.decorators.push(['fn = ', this.decorators.functionCall(foundDecorator, '', ['fn', 'props', 'container', options]), ' || fn;']); - }, - - // [invokeHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // Pops off the helper's parameters, invokes the helper, - // and pushes the helper's return value onto the stack. - // - // If the helper is not found, `helperMissing` is called. - invokeHelper: function invokeHelper(paramSize, name, isSimple) { - var nonHelper = this.popStack(), - helper = this.setupHelper(paramSize, name), - simple = isSimple ? [helper.name, ' || '] : ''; - - var lookup = ['('].concat(simple, nonHelper); - if (!this.options.strict) { - lookup.push(' || ', this.aliasable('helpers.helperMissing')); - } - lookup.push(')'); - - this.push(this.source.functionCall(lookup, 'call', helper.callParams)); - }, - - // [invokeKnownHelper] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of helper invocation - // - // This operation is used when the helper is known to exist, - // so a `helperMissing` fallback is not required. - invokeKnownHelper: function invokeKnownHelper(paramSize, name) { - var helper = this.setupHelper(paramSize, name); - this.push(this.source.functionCall(helper.name, 'call', helper.callParams)); - }, - - // [invokeAmbiguous] - // - // On stack, before: hash, inverse, program, params..., ... - // On stack, after: result of disambiguation - // - // This operation is used when an expression like `{{foo}}` - // is provided, but we don't know at compile-time whether it - // is a helper or a path. - // - // This operation emits more code than the other options, - // and can be avoided by passing the `knownHelpers` and - // `knownHelpersOnly` flags at compile-time. - invokeAmbiguous: function invokeAmbiguous(name, helperCall) { - this.useRegister('helper'); - - var nonHelper = this.popStack(); - - this.emptyHash(); - var helper = this.setupHelper(0, name, helperCall); - - var helperName = this.lastHelper = this.nameLookup('helpers', name, 'helper'); - - var lookup = ['(', '(helper = ', helperName, ' || ', nonHelper, ')']; - if (!this.options.strict) { - lookup[0] = '(helper = '; - lookup.push(' != null ? helper : ', this.aliasable('helpers.helperMissing')); - } - - this.push(['(', lookup, helper.paramsInit ? ['),(', helper.paramsInit] : [], '),', '(typeof helper === ', this.aliasable('"function"'), ' ? ', this.source.functionCall('helper', 'call', helper.callParams), ' : helper))']); - }, - - // [invokePartial] - // - // On stack, before: context, ... - // On stack after: result of partial invocation - // - // This operation pops off a context, invokes a partial with that context, - // and pushes the result of the invocation back. - invokePartial: function invokePartial(isDynamic, name, indent) { - var params = [], - options = this.setupParams(name, 1, params); - - if (isDynamic) { - name = this.popStack(); - delete options.name; - } - - if (indent) { - options.indent = JSON.stringify(indent); - } - options.helpers = 'helpers'; - options.partials = 'partials'; - options.decorators = 'container.decorators'; - - if (!isDynamic) { - params.unshift(this.nameLookup('partials', name, 'partial')); - } else { - params.unshift(name); - } - - if (this.options.compat) { - options.depths = 'depths'; - } - options = this.objectLiteral(options); - params.push(options); - - this.push(this.source.functionCall('container.invokePartial', '', params)); - }, - - // [assignToHash] - // - // On stack, before: value, ..., hash, ... - // On stack, after: ..., hash, ... - // - // Pops a value off the stack and assigns it to the current hash - assignToHash: function assignToHash(key) { - var value = this.popStack(), - context = undefined, - type = undefined, - id = undefined; - - if (this.trackIds) { - id = this.popStack(); - } - if (this.stringParams) { - type = this.popStack(); - context = this.popStack(); - } - - var hash = this.hash; - if (context) { - hash.contexts[key] = context; - } - if (type) { - hash.types[key] = type; - } - if (id) { - hash.ids[key] = id; - } - hash.values[key] = value; - }, - - pushId: function pushId(type, name, child) { - if (type === 'BlockParam') { - this.pushStackLiteral('blockParams[' + name[0] + '].path[' + name[1] + ']' + (child ? ' + ' + JSON.stringify('.' + child) : '')); - } else if (type === 'PathExpression') { - this.pushString(name); - } else if (type === 'SubExpression') { - this.pushStackLiteral('true'); - } else { - this.pushStackLiteral('null'); - } - }, - - // HELPERS - - compiler: JavaScriptCompiler, - - compileChildren: function compileChildren(environment, options) { - var children = environment.children, - child = undefined, - compiler = undefined; - - for (var i = 0, l = children.length; i < l; i++) { - child = children[i]; - compiler = new this.compiler(); // eslint-disable-line new-cap - - var existing = this.matchExistingProgram(child); - - if (existing == null) { - this.context.programs.push(''); // Placeholder to prevent name conflicts for nested children - var index = this.context.programs.length; - child.index = index; - child.name = 'program' + index; - this.context.programs[index] = compiler.compile(child, options, this.context, !this.precompile); - this.context.decorators[index] = compiler.decorators; - this.context.environments[index] = child; - - this.useDepths = this.useDepths || compiler.useDepths; - this.useBlockParams = this.useBlockParams || compiler.useBlockParams; - child.useDepths = this.useDepths; - child.useBlockParams = this.useBlockParams; - } else { - child.index = existing.index; - child.name = 'program' + existing.index; - - this.useDepths = this.useDepths || existing.useDepths; - this.useBlockParams = this.useBlockParams || existing.useBlockParams; - } - } - }, - matchExistingProgram: function matchExistingProgram(child) { - for (var i = 0, len = this.context.environments.length; i < len; i++) { - var environment = this.context.environments[i]; - if (environment && environment.equals(child)) { - return environment; - } - } - }, - - programExpression: function programExpression(guid) { - var child = this.environment.children[guid], - programParams = [child.index, 'data', child.blockParams]; - - if (this.useBlockParams || this.useDepths) { - programParams.push('blockParams'); - } - if (this.useDepths) { - programParams.push('depths'); - } - - return 'container.program(' + programParams.join(', ') + ')'; - }, - - useRegister: function useRegister(name) { - if (!this.registers[name]) { - this.registers[name] = true; - this.registers.list.push(name); - } - }, - - push: function push(expr) { - if (!(expr instanceof Literal)) { - expr = this.source.wrap(expr); - } - - this.inlineStack.push(expr); - return expr; - }, - - pushStackLiteral: function pushStackLiteral(item) { - this.push(new Literal(item)); - }, - - pushSource: function pushSource(source) { - if (this.pendingContent) { - this.source.push(this.appendToBuffer(this.source.quotedString(this.pendingContent), this.pendingLocation)); - this.pendingContent = undefined; - } - - if (source) { - this.source.push(source); - } - }, - - replaceStack: function replaceStack(callback) { - var prefix = ['('], - stack = undefined, - createdStack = undefined, - usedLiteral = undefined; - - /* istanbul ignore next */ - if (!this.isInline()) { - throw new _exception2['default']('replaceStack on non-inline'); - } - - // We want to merge the inline statement into the replacement statement via ',' - var top = this.popStack(true); - - if (top instanceof Literal) { - // Literals do not need to be inlined - stack = [top.value]; - prefix = ['(', stack]; - usedLiteral = true; - } else { - // Get or create the current stack name for use by the inline - createdStack = true; - var _name = this.incrStack(); - - prefix = ['((', this.push(_name), ' = ', top, ')']; - stack = this.topStack(); - } - - var item = callback.call(this, stack); - - if (!usedLiteral) { - this.popStack(); - } - if (createdStack) { - this.stackSlot--; - } - this.push(prefix.concat(item, ')')); - }, - - incrStack: function incrStack() { - this.stackSlot++; - if (this.stackSlot > this.stackVars.length) { - this.stackVars.push('stack' + this.stackSlot); - } - return this.topStackName(); - }, - topStackName: function topStackName() { - return 'stack' + this.stackSlot; - }, - flushInline: function flushInline() { - var inlineStack = this.inlineStack; - this.inlineStack = []; - for (var i = 0, len = inlineStack.length; i < len; i++) { - var entry = inlineStack[i]; - /* istanbul ignore if */ - if (entry instanceof Literal) { - this.compileStack.push(entry); - } else { - var stack = this.incrStack(); - this.pushSource([stack, ' = ', entry, ';']); - this.compileStack.push(stack); - } - } - }, - isInline: function isInline() { - return this.inlineStack.length; - }, - - popStack: function popStack(wrapped) { - var inline = this.isInline(), - item = (inline ? this.inlineStack : this.compileStack).pop(); - - if (!wrapped && item instanceof Literal) { - return item.value; - } else { - if (!inline) { - /* istanbul ignore next */ - if (!this.stackSlot) { - throw new _exception2['default']('Invalid stack pop'); - } - this.stackSlot--; - } - return item; - } - }, - - topStack: function topStack() { - var stack = this.isInline() ? this.inlineStack : this.compileStack, - item = stack[stack.length - 1]; - - /* istanbul ignore if */ - if (item instanceof Literal) { - return item.value; - } else { - return item; - } - }, - - contextName: function contextName(context) { - if (this.useDepths && context) { - return 'depths[' + context + ']'; - } else { - return 'depth' + context; - } - }, - - quotedString: function quotedString(str) { - return this.source.quotedString(str); - }, - - objectLiteral: function objectLiteral(obj) { - return this.source.objectLiteral(obj); - }, - - aliasable: function aliasable(name) { - var ret = this.aliases[name]; - if (ret) { - ret.referenceCount++; - return ret; - } - - ret = this.aliases[name] = this.source.wrap(name); - ret.aliasable = true; - ret.referenceCount = 1; - - return ret; - }, - - setupHelper: function setupHelper(paramSize, name, blockHelper) { - var params = [], - paramsInit = this.setupHelperArgs(name, paramSize, params, blockHelper); - var foundHelper = this.nameLookup('helpers', name, 'helper'), - callContext = this.aliasable(this.contextName(0) + ' != null ? ' + this.contextName(0) + ' : (container.nullContext || {})'); - - return { - params: params, - paramsInit: paramsInit, - name: foundHelper, - callParams: [callContext].concat(params) - }; - }, - - setupParams: function setupParams(helper, paramSize, params) { - var options = {}, - contexts = [], - types = [], - ids = [], - objectArgs = !params, - param = undefined; - - if (objectArgs) { - params = []; - } - - options.name = this.quotedString(helper); - options.hash = this.popStack(); - - if (this.trackIds) { - options.hashIds = this.popStack(); - } - if (this.stringParams) { - options.hashTypes = this.popStack(); - options.hashContexts = this.popStack(); - } - - var inverse = this.popStack(), - program = this.popStack(); - - // Avoid setting fn and inverse if neither are set. This allows - // helpers to do a check for `if (options.fn)` - if (program || inverse) { - options.fn = program || 'container.noop'; - options.inverse = inverse || 'container.noop'; - } - - // The parameters go on to the stack in order (making sure that they are evaluated in order) - // so we need to pop them off the stack in reverse order - var i = paramSize; - while (i--) { - param = this.popStack(); - params[i] = param; - - if (this.trackIds) { - ids[i] = this.popStack(); - } - if (this.stringParams) { - types[i] = this.popStack(); - contexts[i] = this.popStack(); - } - } - - if (objectArgs) { - options.args = this.source.generateArray(params); - } - - if (this.trackIds) { - options.ids = this.source.generateArray(ids); - } - if (this.stringParams) { - options.types = this.source.generateArray(types); - options.contexts = this.source.generateArray(contexts); - } - - if (this.options.data) { - options.data = 'data'; - } - if (this.useBlockParams) { - options.blockParams = 'blockParams'; - } - return options; - }, - - setupHelperArgs: function setupHelperArgs(helper, paramSize, params, useRegister) { - var options = this.setupParams(helper, paramSize, params); - options = this.objectLiteral(options); - if (useRegister) { - this.useRegister('options'); - params.push('options'); - return ['options=', options]; - } else if (params) { - params.push(options); - return ''; - } else { - return options; - } - } - }; - - (function () { - var reservedWords = ('break else new var' + ' case finally return void' + ' catch for switch while' + ' continue function this with' + ' default if throw' + ' delete in try' + ' do instanceof typeof' + ' abstract enum int short' + ' boolean export interface static' + ' byte extends long super' + ' char final native synchronized' + ' class float package throws' + ' const goto private transient' + ' debugger implements protected volatile' + ' double import public let yield await' + ' null true false').split(' '); - - var compilerWords = JavaScriptCompiler.RESERVED_WORDS = {}; - - for (var i = 0, l = reservedWords.length; i < l; i++) { - compilerWords[reservedWords[i]] = true; - } - })(); - - JavaScriptCompiler.isValidJavaScriptVariableName = function (name) { - return !JavaScriptCompiler.RESERVED_WORDS[name] && /^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(name); - }; - - function strictLookup(requireTerminal, compiler, parts, type) { - var stack = compiler.popStack(), - i = 0, - len = parts.length; - if (requireTerminal) { - len--; - } - - for (; i < len; i++) { - stack = compiler.nameLookup(stack, parts[i], type); - } - - if (requireTerminal) { - return [compiler.aliasable('container.strict'), '(', stack, ', ', compiler.quotedString(parts[i]), ')']; - } else { - return stack; - } - } - - exports['default'] = JavaScriptCompiler; - module.exports = exports['default']; - -/***/ }), -/* 43 */ -/***/ (function(module, exports, __webpack_require__) { - - /* global define */ - 'use strict'; - - exports.__esModule = true; - - var _utils = __webpack_require__(5); - - var SourceNode = undefined; - - try { - /* istanbul ignore next */ - if (false) { - // We don't support this in AMD environments. For these environments, we asusme that - // they are running on the browser and thus have no need for the source-map library. - var SourceMap = require('source-map'); - SourceNode = SourceMap.SourceNode; - } - } catch (err) {} - /* NOP */ - - /* istanbul ignore if: tested but not covered in istanbul due to dist build */ - if (!SourceNode) { - SourceNode = function (line, column, srcFile, chunks) { - this.src = ''; - if (chunks) { - this.add(chunks); - } - }; - /* istanbul ignore next */ - SourceNode.prototype = { - add: function add(chunks) { - if (_utils.isArray(chunks)) { - chunks = chunks.join(''); - } - this.src += chunks; - }, - prepend: function prepend(chunks) { - if (_utils.isArray(chunks)) { - chunks = chunks.join(''); - } - this.src = chunks + this.src; - }, - toStringWithSourceMap: function toStringWithSourceMap() { - return { code: this.toString() }; - }, - toString: function toString() { - return this.src; - } - }; - } - - function castChunk(chunk, codeGen, loc) { - if (_utils.isArray(chunk)) { - var ret = []; - - for (var i = 0, len = chunk.length; i < len; i++) { - ret.push(codeGen.wrap(chunk[i], loc)); - } - return ret; - } else if (typeof chunk === 'boolean' || typeof chunk === 'number') { - // Handle primitives that the SourceNode will throw up on - return chunk + ''; - } - return chunk; - } - - function CodeGen(srcFile) { - this.srcFile = srcFile; - this.source = []; - } - - CodeGen.prototype = { - isEmpty: function isEmpty() { - return !this.source.length; - }, - prepend: function prepend(source, loc) { - this.source.unshift(this.wrap(source, loc)); - }, - push: function push(source, loc) { - this.source.push(this.wrap(source, loc)); - }, - - merge: function merge() { - var source = this.empty(); - this.each(function (line) { - source.add([' ', line, '\n']); - }); - return source; - }, - - each: function each(iter) { - for (var i = 0, len = this.source.length; i < len; i++) { - iter(this.source[i]); - } - }, - - empty: function empty() { - var loc = this.currentLocation || { start: {} }; - return new SourceNode(loc.start.line, loc.start.column, this.srcFile); - }, - wrap: function wrap(chunk) { - var loc = arguments.length <= 1 || arguments[1] === undefined ? this.currentLocation || { start: {} } : arguments[1]; - - if (chunk instanceof SourceNode) { - return chunk; - } - - chunk = castChunk(chunk, this, loc); - - return new SourceNode(loc.start.line, loc.start.column, this.srcFile, chunk); - }, - - functionCall: function functionCall(fn, type, params) { - params = this.generateList(params); - return this.wrap([fn, type ? '.' + type + '(' : '(', params, ')']); - }, - - quotedString: function quotedString(str) { - return '"' + (str + '').replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\u2028/g, '\\u2028') // Per Ecma-262 7.3 + 7.8.4 - .replace(/\u2029/g, '\\u2029') + '"'; - }, - - objectLiteral: function objectLiteral(obj) { - var pairs = []; - - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - var value = castChunk(obj[key], this); - if (value !== 'undefined') { - pairs.push([this.quotedString(key), ':', value]); - } - } - } - - var ret = this.generateList(pairs); - ret.prepend('{'); - ret.add('}'); - return ret; - }, - - generateList: function generateList(entries) { - var ret = this.empty(); - - for (var i = 0, len = entries.length; i < len; i++) { - if (i) { - ret.add(','); - } - - ret.add(castChunk(entries[i], this)); - } - - return ret; - }, - - generateArray: function generateArray(entries) { - var ret = this.generateList(entries); - ret.prepend('['); - ret.add(']'); - - return ret; - } - }; - - exports['default'] = CodeGen; - module.exports = exports['default']; - -/***/ }) -/******/ ]) -}); -; \ No newline at end of file diff --git a/static/js/sapphire.js b/static/js/sapphire.js index db65fdd1..4f2e6c29 100644 --- a/static/js/sapphire.js +++ b/static/js/sapphire.js @@ -21,189 +21,332 @@ // General options //===================================== // Times the canvas is refreshed a second -var FPS = 30; +let FPS = 30; // Determine the length of the trail (0=instant disappear, maximum=window.innerHeight=no disappear) -var TRAIL_TIME = 5; +let TRAIL_TIME = 5; // The color of the characters -var RAIN_COLOR = "#00F"; +let RAIN_COLOR = "#00F"; // The characters displayed -var CHARACTERS = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split(""); +let CHARACTERS = "田由甲申甴电甶男甸甹町画甼甽甾甿畀畁畂畃畄畅畆畇畈畉畊畋界畍畎畏畐畑".split(""); // The font size used to display the characters -var FONT_SIZE = 10; +let FONT_SIZE = 10; // The maximum number of characters displayed by column -var MAX_CHAR = 7; +let MAX_CHAR = 7; -var Sapphire = function () { - var sapphire = { +let Sapphire; + +Sapphire = function () { + let sapphire = { triggerHandle: undefined, - activated: false, - runOnce: false, + activated: false, + runOnce: false, - getClass: function(elt, main, name) { elt.obj = main.getElementsByClassName(name); }, - getTag: function(elt, main, name) { elt.obj = main.getElementsByTagName(name); }, + getClass: function (elt, main, name) { + elt.obj = main.getElementsByClassName(name); + }, + getTag: function (elt, main, name) { + elt.obj = main.getElementsByTagName(name); + }, - getProp: function(elt) { - for (var i=0 ; i sapphire.canvas.height && Math.random() > 0.975) + for (let i = 0; i < sapphire.drops.length; i++) { + for (let j = 0; j < sapphire.drops[i].length; j++) { + let text = CHARACTERS[Math.floor(Math.random() * CHARACTERS.length)]; + ctx.fillText(text, i * FONT_SIZE, sapphire.drops[i][j] * FONT_SIZE); + if (sapphire.drops[i][j] * FONT_SIZE > sapphire.canvas.height && Math.random() > 0.975) sapphire.drops[i][j] = 0; sapphire.drops[i][j]++; } @@ -278,19 +425,29 @@ var Sapphire = function () { sapphire.resize(); window.addEventListener('resize', sapphire.resize); - sapphire.triggerHandle = setInterval(drawEverything, 1000/FPS); + sapphire.triggerHandle = setInterval(drawEverything, 1000 / FPS); }, - stop: function() { + stop: function () { window.removeEventListener('resize', sapphire.resize); clearInterval(sapphire.triggerHandle); sapphire.canvas.parentNode.removeChild(sapphire.canvas); }, - alterElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].alter(main); } }, - revertElts: function() { for (var e in sapphire.elts) { sapphire.elts[e].revert(main); } }, + alterElts: function () { + for (let e in sapphire.elts) { + let main = document.getElementById("main"); + sapphire.elts[e].alter(main); + } + }, + revertElts: function () { + for (let e in sapphire.elts) { + let main = document.getElementById("main"); + sapphire.elts[e].revert(main); + } + }, - activate: function() { + activate: function () { if (!sapphire.runOnce) { sapphire.runOnce = true; sapphire.init(); @@ -306,11 +463,7 @@ var Sapphire = function () { sapphire.revertElts(); } } - } + }; return sapphire; -} - -var s = Sapphire(); -Konami(s.activate); - +}; \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index aeb840da..401ece70 100644 --- a/templates/base.html +++ b/templates/base.html @@ -39,224 +39,249 @@ with this program; if not, write to the Free Software Foundation, Inc., - - - {# Load CSS and JavaScript #} - {% bootstrap_css %} - - + + {# Preload JavaScript #} {% bootstrap_javascript %} - - - - - + + + {# Load CSS #} + {% bootstrap_css %} + + + + + {{ name_website }} : {% block title %}{% trans "Home" %}{% endblock %} - -
- + +
+
+
+
+
+ {% block sidebar %} + {% endblock %} +
- - -
-
-
-
-
- {% block sidebar %} - {% endblock %} -
-
-
-
- {# Display django.contrib.messages as Bootstrap alerts #} - {% bootstrap_messages %} -
- {% block content %}{% endblock %} -
-
-
- {% if request_user.is_authenticated %} -
-

{{ request_user.name }} {{ request_user.surname }}

-
- - - - - - - - - - - - - - - - - -
{% trans "Username" %}{{ request_user.pseudo }}
{% trans "Room" %}{{ request_user.room }}
{% trans "Internet access" %} - {% if request_user.has_access %} - {% blocktrans with request.user.end_access|date:"d b Y" as date %}Until {{ date }}{% endblocktrans %} - {% else %} - {% trans "Disabled" %} - {% endif %} -
{% trans "Membership" %} - {% if request_user.is_adherent %} - {% blocktrans with request_user.end_adhesion|date:"d b Y" as date %}Until {{ date }}{% endblocktrans %} - {% else %} - {% trans "Not a member" %} - {% endif %} -
- - {% else %} -

{% trans "You are not logged in." %}

- {% endif %} -
+
+ {# Display django.contrib.messages as Bootstrap alerts #} + {% bootstrap_messages %} + {% block content %}{% endblock %} +
+
+
{% if request_user.is_authenticated %} -
-
-

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

-
- +
+

{{ request_user.name }} {{ request_user.surname }}

+ + + + + + + + + + + + + + + + + +
{% trans "Username" %}{{ request_user.pseudo }}
{% trans "Room" %}{{ request_user.room }}
{% trans "Internet access" %} + {% if request_user.has_access %} + {% blocktrans with end_access_date=request.user.end_access|date:"d b Y" %}Until {{ end_access_date }}{% endblocktrans %} + {% else %} + {% trans "Disabled" %} + {% endif %} +
{% trans "Membership" %} + {% if request_user.is_adherent %} + {% blocktrans with end_adhesion_date=request_user.end_adhesion|date:"d b Y" %}Until {{ end_adhesion_date }}{% endblocktrans %} + {% else %} + {% trans "Non member" %} + {% endif %} +
+ + {% else %} +
+

{% trans "You are not logged in." %}

+
{% endif %}
+ {% if request_user.is_authenticated %} +
+
+

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

+
+ +
+ {% endif %}
-