diff --git a/cotisations/admin.py b/cotisations/admin.py index ebe7d683..0519e819 100644 --- a/cotisations/admin.py +++ b/cotisations/admin.py @@ -34,7 +34,7 @@ from .models import CustomInvoice, CostEstimate class FactureAdmin(VersionAdmin): - """Class admin d'une facture, tous les champs""" + """Admin class for invoices.""" pass @@ -52,32 +52,31 @@ class CustomInvoiceAdmin(VersionAdmin): class VenteAdmin(VersionAdmin): - """Class admin d'une vente, tous les champs (facture related)""" + """Admin class for purchases.""" pass class ArticleAdmin(VersionAdmin): - """Class admin d'un article en vente""" + """Admin class for articles.""" pass class BanqueAdmin(VersionAdmin): - """Class admin de la liste des banques (facture related)""" + """Admin class for banks.""" pass class PaiementAdmin(VersionAdmin): - """Class admin d'un moyen de paiement (facture related""" + """Admin class for payment methods.""" pass class CotisationAdmin(VersionAdmin): - """Class admin d'une cotisation (date de debut et de fin), - Vente related""" + """Admin class for subscriptions.""" pass diff --git a/cotisations/views.py b/cotisations/views.py index 62eabef6..680b9483 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -185,7 +185,7 @@ def new_cost_estimate(request): """ # The template needs the list of articles (for the JS part) articles = Article.objects.all() - # Building the invocie form and the article formset + # Building the invoice form and the article formset cost_estimate_form = CostEstimateForm(request.POST or None) articles_formset = formset_factory(SelectArticleForm)( @@ -240,7 +240,7 @@ def new_custom_invoice(request): """ # The template needs the list of articles (for the JS part) articles = Article.objects.all() - # Building the invocie form and the article formset + # Building the invoice form and the article formset invoice_form = CustomInvoiceForm(request.POST or None) articles_formset = formset_factory(SelectArticleForm)( @@ -366,7 +366,7 @@ def edit_facture(request, facture, **_kwargs): @can_delete(Facture) def del_facture(request, facture, **_kwargs): """ - View used to delete an existing invocie. + View used to delete an existing invoice. """ if request.method == "POST": facture.delete() @@ -382,7 +382,7 @@ def del_facture(request, facture, **_kwargs): @login_required @can_edit(CostEstimate) def edit_cost_estimate(request, invoice, **kwargs): - # Building the invocie form and the article formset + # Building the invoice 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( @@ -411,7 +411,7 @@ def edit_cost_estimate(request, invoice, **kwargs): @can_edit(CostEstimate) @can_create(CustomInvoice) def cost_estimate_to_invoice(request, cost_estimate, **_kwargs): - """Create a custom invoice from a cos estimate""" + """Create a custom invoice from a cost estimate.""" cost_estimate.create_invoice() messages.success( request, _("An invoice was successfully created from your cost estimate.") @@ -422,7 +422,7 @@ def cost_estimate_to_invoice(request, cost_estimate, **_kwargs): @login_required @can_edit(CustomInvoice) def edit_custom_invoice(request, invoice, **kwargs): - # Building the invocie form and the article formset + # Building the invoice form and the article formset invoice_form = CustomInvoiceForm(request.POST or None, instance=invoice) purchases_objects = Vente.objects.filter(facture=invoice) purchase_form_set = modelformset_factory( @@ -498,7 +498,7 @@ def cost_estimate_pdf(request, invoice, **_kwargs): @can_delete(CostEstimate) def del_cost_estimate(request, estimate, **_kwargs): """ - View used to delete an existing invocie. + View used to delete an existing invoice. """ if request.method == "POST": estimate.delete() @@ -561,7 +561,7 @@ def custom_invoice_pdf(request, invoice, **_kwargs): @can_delete(CustomInvoice) def del_custom_invoice(request, invoice, **_kwargs): """ - View used to delete an existing invocie. + View used to delete an existing invoice. """ if request.method == "POST": invoice.delete() diff --git a/logs/forms.py b/logs/forms.py index 40bae849..5cdeed7c 100644 --- a/logs/forms.py +++ b/logs/forms.py @@ -53,6 +53,15 @@ CHOICES_TYPE = ( def all_classes(module): + """Get the list of all class names of the module. + + Args: + module: the module in which to retrieve classes. + + Returns: + A list containing the names of all classes that are defined in + the module. + """ classes = [] for name, obj in inspect.getmembers(module): @@ -63,8 +72,16 @@ def all_classes(module): def classes_for_action_type(action_type): - """Return the list of class names to be displayed for a - given actions type filter""" + """Get the list of class names to be displayed for a given action type + filter. + + Args: + action_type: the string used to filter the class names. + + Returns: + A list containing the class names corresponding to the action type + filter. + """ if action_type == "users": return [ users.models.User.__name__, @@ -96,7 +113,7 @@ def classes_for_action_type(action_type): class ActionsSearchForm(Form): - """The form for a simple search""" + """Form used to do an advanced search through the logs.""" u = forms.ModelChoiceField( label=_("Performed by"), queryset=users.models.User.objects.all(), @@ -123,7 +140,7 @@ class ActionsSearchForm(Form): class MachineHistorySearchForm(Form): - """The form for a simple search""" + """Form used to do a search through the machine histories.""" q = forms.CharField( label=_("Search"), max_length=100, diff --git a/logs/models.py b/logs/models.py index f4fb23f6..4add391a 100644 --- a/logs/models.py +++ b/logs/models.py @@ -69,12 +69,16 @@ def make_version_filter(key, value): class MachineHistorySearchEvent: def __init__(self, user, machine, interface, start=None, end=None): - """ - :param user: User, The user owning the maching at the time of the event - :param machine: Version, the machine version related to the interface - :param interface: Version, the interface targeted by this event - :param start: datetime, the date at which this version was created - :param end: datetime, the date at which this version was replace by a new one + """Initialise an instance of MachineHistorySearchEvent. + + Args: + user: User, the user owning the machine at the time of the event. + machine: Version, the machine version related to the interface. + interface: Version, the interface targeted by this event. + start: datetime, the date at which this version was created + (default: None). + end: datetime, the date at which this version was replace by a new + one (default: None). """ self.user = user self.machine = machine @@ -86,9 +90,13 @@ class MachineHistorySearchEvent: self.comment = interface.revision.get_comment() or None def is_similar(self, elt2): - """ - Checks whether two events are similar enough to be merged - :return: bool + """Check whether two events are similar enough to be merged. + + Args: + elt2: MachineHistorySearchEvent, the event to compare with self. + + Returns: + A boolean, True if the events can be merged and False otherwise. """ return ( elt2 is not None @@ -115,10 +123,14 @@ class MachineHistorySearch: self._last_evt = None def get(self, search, params): - """ - :param search: ip or mac to lookup - :param params: dict built by the search view - :return: list or None, a list of MachineHistorySearchEvent in reverse chronological order + """Get the events in machine histories related to the search. + + Args: + search: the IP or MAC address used in the search. + params: the dictionary built by the search view. + + Returns: + A list of MachineHistorySearchEvent in reverse chronological order. """ self.start = params.get("s", None) self.end = params.get("e", None) @@ -140,11 +152,12 @@ class MachineHistorySearch: return [] def _add_revision(self, user, machine, interface): - """ - Add a new revision to the chronological order - :param user: User, The user owning the maching at the time of the event - :param machine: Version, the machine version related to the interface - :param interface: Version, the interface targeted by this event + """Add a new revision to the chronological order. + + Args: + user: User, the user owning the maching at the time of the event. + machine: Version, the machine version related to the interface. + interface: Version, the interface targeted by this event. """ evt = MachineHistorySearchEvent(user, machine, interface) evt.start_date = interface.revision.date_created @@ -171,10 +184,15 @@ class MachineHistorySearch: self._last_evt = evt def _get_interfaces_for_ip(self, ip): - """ - :param ip: str - :return: An iterable object with the Version objects - of Interfaces with the given IP + """Get the Version objects of interfaces with the given IP + address. + + Args: + ip: the string corresponding to the IP address. + + Returns: + An iterable object with the Version objects of interfaces with the + given IP address. """ # TODO: What if ip list was deleted? try: @@ -189,10 +207,15 @@ class MachineHistorySearch: ) def _get_interfaces_for_mac(self, mac): - """ - :param mac: str - :return: An iterable object with the Version objects - of Interfaces with the given MAC address + """Get the Version objects of interfaces with the given MAC + address. + + Args: + mac: the string corresponding to the MAC address. + + Returns: + An iterable object with the Version objects of interfaces with the + given MAC address. """ return ( Version.objects.get_for_model(Interface) @@ -201,10 +224,14 @@ class MachineHistorySearch: ) def _get_machines_for_interface(self, interface): - """ - :param interface: Version, the interface for which to find the machines - :return: An iterable object with the Version objects of Machine to - which the given interface was attributed + """Get the Version objects of machines with the given interface. + + Args: + interface: Version, the interface used to find machines. + + Returns: + An iterable object with the Version objects of machines to which + the given interface was assigned. """ machine_id = interface.field_dict["machine_id"] return ( @@ -214,18 +241,27 @@ class MachineHistorySearch: ) def _get_user_for_machine(self, machine): - """ - :param machine: Version, the machine of which the owner must be found - :return: The user to which the given machine belongs + """Get the User instance owning the given machine. + + Args: + machine: Version, the machine used to find its owner. + + Returns: + The User instance of the owner of the given machine. """ # TODO: What if user was deleted? user_id = machine.field_dict["user_id"] return User.objects.get(id=user_id) def _get_by_ip(self, ip): - """ - :param ip: str, The IP to lookup - :returns: list, a list of MachineHistorySearchEvent + """Get events related to the given IP address. + + Args: + ip: the string corresponding to the IP address. + + Returns: + A list of MachineHistorySearchEvent related to the given IP + address. """ interfaces = self._get_interfaces_for_ip(ip) @@ -239,9 +275,14 @@ class MachineHistorySearch: return self.events def _get_by_mac(self, mac): - """ - :param mac: str, The MAC address to lookup - :returns: list, a list of MachineHistorySearchEvent + """Get events related to the given MAC address. + + Args: + mac: the string corresponding to the MAC address. + + Returns: + A list of MachineHistorySearchEvent related to the given MAC + address. """ interfaces = self._get_interfaces_for_mac(mac) @@ -261,10 +302,10 @@ class MachineHistorySearch: class RelatedHistory: def __init__(self, version): - """ - :param name: Name of this instance - :param model_name: Name of the related model (e.g. "user") - :param object_id: ID of the related object + """Initialise an instance of RelatedHistory. + + Args: + version: Version, the version related to the history. """ self.version = version self.app_name = version.content_type.app_label @@ -287,10 +328,14 @@ class RelatedHistory: class HistoryEvent: def __init__(self, version, previous_version=None, edited_fields=None): - """ - :param version: Version, the version of the object for this event - :param previous_version: Version, the version of the object before this event - :param edited_fields: list, The list of modified fields by this event + """Initialise an instance of HistoryEvent. + + Args: + version: Version, the version of the object for this event. + previous_version: Version, the version of the object before this + event (default: None). + edited_fields: list, The list of modified fields by this event + (default: None). """ self.version = version self.previous_version = previous_version @@ -300,11 +345,15 @@ class HistoryEvent: self.comment = version.revision.get_comment() or None def _repr(self, name, value): - """ - Returns the best representation of the given field - :param name: the name of the field - :param value: the value of the field - :return: object + """Get the appropriate representation of the given field. + + Args: + name: the name of the field + value: the value of the field + + Returns: + The string corresponding to the appropriate representation of the + given field. """ if value is None: return _("None") @@ -312,10 +361,14 @@ class HistoryEvent: return value def edits(self, hide=[]): - """ - Build a list of the changes performed during this event - :param hide: list, the list of fields for which not to show details - :return: str + """Get the list of the changes performed during this event. + + Args: + hide: the list of fields for which not to show details (default: + []). + + Returns: + The list of fields edited by the event to display. """ edits = [] @@ -342,10 +395,15 @@ class History: self.event_type = HistoryEvent def get(self, instance_id, model): - """ - :param instance_id: int, The id of the instance to lookup - :param model: class, The type of object to lookup - :return: list or None, a list of HistoryEvent, in reverse chronological order + """Get the list of history events of the given object. + + Args: + instance_id: int, the id of the instance to lookup. + model: class, the type of object to lookup. + + Returns: + A list of HistoryEvent, in reverse chronological order, related to + the given object or None if no version was found. """ self.events = [] @@ -368,12 +426,16 @@ class History: return self.events[::-1] def _compute_diff(self, v1, v2, ignoring=[]): - """ - Find the edited field between two versions - :param v1: Version - :param v2: Version - :param ignoring: List, a list of fields to ignore - :return: List of field names + """Find the edited fields between two versions. + + Args: + v1: Version to compare. + v2: Version to compare. + ignoring: a list of fields to ignore. + + Returns: + The list of field names in v1 that are different from the ones in + v2. """ fields = [] @@ -384,9 +446,10 @@ class History: return fields def _add_revision(self, version): - """ - Add a new revision to the chronological order - :param version: Version, The version of the interface for this event + """Add a new revision to the chronological order. + + Args: + version: Version, the version of the interface for this event. """ diff = None if self._last_version is not None: @@ -427,6 +490,15 @@ class VersionAction(HistoryEvent): return apps.get_model(self.application(), self.model_name()) def edits(self, hide=["password", "pwd_ntlm", "gpg_fingerprint"]): + """Get the list of the changes performed during this event. + + Args: + hide: the list of fields for which not to show details (default: + ["password", "pwd_ntlm", "gpg_fingerprint"]). + + Returns: + The list of fields edited by the event to display. + """ self.previous_version = self._previous_version() if self.previous_version is None: @@ -436,6 +508,12 @@ class VersionAction(HistoryEvent): return super(VersionAction, self).edits(hide) def _previous_version(self): + """Get the previous version of self. + + Returns: + The Version corresponding to the previous version of self, or None + in case of exception. + """ model = self.object_type() try: query = ( @@ -451,12 +529,16 @@ class VersionAction(HistoryEvent): return None def _compute_diff(self, v1, v2, ignoring=["pwd_ntlm"]): - """ - Find the edited field between two versions - :param v1: Version - :param v2: Version - :param ignoring: List, a list of fields to ignore - :return: List of field names + """Find the edited fields between two versions. + + Args: + v1: Version to compare. + v2: Version to compare. + ignoring: a list of fields to ignore (default: ["pwd_ntlm"]). + + Returns: + The list of field names in v1 that are different from the ones in + v2. """ fields = [] @@ -468,7 +550,8 @@ class VersionAction(HistoryEvent): class RevisionAction: - """A Revision may group multiple Version objects together""" + """A Revision may group multiple Version objects together.""" + def __init__(self, revision): self.performed_by = revision.user self.revision = revision @@ -486,9 +569,13 @@ class RevisionAction: class ActionsSearch: def get(self, params): - """ - :param params: dict built by the search view - :return: QuerySet of Revision objects + """Get the Revision objects corresponding to the search. + + Args: + params: dictionary built by the search view. + + Returns: + The QuerySet of Revision objects corresponding to the search. """ user = params.get("u", None) start = params.get("s", None) @@ -540,11 +627,15 @@ class ActionsSearch: class UserHistoryEvent(HistoryEvent): def _repr(self, name, value): - """ - Returns the best representation of the given field - :param name: the name of the field - :param value: the value of the field - :return: object + """Get the appropriate representation of the given field. + + Args: + name: the name of the field + value: the value of the field + + Returns: + The string corresponding to the appropriate representation of the + given field. """ if name == "groups": if len(value) == 0: @@ -599,10 +690,14 @@ class UserHistoryEvent(HistoryEvent): return super(UserHistoryEvent, self)._repr(name, value) def edits(self, hide=["password", "pwd_ntlm", "gpg_fingerprint"]): - """ - Build a list of the changes performed during this event - :param hide: list, the list of fields for which not to show details - :return: str + """Get the list of the changes performed during this event. + + Args: + hide: the list of fields for which not to show details (default: + ["password", "pwd_ntlm", "gpg_fingerprint"]). + + Returns: + The list of fields edited by the event to display. """ return super(UserHistoryEvent, self).edits(hide) @@ -631,9 +726,14 @@ class UserHistory(History): self.event_type = UserHistoryEvent def get(self, user_id, model): - """ - :param user_id: int, the id of the user to lookup - :return: list or None, a list of UserHistoryEvent, in reverse chronological order + """Get the the list of UserHistoryEvent related to the object. + + Args: + user_id: int, the id of the user to lookup. + + Returns: + The list of UserHistoryEvent, in reverse chronological order, + related to the object, or None if nothing was found. """ self.events = [] @@ -710,10 +810,10 @@ class UserHistory(History): ) def _add_revision(self, version): - """ - Add a new revision to the chronological order - :param user: User, The user displayed in this history - :param version: Version, The version of the user for this event + """Add a new revision to the chronological order. + + Args: + version: Version, the version of the user for this event. """ diff = None if self._last_version is not None: @@ -736,11 +836,15 @@ class UserHistory(History): class MachineHistoryEvent(HistoryEvent): def _repr(self, name, value): - """ - Returns the best representation of the given field - :param name: the name of the field - :param value: the value of the field - :return: object + """Return the appropriate representation of the given field. + + Args: + name: the name of the field + value: the value of the field + + Returns: + The string corresponding to the appropriate representation of the + given field. """ if name == "user_id": try: @@ -757,6 +861,15 @@ class MachineHistory(History): self.event_type = MachineHistoryEvent def get(self, machine_id, model): + """Get the the list of MachineHistoryEvent related to the object. + + Args: + machine_id: int, the id of the machine to lookup. + + Returns: + The list of MachineHistoryEvent, in reverse chronological order, + related to the object. + """ self.related = ( Version.objects.get_for_model(Interface) .filter(make_version_filter("machine", machine_id)) @@ -772,11 +885,15 @@ class MachineHistory(History): class InterfaceHistoryEvent(HistoryEvent): def _repr(self, name, value): - """ - Returns the best representation of the given field - :param name: the name of the field - :param value: the value of the field - :return: object + """Get the appropriate representation of the given field. + + Args: + name: the name of the field + value: the value of the field + + Returns: + The string corresponding to the appropriate representation of the + given field. """ if name == "ipv4_id" and value is not None: try: @@ -813,6 +930,15 @@ class InterfaceHistory(History): self.event_type = InterfaceHistoryEvent def get(self, interface_id, model): + """Get the the list of InterfaceHistoryEvent related to the object. + + Args: + interface_id: int, the id of the interface to lookup. + + Returns: + The list of InterfaceHistoryEvent, in reverse chronological order, + related to the object. + """ return super(InterfaceHistory, self).get(interface_id, Interface) @@ -829,10 +955,15 @@ HISTORY_CLASS_MAPPING = { def get_history_class(model): - """ - Find the mos appropriate History subclass to represent - the given model's history - :model: class + """Get the most appropriate History subclass to represent the given model's + history. + + Args: + model: the class for which to get the history. + + Returns: + The most appropriate History subclass for the given model's history, + or History if no other was found. """ try: return HISTORY_CLASS_MAPPING[model]() diff --git a/logs/urls.py b/logs/urls.py index d70cc4a8..327ccb8a 100644 --- a/logs/urls.py +++ b/logs/urls.py @@ -19,9 +19,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. -""" -Urls de l'application logs, pointe vers les fonctions de views. -Inclu dans le re2o.urls +"""logs.urls +The defined URLs for the logs app. Included in re2o.urls. """ from __future__ import unicode_literals diff --git a/logs/views.py b/logs/views.py index cf69ae38..87084d66 100644 --- a/logs/views.py +++ b/logs/views.py @@ -24,16 +24,16 @@ # App de gestion des statistiques pour re2o # Gabriel Détraz # Gplv2 -""" -Vues des logs et statistiques générales. +"""logs.views +Views of logs and general statistics. -La vue index générale affiche une selection des dernières actions, -classées selon l'importance, avec date, et user formatés. +The general indew view displays a list of the last actions, sorted by +importance, with date and user formatted. -Stats_logs renvoie l'ensemble des logs. +stats_logs returns all the logs. -Les autres vues sont thématiques, ensemble des statistiques et du -nombre d'objets par models, nombre d'actions par user, etc +The other views are related to specific topics, with statistics for number of +objects for per model, number of actions per user etc. """ from __future__ import unicode_literals @@ -113,8 +113,7 @@ from .forms import ActionsSearchForm, MachineHistorySearchForm @login_required @can_view_app("logs") def index(request): - """Affiche les logs affinés, date reformatées, selectionne - les event importants (ajout de droits, ajout de ban/whitelist)""" + """View used to display summary of events about users.""" pagination_number = GeneralOption.get_cached_value("pagination_number") # The types of content kept for display content_type_filter = ["ban", "whitelist", "vente", "interface", "user"] @@ -155,8 +154,7 @@ def index(request): @login_required @can_view_all(GeneralOption) def stats_logs(request): - """Affiche l'ensemble des logs et des modifications sur les objets, - classés par date croissante, en vrac""" + """View used to do an advanced search through the logs.""" actions_form = ActionsSearchForm(request.GET or None) if actions_form.is_valid(): @@ -185,7 +183,7 @@ def stats_logs(request): @login_required @can_edit_history def revert_action(request, revision_id): - """ Annule l'action en question """ + """View used to revert actions.""" try: revision = Revision.objects.get(id=revision_id) except Revision.DoesNotExist: @@ -204,9 +202,10 @@ def revert_action(request, revision_id): @login_required @can_view_all(IpList, Interface, User) def stats_general(request): - """Statistiques générales affinées sur les ip, activées, utilisées par - range, et les statistiques générales sur les users : users actifs, - cotisants, activés, archivés, etc""" + """View used to display general statistics about users (activated, + disabled, archived etc.) and IP addresses (ranges, number of assigned + addresses etc.). + """ ip_dict = dict() for ip_range in IpType.objects.select_related("vlan").all(): all_ip = IpList.objects.filter(ip_type=ip_range) @@ -377,9 +376,9 @@ def stats_general(request): @login_required @can_view_app("users", "cotisations", "machines", "topologie") def stats_models(request): - """Statistiques générales, affiche les comptages par models: - nombre d'users, d'écoles, de droits, de bannissements, - de factures, de ventes, de banque, de machines, etc""" + """View used to display general statistics about the number of objects + stored in the database, for each model. + """ stats = { _("Users (members and clubs)"): { "users": [User._meta.verbose_name, User.objects.count()], @@ -452,10 +451,9 @@ def stats_models(request): @login_required @can_view_app("users") def stats_users(request): - """Affiche les statistiques base de données aggrégées par user : - nombre de machines par user, d'etablissements par user, - de moyens de paiements par user, de banque par user, - de bannissement par user, etc""" + """View used to display statistics aggregated by user (number of machines, + bans, whitelists, rights etc.). + """ stats = { User._meta.verbose_name: { Machine._meta.verbose_name_plural: User.objects.annotate( @@ -496,9 +494,7 @@ def stats_users(request): @login_required @can_view_app("users") def stats_actions(request): - """Vue qui affiche les statistiques de modifications d'objets par - utilisateurs. - Affiche le nombre de modifications aggrégées par utilisateurs""" + """View used to display the number of actions, aggregated by user.""" stats = { User._meta.verbose_name: { _("actions"): User.objects.annotate(num=Count("revision")).order_by("-num")[ @@ -512,8 +508,9 @@ def stats_actions(request): @login_required @can_view_app("users") def stats_search_machine_history(request): - """View which displays the history of machines with the given - une IP or MAC adresse""" + """View used to display the history of machines with the given IP or MAC + address. + """ history_form = MachineHistorySearchForm(request.GET or None) if history_form.is_valid(): history = MachineHistorySearch() diff --git a/machines/forms.py b/machines/forms.py index 629538e0..356992a8 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -22,15 +22,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Formulaires d'ajout, edition et suppressions de : - - machines - - interfaces - - domain (noms de machine) - - alias (cname) - - service (dhcp, dns..) - - ns (serveur dns) - - mx (serveur mail) - - ports ouverts et profils d'ouverture par interface +Forms to create, edit and delete: + * machines + * interfaces + * domains (machine names) + * aliases (CNAME) + * services (DHCP, DNS...) + * NS records (DNS server) + * MX records (mail serveur) + * open ports and ports opening for interfaces """ from __future__ import unicode_literals @@ -66,7 +66,7 @@ from .models import ( class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Formulaire d'édition d'une machine""" + """Form used to edit a machine.""" class Meta: model = Machine @@ -79,14 +79,14 @@ class EditMachineForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class NewMachineForm(EditMachineForm): - """Creation d'une machine, ne renseigne que le nom""" + """Form used to create a machine.""" class Meta(EditMachineForm.Meta): fields = ["name"] class EditInterfaceForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Edition d'une interface. Edition complète""" + """Form used to edit an interface.""" class Meta: model = Interface @@ -127,15 +127,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""" + """Form used to add an interface to a machine.""" class Meta(EditInterfaceForm.Meta): fields = ["machine_type", "ipv4", "mac_address", "details"] class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Ajout d'un alias (et edition), CNAME, contenant nom et extension""" + """Form used to add and edit an alias (CNAME).""" class Meta: model = Domain @@ -153,7 +152,9 @@ class AliasForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class DomainForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Ajout et edition d'un enregistrement de nom, relié à interface""" + """Form used to add and edit a domain record, related to an + interface. + """ class Meta: model = Domain @@ -165,7 +166,7 @@ class DomainForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class DelAliasForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs objets alias""" + """Form used to delete one or several aliases.""" alias = forms.ModelMultipleChoiceField( queryset=Domain.objects.all(), @@ -182,7 +183,7 @@ class DelAliasForm(FormRevMixin, Form): class MachineTypeForm(FormRevMixin, ModelForm): - """Ajout et edition d'un machinetype, relié à un iptype""" + """Form used to add and edit a machine type, related to an IP type.""" class Meta: model = MachineType @@ -196,7 +197,7 @@ class MachineTypeForm(FormRevMixin, ModelForm): class DelMachineTypeForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs machinetype""" + """Form used to delete one or several machines types.""" machinetypes = forms.ModelMultipleChoiceField( queryset=MachineType.objects.none(), @@ -214,8 +215,9 @@ 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""" + """Form used to add an IP type. The start and stop IP addresses cannot + be changed afterwards. + """ class Meta: model = IpType @@ -228,8 +230,9 @@ class IpTypeForm(FormRevMixin, ModelForm): class EditIpTypeForm(IpTypeForm): - """Edition d'un iptype. Pas d'edition du rangev4 possible, car il faudrait - synchroniser les objets iplist""" + """Form used to edit an IP type. The start and stop IP addresses cannot + be changed. + """ class Meta(IpTypeForm.Meta): fields = [ @@ -248,7 +251,7 @@ class EditIpTypeForm(IpTypeForm): class DelIpTypeForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs iptype""" + """Form used to delete one or several IP types.""" iptypes = forms.ModelMultipleChoiceField( queryset=IpType.objects.none(), @@ -266,7 +269,7 @@ class DelIpTypeForm(FormRevMixin, Form): class ExtensionForm(FormRevMixin, ModelForm): - """Formulaire d'ajout et edition d'une extension""" + """Form used to add and edit extensions.""" class Meta: model = Extension @@ -283,7 +286,7 @@ class ExtensionForm(FormRevMixin, ModelForm): class DelExtensionForm(FormRevMixin, Form): - """Suppression d'une ou plusieurs extensions""" + """Form used to delete one or several extensions.""" extensions = forms.ModelMultipleChoiceField( queryset=Extension.objects.none(), @@ -301,7 +304,7 @@ class DelExtensionForm(FormRevMixin, Form): class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Gestion des ipv6 d'une machine""" + """Form used to manage lists of IPv6 addresses.""" class Meta: model = Ipv6List @@ -313,7 +316,7 @@ class Ipv6ListForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class SOAForm(FormRevMixin, ModelForm): - """Ajout et edition d'un SOA""" + """Form used to add and edit SOA records.""" class Meta: model = SOA @@ -325,7 +328,7 @@ class SOAForm(FormRevMixin, ModelForm): class DelSOAForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs SOA""" + """Form used to delete one or several SOA records.""" soa = forms.ModelMultipleChoiceField( queryset=SOA.objects.none(), @@ -343,7 +346,7 @@ class DelSOAForm(FormRevMixin, Form): class MxForm(FormRevMixin, ModelForm): - """Ajout et edition d'un MX""" + """Form used to add and edit MX records.""" class Meta: model = Mx @@ -358,7 +361,7 @@ class MxForm(FormRevMixin, ModelForm): class DelMxForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs MX""" + """Form used to delete one or several MX records.""" mx = forms.ModelMultipleChoiceField( queryset=Mx.objects.none(), @@ -376,9 +379,9 @@ class DelMxForm(FormRevMixin, Form): 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 """ + """Form used to add and edit NS records. Only interface names are + available because CNAME aliases should not be used in the records. + """ class Meta: model = Ns @@ -393,7 +396,7 @@ class NsForm(FormRevMixin, ModelForm): class DelNsForm(FormRevMixin, Form): - """Suppresion d'un ou plusieurs NS""" + """Form used to delete one or several NS records.""" nss = forms.ModelMultipleChoiceField( queryset=Ns.objects.none(), @@ -411,7 +414,7 @@ class DelNsForm(FormRevMixin, Form): class TxtForm(FormRevMixin, ModelForm): - """Ajout d'un txt pour une zone""" + """Form used to add and edit TXT records.""" class Meta: model = Txt @@ -423,7 +426,7 @@ class TxtForm(FormRevMixin, ModelForm): class DelTxtForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs TXT""" + """Form used to delete one or several TXT records.""" txt = forms.ModelMultipleChoiceField( queryset=Txt.objects.none(), @@ -441,7 +444,7 @@ class DelTxtForm(FormRevMixin, Form): class DNameForm(FormRevMixin, ModelForm): - """Add a DNAME entry for a zone""" + """Form used to add and edit DNAME records.""" class Meta: model = DName @@ -453,7 +456,7 @@ class DNameForm(FormRevMixin, ModelForm): class DelDNameForm(FormRevMixin, Form): - """Delete a set of DNAME entries""" + """Form used to delete one or several DNAME records.""" dnames = forms.ModelMultipleChoiceField( queryset=Txt.objects.none(), @@ -471,7 +474,7 @@ class DelDNameForm(FormRevMixin, Form): class SrvForm(FormRevMixin, ModelForm): - """Ajout d'un srv pour une zone""" + """Form used to add and edit SRV records.""" class Meta: model = Srv @@ -483,7 +486,7 @@ class SrvForm(FormRevMixin, ModelForm): class DelSrvForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs Srv""" + """Form used to delete one or several SRV records.""" srv = forms.ModelMultipleChoiceField( queryset=Srv.objects.none(), @@ -501,8 +504,7 @@ class DelSrvForm(FormRevMixin, Form): class NasForm(FormRevMixin, ModelForm): - """Ajout d'un type de nas (machine d'authentification, - swicths, bornes...)""" + """Form used to create and edit NAS devices.""" class Meta: model = Nas @@ -514,7 +516,7 @@ class NasForm(FormRevMixin, ModelForm): class DelNasForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs nas""" + """Form used to delete one or several NAS devices.""" nas = forms.ModelMultipleChoiceField( queryset=Nas.objects.none(), @@ -532,7 +534,7 @@ class DelNasForm(FormRevMixin, Form): class RoleForm(FormRevMixin, ModelForm): - """Add and edit role.""" + """Form used to add and edit roles.""" class Meta: model = Role @@ -547,7 +549,7 @@ class RoleForm(FormRevMixin, ModelForm): class DelRoleForm(FormRevMixin, Form): - """Deletion of one or several roles.""" + """Form used to delete one or several roles.""" role = forms.ModelMultipleChoiceField( queryset=Role.objects.none(), @@ -565,7 +567,7 @@ class DelRoleForm(FormRevMixin, Form): class ServiceForm(FormRevMixin, ModelForm): - """Ajout et edition d'une classe de service : dns, dhcp, etc""" + """Form to add and edit services (DHCP, DNS etc.).""" class Meta: model = Service @@ -589,7 +591,7 @@ class ServiceForm(FormRevMixin, ModelForm): class DelServiceForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs service""" + """Form used to delete one or several services.""" service = forms.ModelMultipleChoiceField( queryset=Service.objects.none(), @@ -607,7 +609,7 @@ class DelServiceForm(FormRevMixin, Form): class VlanForm(FormRevMixin, ModelForm): - """Ajout d'un vlan : id, nom""" + """Form used to add and edit VLANs.""" class Meta: model = Vlan @@ -619,7 +621,7 @@ class VlanForm(FormRevMixin, ModelForm): class EditOptionVlanForm(FormRevMixin, ModelForm): - """Ajout d'un vlan : id, nom""" + """Form used to edit the options of a VLAN.""" class Meta: model = Vlan @@ -631,7 +633,7 @@ class EditOptionVlanForm(FormRevMixin, ModelForm): class DelVlanForm(FormRevMixin, Form): - """Suppression d'un ou plusieurs vlans""" + """Form used to delete one or several VLANs.""" vlan = forms.ModelMultipleChoiceField( queryset=Vlan.objects.none(), @@ -649,8 +651,7 @@ class DelVlanForm(FormRevMixin, Form): class EditOuverturePortConfigForm(FormRevMixin, ModelForm): - """Edition de la liste des profils d'ouverture de ports - pour l'interface""" + """Form to edit the ports opening list of an interface.""" class Meta: model = Interface @@ -664,8 +665,7 @@ class EditOuverturePortConfigForm(FormRevMixin, ModelForm): class EditOuverturePortListForm(FormRevMixin, ModelForm): - """Edition de la liste des ports et profils d'ouverture - des ports""" + """Form used to add and edit ports opening lists.""" class Meta: model = OuverturePortList @@ -677,7 +677,7 @@ class EditOuverturePortListForm(FormRevMixin, ModelForm): class SshFpForm(FormRevMixin, ModelForm): - """Edits a SSHFP record.""" + """Form used to add and edit SSHFP records.""" class Meta: model = SshFp diff --git a/machines/models.py b/machines/models.py index 3925e236..fa4db48c 100644 --- a/machines/models.py +++ b/machines/models.py @@ -63,8 +63,13 @@ from re2o.mixins import AclMixin, RevMixin class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): - """ Class définissant une machine, object parent user, objets fils - interfaces""" + """Machine. + + Attributes: + user: the user who owns the machine. + name: the name of the machine. + active: whether the machine is active. + """ user = models.ForeignKey("users.User", on_delete=models.CASCADE) name = models.CharField( @@ -81,8 +86,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): verbose_name_plural = _("machines") def linked_objects(self): - """Return linked objects : machine and domain. - Usefull in history display""" + """Get the related interface and domain.""" return chain( self.interface_set.all(), Domain.objects.filter(interface_parent__in=self.interface_set.all()), @@ -90,11 +94,11 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): @staticmethod def can_change_user(user_request, *_args, **_kwargs): - """Checks if an user is allowed to change the user who owns a + """Check if an user is allowed to change the user who owns a Machine. Args: - user_request: The user requesting to change owner. + user_request: the user requesting to change owner. Returns: A tuple with a boolean stating if edition is allowed and an @@ -111,10 +115,15 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): @staticmethod def can_view_all(user_request, *_args, **_kwargs): - """Vérifie qu'on peut bien afficher l'ensemble des machines, - droit particulier correspondant - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can view all machines. + + Args: + user_request: the user requesting to view the machines. + + Returns: + A tuple indicating whether the user can view all machines and a + message if not. + """ if not user_request.has_perm("machines.view_machine"): return ( False, @@ -125,11 +134,17 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): @staticmethod def can_create(user_request, userid, *_args, **_kwargs): - """Vérifie qu'un user qui fait la requète peut bien créer la machine - et n'a pas atteint son quota, et crée bien une machine à lui - :param user_request: Utilisateur qui fait la requête - :param userid: id de l'user dont on va créer une machine - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can create the machine, did not reach his quota + and create a machine for themselves. + + Args: + user_request: the user requesting to create the machine. + userid: the ID of the owner of the machine to be created. + + Returns: + A tuple indicating whether the user can create the machine and a + message if not. + """ try: user = users.models.User.objects.get(pk=userid) except users.models.User.DoesNotExist: @@ -165,11 +180,15 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): return True, None, None def can_edit(self, user_request, *args, **kwargs): - """Vérifie qu'on peut bien éditer cette instance particulière (soit - machine de soi, soit droit particulier - :param self: instance machine à éditer - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison le cas échéant""" + """Check if the user can edit the current instance of Machine (self). + + Args: + user_request: the user requesting to edit self. + + Returns: + A tuple indicating whether the user can edit self and a + message if not. + """ if self.user != user_request: can_user, _message, permissions = self.user.can_edit( self.user, user_request, *args, **kwargs @@ -183,11 +202,15 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): return True, None, None def can_delete(self, user_request, *args, **kwargs): - """Vérifie qu'on peut bien supprimer cette instance particulière (soit - machine de soi, soit droit particulier - :param self: instance machine à supprimer - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can delete the current instance of Machine (self). + + Args: + user_request: the user requesting to delete self. + + Returns: + A tuple indicating whether the user can delete self and a + message if not. + """ if self.user != user_request: can_user, _message, permissions = self.user.can_edit( self.user, user_request, *args, **kwargs @@ -204,11 +227,15 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): return True, None, None def can_view(self, user_request, *_args, **_kwargs): - """Vérifie qu'on peut bien voir cette instance particulière (soit - machine de soi, soit droit particulier - :param self: instance machine à éditer - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can view the current instance of Machine (self). + + Args: + user_request: the user requesting to view self. + + Returns: + A tuple indicating whether the user can view self and a + message if not. + """ if ( not user_request.has_perm("machines.view_machine") and self.user != user_request @@ -222,8 +249,10 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): @cached_property def short_name(self): - """Par defaut, renvoie le nom de la première interface - de cette machine""" + """Get the short name of the machine. + + By default, get the name of the first interface of the machine. + """ interfaces_set = self.interface_set.first() if interfaces_set: return str(interfaces_set.domain.name) @@ -232,14 +261,15 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): @cached_property def complete_name(self): - """Par defaut, renvoie le nom de la première interface - de cette machine""" + """Get the complete name of the machine. + + By default, get the name of the first interface of the machine. + """ return str(self.interface_set.first()) @cached_property def all_short_names(self): - """Renvoie de manière unique, le nom des interfaces de cette - machine""" + """Get the short names of all interfaces of the machine.""" return ( Domain.objects.filter(interface_parent__machine=self) .values_list("name", flat=True) @@ -248,12 +278,15 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): @cached_property def get_name(self): - """Return a name : user provided name or first interface name""" + """Get the name of the machine. + + The name can be provided by the user, else the short name is used. + """ return self.name or self.short_name @classmethod def mass_delete(cls, machine_queryset): - """Mass delete for machine queryset""" + """Mass delete for machine queryset.""" from topologie.models import AccessPoint Domain.objects.filter( @@ -278,7 +311,7 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): @cached_property def all_complete_names(self): - """Renvoie tous les tls complets de la machine""" + """Get the complete names of all interfaces of the machine.""" return [ str(domain) for domain in Domain.objects.filter( @@ -296,7 +329,12 @@ class Machine(RevMixin, FieldPermissionModelMixin, AclMixin, models.Model): class MachineType(RevMixin, AclMixin, models.Model): - """ Type de machine, relié à un type d'ip, affecté aux interfaces""" + """Machine type, related to an IP type and assigned to interfaces. + + Attributes: + name: the name of the machine type. + ip_type: the IP type of the machine type. + """ name = models.CharField(max_length=255) ip_type = models.ForeignKey( @@ -312,18 +350,18 @@ class MachineType(RevMixin, AclMixin, models.Model): verbose_name_plural = _("machine types") def all_interfaces(self): - """ Renvoie toutes les interfaces (cartes réseaux) de type - machinetype""" + """Get all interfaces of the current machine type (self).""" return Interface.objects.filter(machine_type=self) @staticmethod def can_use_all(user_request, *_args, **_kwargs): - """Check if an user can use every MachineType. + """Check if an user can use all machine types. Args: - user_request: The user requesting edition. + user_request: the user requesting to use all machine types. + Returns: - A tuple with a boolean stating if user can acces and an explanation + A tuple with a boolean stating if user can access and an explanation message is acces is not allowed. """ if not user_request.has_perm("machines.use_all_machinetype"): @@ -339,7 +377,23 @@ class MachineType(RevMixin, AclMixin, models.Model): class IpType(RevMixin, AclMixin, models.Model): - """ Type d'ip, définissant un range d'ip, affecté aux machine types""" + """IP type, defining an IP range and assigned to machine types. + + Attributes: + name: the name of the IP type. + extension: the extension related to the IP type. + need_infra: whether the 'infra' right is required. + domaine_ip_start: the start IPv4 address of the IP type. + domaine_ip_stop: the stop IPv4 address of the IP type. + domaine_ip_network: the IPv4 network containg the IP range (optional). + domaine_ip_netmask: the netmask of the domain's IPv4 range. + reverse_v4: whether reverse DNS is enabled for IPv4. + prefix_v6: the IPv6 prefix. + prefix_v6_length: the IPv6 prefix length. + reverse_v6: whether reverse DNS is enabled for IPv6. + vlan: the VLAN related to the IP type. + ouverture_ports: the ports opening list related to the IP type. + """ name = models.CharField(max_length=255) extension = models.ForeignKey("Extension", on_delete=models.PROTECT) @@ -380,27 +434,30 @@ class IpType(RevMixin, AclMixin, models.Model): @cached_property def ip_range(self): - """ Renvoie un objet IPRange à partir de l'objet IpType""" + """Get the IPRange object from the current IP type.""" return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop) @cached_property def ip_set(self): - """ Renvoie une IPSet à partir de l'iptype""" + """Get the IPSet object from the current IP type.""" return IPSet(self.ip_range) @cached_property def ip_set_as_str(self): - """ Renvoie une liste des ip en string""" + """Get the list of the IP addresses in the range as strings.""" return [str(x) for x in self.ip_set] @cached_property def ip_set_cidrs_as_str(self): - """Renvoie la liste des cidrs du range en str""" + """Get the list of CIDRs from the IP range.""" return [str(ip_range) for ip_range in self.ip_set.iter_cidrs()] @cached_property def ip_set_full_info(self): - """Iter sur les range cidr, et renvoie network, broacast , etc""" + """Get the set of all information for IPv4. + + Iter over the CIDRs and get the network, broadcast etc. + """ return [ { "network": str(ip_set.network), @@ -415,6 +472,7 @@ class IpType(RevMixin, AclMixin, models.Model): @cached_property def ip6_set_full_info(self): + """Get the set of all information for IPv6.""" if self.prefix_v6: return { "network": str(self.prefix_v6), @@ -428,8 +486,7 @@ class IpType(RevMixin, AclMixin, models.Model): @cached_property def ip_network(self): - """Renvoie le network parent du range start-stop, si spécifié - Différent de ip_set_cidrs ou iP_set, car lui est supérieur ou égal""" + """Get the parent IP network of the range, if specified.""" if self.domaine_ip_network: return IPNetwork( str(self.domaine_ip_network) + "/" + str(self.domaine_ip_netmask) @@ -438,7 +495,7 @@ class IpType(RevMixin, AclMixin, models.Model): @cached_property def ip_net_full_info(self): - """Renvoie les infos du network contenant du range""" + """Get all information on the network including the range.""" if self.ip_network: return { "network": str(self.ip_network.network), @@ -453,35 +510,39 @@ class IpType(RevMixin, AclMixin, models.Model): @cached_property def complete_prefixv6(self): - """Return the complete prefix v6 as cidr""" + """Get the complete prefix v6 as CIDR.""" return str(self.prefix_v6) + "/" + str(self.prefix_v6_length) def ip_objects(self): - """ Renvoie tous les objets ipv4 relié à ce type""" + """Get all IPv4 objects related to the current IP type.""" return IpList.objects.filter(ip_type=self) def free_ip(self): - """ Renvoie toutes les ip libres associées au type donné (self)""" + """Get all free IP addresses related to the current IP type.""" return IpList.objects.filter(interface__isnull=True).filter(ip_type=self) def gen_ip_range(self): - """ Cree les IpList associées au type self. Parcours pédestrement et - crée les ip une par une. Si elles existent déjà, met à jour le type - associé à l'ip""" - # Creation du range d'ip dans les objets iplist + """Create the IpList objects related to the current IP type. + + Goes through the IP addresses on by one. If they already exist, update the + type related to the IP addresses. + """ + # Creation of the IP range in the IpList objects ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in self.ip_range] listes_ip = IpList.objects.filter(ipv4__in=[str(ip) for ip in self.ip_range]) - # Si il n'y a pas d'ip, on les crée + # If there are no IP addresses, create them if not listes_ip: IpList.objects.bulk_create(ip_obj) - # Sinon on update l'ip_type + # Else, up the IP type else: listes_ip.update(ip_type=self) return def del_ip_range(self): - """ Methode dépréciée, IpList est en mode cascade et supprimé - automatiquement""" + """Deprecated method. + + Delete the IP range and the IpList in cascade. + """ if Interface.objects.filter(ipv4__in=self.ip_objects()): raise ValidationError( _( @@ -494,7 +555,9 @@ class IpType(RevMixin, AclMixin, models.Model): ip.delete() def check_replace_prefixv6(self): - """Remplace les prefixv6 des interfaces liées à ce type d'ip""" + """Replace the IPv6 prefixes of the interfaces related to the current + IP type. + """ if not self.prefix_v6: return else: @@ -506,6 +569,9 @@ class IpType(RevMixin, AclMixin, models.Model): ipv6.check_and_replace_prefix(prefix=self.prefix_v6) def get_associated_ptr_records(self): + """Get the PTR records related to the current IP type, if reverse DNS + is enabled for IPv4. + """ from re2o.utils import all_active_assigned_interfaces if self.reverse_v4: @@ -518,6 +584,9 @@ class IpType(RevMixin, AclMixin, models.Model): return None def get_associated_ptr_v6_records(self): + """Get the PTR records related to the current IP type, if reverse DNS + is enabled for IPv6. + """ from re2o.utils import all_active_interfaces if self.reverse_v6: @@ -526,16 +595,18 @@ class IpType(RevMixin, AclMixin, models.Model): return None def clean(self): - """ Nettoyage. Vérifie : - - Que ip_stop est après ip_start - - Qu'on ne crée pas plus gros qu'un /16 - - Que le range crée ne recoupe pas un range existant - - Formate l'ipv6 donnée en /64""" + """ + Check if: + * ip_stop is after ip_start + * the range is not more than a /16 + * the range is disjoint from existing ranges + * the IPv6 prefix is formatted + """ if not self.domaine_ip_start or not self.domaine_ip_stop: raise ValidationError(_("Domaine IPv4 start and stop must be valid")) if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop): raise ValidationError(_("Range end must be after range start...")) - # On ne crée pas plus grand qu'un /16 + # The range should not be more than a /16 if self.ip_range.size > 65536: raise ValidationError( _( @@ -543,16 +614,16 @@ class IpType(RevMixin, AclMixin, models.Model): " a larger one than a /16." ) ) - # On check que les / ne se recoupent pas + # Check that the ranges do not overlap for element in IpType.objects.all().exclude(pk=self.pk): if not self.ip_set.isdisjoint(element.ip_set): raise ValidationError( _("The specified range is not disjoint from existing" " ranges.") ) - # On formate le prefix v6 + # Format the IPv6 prefix if self.prefix_v6: self.prefix_v6 = str(IPNetwork(self.prefix_v6 + "/64").network) - # On vérifie qu'un domaine network/netmask contiens bien le domaine ip start-stop + # Check if the domain network/netmask contains the domain IP start-stop if self.domaine_ip_network: if ( not self.domaine_ip_start in self.ip_network @@ -573,10 +644,15 @@ class IpType(RevMixin, AclMixin, models.Model): @staticmethod def can_use_all(user_request, *_args, **_kwargs): - """Superdroit qui permet d'utiliser toutes les extensions sans - restrictions - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can use all IP types without restrictions. + + Args: + user_request: the user requesting to use all IP types. + + Returns: + A tuple indicating whether the user can use all IP types and a + message if not. + """ return ( user_request.has_perm("machines.use_all_iptype"), None, @@ -588,13 +664,25 @@ class IpType(RevMixin, AclMixin, models.Model): class Vlan(RevMixin, AclMixin, models.Model): - """ Un vlan : vlan_id et nom - On limite le vlan id entre 0 et 4096, comme défini par la norme""" + """VLAN. + + The VLAN ID is limited between 0 and 4096. + + Attributes: + vlan_id: the ID of the VLAN. + name: the name of the VLAN. + comment: the comment to describe the VLAN. + arp_protect: whether ARP protection is enabled. + dhcp_snooping: whether DHCP snooping is enabled. + dhcpv6_snooping: whether DHCPv6 snooping is enabled. + igmp: whether IGMP (v4 multicast management) is enabled. + mld: whether MLD (v6 multicast management) is enabled. + """ 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 + # Additional settings arp_protect = models.BooleanField(default=False) dhcp_snooping = models.BooleanField(default=False) dhcpv6_snooping = models.BooleanField(default=False) @@ -611,9 +699,16 @@ class Vlan(RevMixin, AclMixin, models.Model): class Nas(RevMixin, AclMixin, models.Model): - """ Les nas. Associé à un machine_type. - Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour - le radius. Champ autocapture de la mac à true ou false""" + """NAS device, related to a machine type. + + Attributes: + name: the name of the NAS device. + nas_type: the type of the NAS device. + machine_type: the machine type of the NAS device. + port_access_mode: the access mode of the port related to the NAS + device. + autocapture_mac: whether MAC autocapture is enabled. + """ default_mode = "802.1X" AUTH = (("802.1X", "802.1X"), ("Mac-address", _("MAC-address"))) @@ -640,10 +735,21 @@ class Nas(RevMixin, AclMixin, models.Model): class SOA(RevMixin, AclMixin, models.Model): - """ - Un enregistrement SOA associé à une extension - Les valeurs par défault viennent des recommandations RIPE : + """SOA record. + + Default values come from the RIPE's recommendations here: https://www.ripe.net/publications/docs/ripe-203 + + Attributes: + name: the name of the SOA record. + mail: the contact email address of the SOA record. + refresh: the number of seconds before the secondary DNS need to + refresh. + retry: the number of seconds before the secondary DNS need to retry + in case of timeout. + expire: the number of seconds before the secondary DNS stop answering + requests in case of timeout. + ttl: the Time To Live of the SOA record. """ name = models.CharField(max_length=255) @@ -684,11 +790,11 @@ class SOA(RevMixin, AclMixin, models.Model): @cached_property def dns_soa_param(self): """ - Renvoie la partie de l'enregistrement SOA correspondant aux champs : - ; refresh - ; retry - ; expire - ; TTL + Get the following fields of the SOA record: + * refresh + * retry + * expire + * ttl """ return ( " {refresh}; refresh\n" @@ -704,24 +810,34 @@ class SOA(RevMixin, AclMixin, models.Model): @cached_property def dns_soa_mail(self): - """ Renvoie le mail dans l'enregistrement SOA """ + """Get the contact email address formatted in the SOA record.""" mail_fields = str(self.mail).split("@") return mail_fields[0].replace(".", "\\.") + "." + mail_fields[1] + "." @classmethod def new_default_soa(cls): - """ Fonction pour créer un SOA par défaut, utile pour les nouvelles - extensions . - /!\ Ne jamais supprimer ou renommer cette fonction car elle est - utilisée dans les migrations de la BDD. """ + """Create a new default SOA, useful for new extensions. + + /!\ Never delete or rename this function, it is used to make migrations + of the database. + """ return cls.objects.get_or_create( name=_("SOA to edit"), mail="postmaster@example.com" )[0].pk class Extension(RevMixin, AclMixin, models.Model): - """ Extension dns type example.org. Précise si tout le monde peut - l'utiliser, associé à un origin (ip d'origine)""" + """Extension. + + DNS extension such as example.org. + + Attributes: + name: the name of the extension. + need_infra: whether the 'infra' right is required. + origin: the A record (IpList) related to the extension. + origin_v6: the AAAA record related to the extension. + soa: the SOA record related to the extension. + """ name = models.CharField( max_length=255, @@ -757,7 +873,7 @@ class Extension(RevMixin, AclMixin, models.Model): @cached_property def dns_entry(self): - """ Une entrée DNS A et AAAA sur origin (zone self)""" + """A DNS A and AAAA entry on origin for the current extension.""" entry = "" if self.origin: entry += "@ IN A " + str(self.origin) @@ -768,6 +884,7 @@ class Extension(RevMixin, AclMixin, models.Model): return entry def get_associated_sshfp_records(self): + """Get all SSHFP records related to the extension.""" from re2o.utils import all_active_assigned_interfaces return ( @@ -777,6 +894,7 @@ class Extension(RevMixin, AclMixin, models.Model): ) def get_associated_a_records(self): + """Get all A records related to the extension.""" from re2o.utils import all_active_assigned_interfaces return ( @@ -786,6 +904,7 @@ class Extension(RevMixin, AclMixin, models.Model): ) def get_associated_aaaa_records(self): + """Get all AAAA records related to the extension.""" from re2o.utils import all_active_interfaces return all_active_interfaces(full=True).filter( @@ -793,6 +912,7 @@ class Extension(RevMixin, AclMixin, models.Model): ) def get_associated_cname_records(self): + """Get all CNAME records related to the extension.""" from re2o.utils import all_active_assigned_interfaces return ( @@ -802,14 +922,20 @@ class Extension(RevMixin, AclMixin, models.Model): ) def get_associated_dname_records(self): + """Get all DNAME records related to the extension.""" return DName.objects.filter(alias=self) @staticmethod def can_use_all(user_request, *_args, **_kwargs): - """Superdroit qui permet d'utiliser toutes les extensions sans - restrictions - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can use all extensions without restrictions. + + Args: + user_request: the user requesting to use all extensions. + + Returns: + A tuple indicating whether the user can use all extensions and a + message if not. + """ can = user_request.has_perm("machines.use_all_extension") return ( can, @@ -827,9 +953,16 @@ class Extension(RevMixin, AclMixin, models.Model): class Mx(RevMixin, AclMixin, models.Model): - """ Entrées des MX. Enregistre la zone (extension) associée et la - priorité - Todo : pouvoir associer un MX à une interface """ + """MX record. + + TODO link an MX record to an interface. + + Attributes: + zone: the extension related to the MX record. + priority: the priority of the MX record. + name: the domain related to the MX record. + ttl: the Time To Live of the MX record. + """ zone = models.ForeignKey("Extension", on_delete=models.PROTECT) priority = models.PositiveIntegerField() @@ -845,8 +978,8 @@ class Mx(RevMixin, AclMixin, models.Model): @cached_property def dns_entry(self): - """Renvoie l'entrée DNS complète pour un MX à mettre dans les - fichiers de zones""" + """Get the complete DNS entry of the MX record, to put in zone files. + """ return "@ IN MX {prior} {name}".format( prior=str(self.priority).ljust(3), name=str(self.name) ) @@ -856,7 +989,13 @@ class Mx(RevMixin, AclMixin, models.Model): class Ns(RevMixin, AclMixin, models.Model): - """Liste des enregistrements name servers par zone considéérée""" + """NS record. + + Attributes: + zone: the extension related to the NS record. + ns: the domain related to the NS record. + ttl: the Time To Live of the NS record. + """ zone = models.ForeignKey("Extension", on_delete=models.PROTECT) ns = models.ForeignKey("Domain", on_delete=models.PROTECT) @@ -871,7 +1010,8 @@ class Ns(RevMixin, AclMixin, models.Model): @cached_property def dns_entry(self): - """Renvoie un enregistrement NS complet pour les filezones""" + """Get the complete DNS entry of the NS record, to put in zone files. + """ return "@ IN NS " + str(self.ns) def __str__(self): @@ -879,7 +1019,14 @@ class Ns(RevMixin, AclMixin, models.Model): class Txt(RevMixin, AclMixin, models.Model): - """ Un enregistrement TXT associé à une extension""" + """TXT record. + + Attributes: + zone: the extension related to the TXT record. + field1: the first field of the TXT record. + field2: the second field of the TXT record. + ttl: the Time To Live of the TXT record. + """ zone = models.ForeignKey("Extension", on_delete=models.PROTECT) field1 = models.CharField(max_length=255) @@ -898,12 +1045,19 @@ class Txt(RevMixin, AclMixin, models.Model): @cached_property def dns_entry(self): - """Renvoie l'enregistrement TXT complet pour le fichier de zone""" + """Get the complete DNS entry of the TXT record, to put in zone files. + """ return str(self.field1).ljust(15) + " IN TXT " + str(self.field2) class DName(RevMixin, AclMixin, models.Model): - """A DNAME entry for the DNS.""" + """DNAME record. + + Attributes: + zone: the extension related to the DNAME record. + alias: the alias of the DNAME record. + ttl: the Time To Live of the DNAME record. + """ zone = models.ForeignKey("Extension", on_delete=models.PROTECT) alias = models.CharField(max_length=255) @@ -921,12 +1075,24 @@ class DName(RevMixin, AclMixin, models.Model): @cached_property def dns_entry(self): - """Returns the DNAME record for the DNS zone file.""" + """Get the complete DNS entry of the TXT record, to put in zone files. + """ return str(self.alias).ljust(15) + " IN DNAME " + str(self.zone) class Srv(RevMixin, AclMixin, models.Model): - """ A SRV record """ + """SRV record. + + Attributes: + service: the name of the service of the SRV record. + protocole: the protocol of the service of the SRV record. + extension: the extension of the SRV record. + ttl: the Time To Live of the SRV record. + priority: the priority of the target server. + weight: the relative weight for records with the same priority. + port: the TCP/UDP port of the SRV record. + target: the target server of the SRV record. + """ TCP = "TCP" UDP = "UDP" @@ -985,7 +1151,8 @@ class Srv(RevMixin, AclMixin, models.Model): @cached_property def dns_entry(self): - """Renvoie l'enregistrement SRV complet pour le fichier de zone""" + """Get the complete DNS entry of the SRV record, to put in zone files. + """ return ( str(self.service) + "._" @@ -1006,7 +1173,14 @@ class Srv(RevMixin, AclMixin, models.Model): class SshFp(RevMixin, AclMixin, models.Model): - """A fingerprint of an SSH public key""" + """SSH public key fingerprint. + + Attributes: + machine: the machine related to the SSH fingerprint. + pub_key_entry: the SSH public key related to the SSH fingerprint. + algo: the algorithm used for the SSH fingerprint. + comment: the comment to describe the SSH fingerprint. + """ ALGO = ( ("ssh-rsa", "ssh-rsa"), @@ -1025,7 +1199,7 @@ class SshFp(RevMixin, AclMixin, models.Model): @cached_property def algo_id(self): - """Return the id of the algorithm for this key""" + """Get the ID of the algorithm for this key.""" if "ecdsa" in self.algo: return 3 elif "rsa" in self.algo: @@ -1035,8 +1209,10 @@ class SshFp(RevMixin, AclMixin, models.Model): @cached_property def hash(self): - """Return the hashess for the pub key with correct id - cf RFC, 1 is sha1 , 2 sha256""" + """Get the hashes for the pub key with correct ID. + + See RFC: 1 is sha1 , 2 is sha256. + """ return { "1": hashlib.sha1(base64.b64decode(self.pub_key_entry)).hexdigest(), "2": hashlib.sha256(base64.b64decode(self.pub_key_entry)).hexdigest(), @@ -1061,13 +1237,16 @@ class SshFp(RevMixin, AclMixin, models.Model): 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 - typemachine - - une onetoone vers IpList pour attribution ipv4 - - le type parent associé au range ip et à l'extension - - un objet domain associé contenant son nom - - la liste des ports oiuvert""" + """Interface, the key object of the app machines. + + Attributes: + ipv4: the IPv4 address (IpList) of the interface. + mac_address: the MAC address of the interface. + machine: the machine to which the interface belongs. + machine_type: the machine type of the interface. + details: the details to describe the interface. + port_lists: the ports opening list of the interface. + """ ipv4 = models.OneToOneField( "IpList", on_delete=models.PROTECT, blank=True, null=True @@ -1088,15 +1267,20 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @cached_property def is_active(self): - """ Renvoie si une interface doit avoir accès ou non """ + """Get the state of the interface. + + The interface is active if the related machine is active and the owner + has access. + """ machine = self.machine user = self.machine.user return machine.active and user.has_access() @cached_property def ipv6_slaac(self): - """ Renvoie un objet type ipv6 à partir du prefix associé à - l'iptype parent""" + """Get the IPv6 type object from the prefix related to the parent IP + type. + """ if self.machine_type.ip_type.prefix_v6: return EUI(self.mac_address).ipv6( IPNetwork(self.machine_type.ip_type.prefix_v6).network @@ -1106,7 +1290,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @cached_property def gen_ipv6_dhcpv6(self): - """Cree une ip, à assigner avec dhcpv6 sur une machine""" + """Create an IPv6 address to assign with DHCPv6.""" prefix_v6 = self.machine_type.ip_type.prefix_v6.encode().decode("utf-8") if not prefix_v6: return None @@ -1116,7 +1300,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @cached_property def get_vendor(self): - """Retourne le vendeur associé à la mac de l'interface""" + """Get the vendor from the MAC address of the interface.""" mac = EUI(self.mac_address) try: oui = mac.oui @@ -1126,7 +1310,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return vendor def sync_ipv6_dhcpv6(self): - """Affecte une ipv6 dhcpv6 calculée à partir de l'id de la machine""" + """Assign an IPv6 address by DHCPv6, computed from the interface's ID. + """ ipv6_dhcpv6 = self.gen_ipv6_dhcpv6 if not ipv6_dhcpv6: return @@ -1138,10 +1323,9 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return def sync_ipv6_slaac(self): - """Cree, mets à jour et supprime si il y a lieu l'ipv6 slaac associée - à la machine - Sans prefixe ipv6, on return - Si l'ip slaac n'est pas celle qu'elle devrait être, on maj""" + """Create, update and delete if necessary the IPv6 SLAAC related to the + interface. + """ ipv6_slaac = self.ipv6_slaac if not ipv6_slaac: return @@ -1153,7 +1337,8 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): ipv6_object.save() def sync_ipv6(self): - """Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi""" + """Create and update the IPv6 addresses according to the IPv6 mode set. + """ if preferences.models.OptionalMachine.get_cached_value("ipv6_mode") == "SLAAC": self.sync_ipv6_slaac() elif ( @@ -1164,9 +1349,11 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return def ipv6(self): - """ Renvoie le queryset de la liste des ipv6 - On renvoie l'ipv6 slaac que si le mode slaac est activé - (et non dhcpv6)""" + """Get the queryset of the IPv6 addresses list. + + The IPv6 SLAAC is returned only if SLAAC mode is enabled (and not + DHCPv6). + """ if preferences.models.OptionalMachine.get_cached_value("ipv6_mode") == "SLAAC": return self.ipv6list.all() elif ( @@ -1177,19 +1364,22 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return [] def mac_bare(self): - """ Formatage de la mac type mac_bare""" + """Get the mac_bare formatted MAC address.""" return str(EUI(self.mac_address, dialect=mac_bare)).lower() def filter_macaddress(self): - """ Tente un formatage mac_bare, si échoue, lève une erreur de - validation""" + """Format the MAC address as mac_bare. + + Raises: + ValidationError: the MAC address cannot be formatted as mac_bare. + """ try: self.mac_address = str(EUI(self.mac_address, dialect=default_dialect())) except: raise ValidationError(_("The given MAC address is invalid.")) def assign_ipv4(self): - """ Assigne une ip à l'interface """ + """Assign an IPv4 address to the interface.""" free_ips = self.machine_type.ip_type.free_ip() if free_ips: self.ipv4 = free_ips[0] @@ -1200,18 +1390,27 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return def unassign_ipv4(self): - """ Sans commentaire, désassigne une ipv4""" + """Unassign the IPv4 address of the interface.""" self.ipv4 = None @classmethod def mass_unassign_ipv4(cls, interface_list): - """Unassign ipv4 to multiple interfaces""" + """Unassign IPv4 addresses to multiple interfaces. + + Args: + interface_list: the list of interfaces to be updated. + """ with transaction.atomic(), reversion.create_revision(): interface_list.update(ipv4=None) reversion.set_comment("IPv4 unassignment") @classmethod def mass_assign_ipv4(cls, interface_list): + """Assign IPv4 addresses to multiple interfaces. + + Args: + interface_list: the list of interfaces to be updated. + """ for interface in interface_list: with transaction.atomic(), reversion.create_revision(): interface.assign_ipv4() @@ -1219,33 +1418,33 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): reversion.set_comment("IPv4 assignment") def update_type(self): - """ Lorsque le machinetype est changé de type d'ip, on réassigne""" + """Reassign addresses when the IP type of the machine type changed.""" self.clean() self.save() def has_private_ip(self): - """ True si l'ip associée est privée""" + """Check if the IPv4 address assigned is private.""" 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)""" + """Check if the interface has a public IP address.""" 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. + """Format the MAC address as mac_bare (see filter_mac) and assign an + IPv4 address in the appropriate range if the current address is + nonexistent or inconsistent. + + 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, "machine_type"): raise ValidationError(_("The selected IP type is invalid.")) self.filter_macaddress() @@ -1266,7 +1465,7 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): def save(self, *args, **kwargs): self.filter_macaddress() - # On verifie la cohérence en forçant l'extension par la méthode + # Check the consistency by forcing the extension if self.ipv4: if self.machine_type.ip_type != self.ipv4.ip_type: raise ValidationError( @@ -1277,11 +1476,17 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @staticmethod def can_create(user_request, machineid, *_args, **_kwargs): - """Verifie que l'user a les bons droits infra pour créer - une interface, ou bien que la machine appartient bien à l'user - :param macineid: Id de la machine parente de l'interface - :param user_request: instance utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can create an interface, or that the machine is + owned by the user. + + Args: + user_request: the user requesting to create the interface. + machineid: the ID of the machine related to the interface. + + Returns: + A tuple indicating whether the user can create the interface and a + message if not. + """ try: machine = Machine.objects.get(pk=machineid) except Machine.DoesNotExist: @@ -1321,8 +1526,14 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @staticmethod def can_change_machine(user_request, *_args, **_kwargs): - """Check if a user can change the machine associated with an - Interface object """ + """Check if the user can edit the machine. + Args: + user_request: the user requesting to edit the machine. + + Returns: + A tuple indicating whether the user can edit the machine and a + message if not. + """ can = user_request.has_perm("machines.change_interface_machine") return ( can, @@ -1331,11 +1542,16 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): ) def can_edit(self, user_request, *args, **kwargs): - """Verifie que l'user a les bons droits infra pour editer - cette instance interface, ou qu'elle lui appartient - :param self: Instance interface à editer - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can edit the current interface (self), or that it + is owned by the user. + + Args: + user_request: the user requesting to edit self. + + Returns: + A tuple indicating whether the user can edit self and a + message if not. + """ if self.machine.user != user_request: can_user, _message, permissions = self.machine.user.can_edit( user_request, *args, **kwargs @@ -1349,11 +1565,16 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None, None def can_delete(self, user_request, *args, **kwargs): - """Verifie que l'user a les bons droits delete object pour del - cette instance interface, ou qu'elle lui appartient - :param self: Instance interface à del - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can delete the current interface (self), or that + it is owned by the user. + + Args: + user_request: the user requesting to delete self. + + Returns: + A tuple indicating whether the user can delete self and a + message if not. + """ if self.machine.user != user_request: can_user, _message, permissions = self.machine.user.can_edit( user_request, *args, **kwargs @@ -1370,11 +1591,16 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None, None def can_view(self, user_request, *_args, **_kwargs): - """Vérifie qu'on peut bien voir cette instance particulière avec - droit view objet ou qu'elle appartient à l'user - :param self: instance interface à voir - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can view the current interface (self), or that it + is owned by the user. + + Args: + user_request: the user requesting to view self. + + Returns: + A tuple indicating whether the user can view self and a + message if not. + """ if ( not user_request.has_perm("machines.view_interface") and self.machine.user != user_request @@ -1399,7 +1625,13 @@ class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): - """ A list of IPv6 """ + """IPv6 addresses list. + + Args: + ipv6: the IPv6 address of the list. + interface: the interface related to the list. + slaac_ip: whether SLAAC mode is enabled. + """ ipv6 = models.GenericIPAddressField(protocol="IPv6") interface = models.ForeignKey( @@ -1420,11 +1652,17 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @staticmethod def can_create(user_request, interfaceid, *_args, **_kwargs): - """Verifie que l'user a les bons droits infra pour créer - une ipv6, ou possède l'interface associée - :param interfaceid: Id de l'interface associée à cet objet domain - :param user_request: instance utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can create an IPv6 address for the given + interface, or that it is owned by the user. + + Args: + user_request: the user requesting to create an IPv6 address. + interfaceid: the ID of the interface to be edited. + + Returns: + A tuple indicating whether the user can create an IPv6 address for + the given interface and a message if not. + """ try: interface = Interface.objects.get(pk=interfaceid) except Interface.DoesNotExist: @@ -1443,7 +1681,7 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @staticmethod def can_change_slaac_ip(user_request, *_args, **_kwargs): - """ Check if a user can change the slaac value """ + """Check if a user can change the SLAAC value.""" can = user_request.has_perm("machines.change_ipv6list_slaac_ip") return ( can, @@ -1454,11 +1692,16 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): ) def can_edit(self, user_request, *args, **kwargs): - """Verifie que l'user a les bons droits infra pour editer - cette instance interface, ou qu'elle lui appartient - :param self: Instance interface à editer - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can edit the current IPv6 addresses list (self), + or that the related interface is owned by the user. + + Args: + user_request: the user requesting to edit self. + + Returns: + A tuple indicating whether the user can edit self and a + message if not. + """ if self.interface.machine.user != user_request: can_user, _message, permissions = self.interface.machine.user.can_edit( user_request, *args, **kwargs @@ -1474,11 +1717,16 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None, None def can_delete(self, user_request, *args, **kwargs): - """Verifie que l'user a les bons droits delete object pour del - cette instance interface, ou qu'elle lui appartient - :param self: Instance interface à del - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can delete the current IPv6 addresses list (self), + or that the related interface is owned by the user. + + Args: + user_request: the user requesting to delete self. + + Returns: + A tuple indicating whether the user can delete self and a + message if not. + """ if self.interface.machine.user != user_request: can_user, _message, permissions = self.interface.machine.user.can_edit( user_request, *args, **kwargs @@ -1494,11 +1742,16 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None, None def can_view(self, user_request, *_args, **_kwargs): - """Vérifie qu'on peut bien voir cette instance particulière avec - droit view objet ou qu'elle appartient à l'user - :param self: instance interface à voir - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can view the current IPv6 addresses list (self), + or that the related interface is owned by the user. + + Args: + user_request: the user requesting to view self. + + Returns: + A tuple indicating whether the user can view self and a + message if not. + """ if ( not user_request.has_perm("machines.view_ipv6list") and self.interface.machine.user != user_request @@ -1517,7 +1770,7 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): self.field_permissions = {"slaac_ip": self.can_change_slaac_ip} def check_and_replace_prefix(self, prefix=None): - """Si le prefixe v6 est incorrect, on maj l'ipv6""" + """Check if the IPv6 prefix is correct and update it if not.""" prefix_v6 = prefix or self.interface.machine_type.ip_type.prefix_v6.encode().decode( "utf-8" ) @@ -1561,7 +1814,7 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): super(Ipv6List, self).clean(*args, **kwargs) def save(self, *args, **kwargs): - """Force à avoir appellé clean avant""" + """Force the call to clean before saving.""" self.full_clean() super(Ipv6List, self).save(*args, **kwargs) @@ -1570,9 +1823,18 @@ class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): - """ Objet domain. Enregistrement A et CNAME en même temps : permet de - stocker les alias et les nom de machines, suivant si interface_parent - ou cname sont remplis""" + """Domain. + + A and CNAME records at the same time: it enables to store aliases and + machine names, according to which fields are used. + + Attributes: + interface_parent: the parent interface of the domain. + name: the name of the domain (mandatory and unique). + extension: the extension of the domain. + cname: the CNAME record related to the domain. + ttl: the Time To Live of the domain. + """ interface_parent = models.OneToOneField( "Interface", on_delete=models.CASCADE, blank=True, null=True @@ -1600,8 +1862,11 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): verbose_name_plural = _("domains") def get_extension(self): - """ Retourne l'extension de l'interface parente si c'est un A - Retourne l'extension propre si c'est un cname, renvoie None sinon""" + """Get the extension of the domain. + + If it is an A record, get the extension of the parent interface. + If it is a CNAME record, get the extension of self. + """ if self.interface_parent: return self.interface_parent.machine_type.ip_type.extension elif hasattr(self, "extension"): @@ -1610,12 +1875,14 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return None def clean(self): - """ Validation : - - l'objet est bien soit A soit CNAME - - le cname est pas pointé sur lui-même - - le nom contient bien les caractères autorisés par la norme - dns et moins de 63 caractères au total - - le couple nom/extension est bien unique""" + """ + Check if: + * the object is either an A or a CNAME record + * the CNAME record does not point to itself + * the name is not over 63 characters + * the name does not contain forbidden characters + * the couple (name, extension) is unique + """ if self.get_extension(): self.extension = self.get_extension() if self.interface_parent and self.cname: @@ -1639,15 +1906,16 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @cached_property def dns_entry(self): - """ Une entrée DNS""" + """Get the DNS entry of the domain.""" if self.cname: return "{name} IN CNAME {cname}.".format( name=str(self.name).ljust(15), cname=str(self.cname) ) def save(self, *args, **kwargs): - """ Empèche le save sans extension valide. - Force à avoir appellé clean avant""" + """Prevent from saving if the extension is invalid and force the call + to clean before saving. + """ if not self.get_extension(): raise ValidationError(_("Invalid extension.")) self.full_clean() @@ -1655,11 +1923,12 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @cached_property def get_source_interface(self): - """Renvoie l'interface source : - - l'interface reliée si c'est un A - - si c'est un cname, suit le cname jusqu'à atteindre le A - et renvoie l'interface parente - Fonction récursive""" + """Get the source interface of the domain. + + If it is an A record, get the parent interface. + If it is a CNAME record, follow recursively until reaching the related + A record and get the parent interface. + """ if self.interface_parent: return self.interface_parent else: @@ -1667,11 +1936,17 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @staticmethod def can_create(user_request, interfaceid, *_args, **_kwargs): - """Verifie que l'user a les bons droits infra pour créer - un domain, ou possède l'interface associée - :param interfaceid: Id de l'interface associée à cet objet domain - :param user_request: instance utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can create a domain for the given interface, or + that it is owned by the user. + + Args: + user_request: the user requesting to create a domain. + interfaceid: the ID of the interface to be edited. + + Returns: + A tuple indicating whether the user can create a domain for + the given interface and a message if not. + """ try: interface = Interface.objects.get(pk=interfaceid) except Interface.DoesNotExist: @@ -1709,11 +1984,16 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None, None def can_edit(self, user_request, *_args, **_kwargs): - """Verifie que l'user a les bons droits pour editer - cette instance domain - :param self: Instance domain à editer - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can edit the current domain, or that the related + interface is owned by the user. + + Args: + user_request: the user requesting to edit self. + + Returns: + A tuple indicating whether the user can edit self and a + message if not. + """ if ( not user_request.has_perm("machines.change_domain") and self.get_source_interface.machine.user != user_request @@ -1729,11 +2009,16 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None, None def can_delete(self, user_request, *_args, **_kwargs): - """Verifie que l'user a les bons droits delete object pour del - cette instance domain, ou qu'elle lui appartient - :param self: Instance domain à del - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can delete the current domain, or that the related + interface is owned by the user. + + Args: + user_request: the user requesting to delete self. + + Returns: + A tuple indicating whether the user can delete self and a + message if not. + """ if ( not user_request.has_perm("machines.delete_domain") and self.get_source_interface.machine.user != user_request @@ -1749,11 +2034,16 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): return True, None, None def can_view(self, user_request, *_args, **_kwargs): - """Vérifie qu'on peut bien voir cette instance particulière avec - droit view objet ou qu'elle appartient à l'user - :param self: instance domain à voir - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if the user can view the current domain, or that the related + interface is owned by the user. + + Args: + user_request: the user requesting to view self. + + Returns: + A tuple indicating whether the user can view self and a + message if not. + """ if ( not user_request.has_perm("machines.view_domain") and self.get_source_interface.machine.user != user_request @@ -1767,6 +2057,7 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): @staticmethod def can_change_ttl(user_request, *_args, **_kwargs): + """Check if the user can change the TTL of the domain.""" can = user_request.has_perm("machines.change_ttl") return ( can, @@ -1781,7 +2072,12 @@ class Domain(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model): class IpList(RevMixin, AclMixin, models.Model): - """ A list of IPv4 """ + """IPv4 addresses list. + + Attributes: + ipv4: the IPv4 address of the list. + ip_type: the IP type of the list. + """ ipv4 = models.GenericIPAddressField(protocol="IPv4", unique=True) ip_type = models.ForeignKey("IpType", on_delete=models.CASCADE) @@ -1793,12 +2089,17 @@ class IpList(RevMixin, AclMixin, models.Model): @cached_property def need_infra(self): - """ Permet de savoir si un user basique peut assigner cette ip ou - non""" + """Check if the 'infra' right is required to assign this IP address. + """ return self.ip_type.need_infra def clean(self): - """ Erreur si l'ip_type est incorrect""" + """Clean self. + + Raises: + ValidationError: if the IPv4 address and the IP type of self do not + match. + """ if not str(self.ipv4) in self.ip_type.ip_set_as_str: raise ValidationError( _("The IPv4 address and the range of the IP type don't match.") @@ -1814,8 +2115,14 @@ class IpList(RevMixin, AclMixin, models.Model): class Role(RevMixin, AclMixin, models.Model): - """Define the role of a machine. - Allow automated generation of the server configuration. + """Role. + + It enabled to automate the generation of server configurations. + + Attributes: + role_type: the type of the role (name provided by the user). + servers: the servers related to the role. + specific_role: the specific role, e.g. DHCP server, LDAP server etc. """ ROLE = ( @@ -1868,7 +2175,14 @@ class Role(RevMixin, AclMixin, models.Model): class Service(RevMixin, AclMixin, models.Model): - """ Definition d'un service (dhcp, dns, etc)""" + """Service (DHCP, DNS...). + + Attributes: + service_type: the type of the service (provided by the user). + min_time_regen: the minimal time before regeneration. + regular_time_regen: the maximal time before regeneration. + servers: the servers related to the service. + """ service_type = models.CharField(max_length=255, blank=True, unique=True) min_time_regen = models.DurationField( @@ -1887,15 +2201,18 @@ class Service(RevMixin, AclMixin, models.Model): verbose_name_plural = _("services to generate (DHCP, DNS, ...)") def ask_regen(self): - """ Marque à True la demande de régénération pour un service x """ + """Set the demand for regen to True for the current Service (self).""" Service_link.objects.filter(service=self).exclude(asked_regen=True).update( asked_regen=True ) return def process_link(self, servers): - """ Django ne peut créer lui meme les relations manytomany avec table - intermediaire explicite""" + """Process the links between services and servers. + + Django does not create the ManyToMany relations with explicit + intermediate table. + """ for serv in servers.exclude(pk__in=Interface.objects.filter(service=self)): link = Service_link(service=self, server=serv) link.save() @@ -1910,8 +2227,11 @@ class Service(RevMixin, AclMixin, models.Model): def regen(service): - """ Fonction externe pour régérération d'un service, prend un objet service - en arg""" + """Ask regeneration for the given service. + + Args: + service: the service to be regenerated. + """ obj = Service.objects.filter(service_type=service) if obj: obj[0].ask_regen() @@ -1919,7 +2239,14 @@ def regen(service): class Service_link(RevMixin, AclMixin, models.Model): - """ Definition du lien entre serveurs et services""" + """Service server link. + + Attributes: + service: the service related to the link. + server: the server related to the link. + last_regen: datetime, the last time of the regeneration. + asked_regen: whether regeneration has been asked. + """ service = models.ForeignKey("Service", on_delete=models.CASCADE) server = models.ForeignKey("Interface", on_delete=models.CASCADE) @@ -1934,15 +2261,16 @@ class Service_link(RevMixin, AclMixin, models.Model): verbose_name_plural = _("links between service and server") def done_regen(self): - """ Appellé lorsqu'un serveur a regénéré son service""" + """Update the regen information when the server regenerated its + service.""" self.last_regen = timezone.now() self.asked_regen = False self.save() @property def need_regen(self): - """ Décide si le temps minimal écoulé est suffisant pour provoquer une - régénération de service""" + """Decide if the minimal time elapsed is enough to regenerate the + service.""" return bool( ( self.asked_regen @@ -1953,11 +2281,12 @@ class Service_link(RevMixin, AclMixin, models.Model): @need_regen.setter def need_regen(self, value): - """ - Force to set the need_regen value. True means a regen is asked and False - means a regen has been done. + """Force to set the need_regen value. - :param value: (bool) The value to set to + True means a regen is asked and False means a regen has been done. + + Args: + value: bool, the value to set. """ if not value: self.last_regen = timezone.now() @@ -1969,7 +2298,11 @@ class Service_link(RevMixin, AclMixin, models.Model): class OuverturePortList(RevMixin, AclMixin, models.Model): - """Liste des ports ouverts sur une interface.""" + """Ports opening list. + + Attributes: + name: the name of the ports configuration. + """ name = models.CharField( help_text=_("Name of the ports configuration"), max_length=255 @@ -1983,11 +2316,15 @@ class OuverturePortList(RevMixin, AclMixin, models.Model): verbose_name_plural = _("ports opening lists") def can_delete(self, user_request, *_args, **_kwargs): - """Verifie que l'user a les bons droits bureau pour delete - cette instance ouvertureportlist - :param self: Instance ouvertureportlist à delete - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if the user can delete the current ports opening list (self). + + Args: + user_request: the user requesting to delete self. + + Returns: + A tuple indicating whether the user can delete self and a + message if not. + """ if not user_request.has_perm("machines.delete_ouvertureportlist"): return ( False, @@ -2002,38 +2339,48 @@ class OuverturePortList(RevMixin, AclMixin, models.Model): return self.name def tcp_ports_in(self): - """Renvoie la liste des ports ouverts en TCP IN pour ce profil""" + """Get the list of ports opened in TCP IN of the current ports opening + list.""" return self.ouvertureport_set.filter( protocole=OuverturePort.TCP, io=OuverturePort.IN ) def udp_ports_in(self): - """Renvoie la liste des ports ouverts en UDP IN pour ce profil""" + """Get the list of ports opened in UDP IN of the current ports opening + list.""" return self.ouvertureport_set.filter( protocole=OuverturePort.UDP, io=OuverturePort.IN ) def tcp_ports_out(self): - """Renvoie la liste des ports ouverts en TCP OUT pour ce profil""" + """Get the list of ports opened in TCP OUT of the current ports opening + list.""" return self.ouvertureport_set.filter( protocole=OuverturePort.TCP, io=OuverturePort.OUT ) def udp_ports_out(self): - """Renvoie la liste des ports ouverts en UDP OUT pour ce profil""" + """Get the list of ports opened in UDP OUT of the current ports opening + list.""" return self.ouvertureport_set.filter( protocole=OuverturePort.UDP, io=OuverturePort.OUT ) class OuverturePort(RevMixin, AclMixin, models.Model): - """ - Représente un simple port ou une plage de ports. + """Ports opening. - Les ports de la plage sont compris entre begin et en inclus. - Si begin == end alors on ne représente qu'un seul port. + The ports of the range are between begin and end (included). + If begin == end, then it represents a single port. + The ports are limited to be between 0 and 65535, as defined in the RFC. - On limite les ports entre 0 et 65535, tels que défini par la RFC + Attributes: + begin: the number of the first port of the ports opening. + end: the number of the last port of the ports opening. + port_list: the ports opening list (configuration for opened ports) of + the ports opening. + protocole: the protocol of the ports opening. + io: the direction of communication, IN or OUT. """ TCP = "T" @@ -2058,14 +2405,13 @@ class OuverturePort(RevMixin, AclMixin, models.Model): return ":".join([str(self.begin), str(self.end)]) def show_port(self): - """Formatage plus joli, alias pour str""" + """Format the ports opening by calling str.""" return str(self) @receiver(post_save, sender=Machine) def machine_post_save(**kwargs): - """Synchronisation ldap et régen parefeu/dhcp lors de la modification - d'une machine""" + """Synchronise LDAP and regen firewall/DHCP after a machine is edited.""" user = kwargs["instance"].user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) regen("dhcp") @@ -2074,8 +2420,7 @@ def machine_post_save(**kwargs): @receiver(post_delete, sender=Machine) def machine_post_delete(**kwargs): - """Synchronisation ldap et régen parefeu/dhcp lors de la suppression - d'une machine""" + """Synchronise LDAP and regen firewall/DHCP after a machine is deleted.""" machine = kwargs["instance"] user = machine.user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) @@ -2085,8 +2430,8 @@ def machine_post_delete(**kwargs): @receiver(post_save, sender=Interface) def interface_post_save(**kwargs): - """Synchronisation ldap et régen parefeu/dhcp lors de la modification - d'une interface""" + """Synchronise LDAP and regen firewall/DHCP after an interface is edited. + """ interface = kwargs["instance"] interface.sync_ipv6() user = interface.machine.user @@ -2098,8 +2443,8 @@ def interface_post_save(**kwargs): @receiver(post_delete, sender=Interface) def interface_post_delete(**kwargs): - """Synchronisation ldap et régen parefeu/dhcp lors de la suppression - d'une interface""" + """Synchronise LDAP and regen firewall/DHCP after an interface is deleted. + """ interface = kwargs["instance"] user = interface.machine.user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) @@ -2107,7 +2452,7 @@ def interface_post_delete(**kwargs): @receiver(post_save, sender=IpType) def iptype_post_save(**kwargs): - """Generation des objets ip après modification d'un range ip""" + """Generate the IP objects after an IP type is edited.""" iptype = kwargs["instance"] iptype.gen_ip_range() iptype.check_replace_prefixv6() @@ -2115,8 +2460,9 @@ def iptype_post_save(**kwargs): @receiver(post_save, sender=MachineType) def machinetype_post_save(**kwargs): - """Mise à jour des interfaces lorsque changement d'attribution - d'une machinetype (changement iptype parent)""" + """Update the interfaces after the machine type is changed (change the + parent IP type). + """ machinetype = kwargs["instance"] for interface in machinetype.all_interfaces(): interface.update_type() @@ -2124,95 +2470,95 @@ def machinetype_post_save(**kwargs): @receiver(post_save, sender=Domain) def domain_post_save(**_kwargs): - """Regeneration dns après modification d'un domain object""" + """Regenerate the DNS after a domain is edited.""" regen("dns") @receiver(post_delete, sender=Domain) def domain_post_delete(**_kwargs): - """Regeneration dns après suppression d'un domain object""" + """Regenerate the DNS after a domain is deleted.""" regen("dns") @receiver(post_save, sender=Extension) def extension_post_save(**_kwargs): - """Regeneration dns après modification d'une extension""" + """Regenerate the DNS after an extension is edited.""" regen("dns") @receiver(post_delete, sender=Extension) def extension_post_delete(**_kwargs): - """Regeneration dns après suppression d'une extension""" + """Regenerate the DNS after an extension is deleted.""" regen("dns") @receiver(post_save, sender=SOA) def soa_post_save(**_kwargs): - """Regeneration dns après modification d'un SOA""" + """Regenerate the DNS after a SOA record is edited.""" regen("dns") @receiver(post_delete, sender=SOA) def soa_post_delete(**_kwargs): - """Regeneration dns après suppresson d'un SOA""" + """Regenerate the DNS after a SOA record is deleted.""" regen("dns") @receiver(post_save, sender=Mx) def mx_post_save(**_kwargs): - """Regeneration dns après modification d'un MX""" + """Regenerate the DNS after an MX record is edited.""" regen("dns") @receiver(post_delete, sender=Mx) def mx_post_delete(**_kwargs): - """Regeneration dns après suppresson d'un MX""" + """Regenerate the DNS after an MX record is deleted.""" regen("dns") @receiver(post_save, sender=Ns) def ns_post_save(**_kwargs): - """Regeneration dns après modification d'un NS""" + """Regenerate the DNS after an NS record is edited.""" regen("dns") @receiver(post_delete, sender=Ns) def ns_post_delete(**_kwargs): - """Regeneration dns après modification d'un NS""" + """Regenerate the DNS after an NS record is deleted.""" regen("dns") @receiver(post_save, sender=Txt) def text_post_save(**_kwargs): - """Regeneration dns après modification d'un TXT""" + """Regenerate the DNS after a TXT record is edited.""" regen("dns") @receiver(post_delete, sender=Txt) def text_post_delete(**_kwargs): - """Regeneration dns après modification d'un TX""" + """Regenerate the DNS after a TXT record is deleted.""" regen("dns") @receiver(post_save, sender=DName) def dname_post_save(**_kwargs): - """Updates the DNS regen after modification of a DName object.""" + """Regenerate the DNS after a DNAME record is edited.""" regen("dns") @receiver(post_delete, sender=DName) def dname_post_delete(**_kwargs): - """Updates the DNS regen after deletion of a DName object.""" + """Regenerate the DNS after a DNAME record is deleted.""" regen("dns") @receiver(post_save, sender=Srv) def srv_post_save(**_kwargs): - """Regeneration dns après modification d'un SRV""" + """Regenerate the DNS after an SRV record is edited.""" regen("dns") @receiver(post_delete, sender=Srv) def srv_post_delete(**_kwargs): - """Regeneration dns après modification d'un SRV""" + """Regenerate the DNS after an SRV record is deleted.""" regen("dns") diff --git a/machines/views.py b/machines/views.py index cc82913b..b4059024 100644 --- a/machines/views.py +++ b/machines/views.py @@ -211,10 +211,11 @@ def generate_ipv4_mbf_param(form_obj, is_type_tt): @can_create(Machine) @can_edit(User) def new_machine(request, user, **_kwargs): - """ Fonction de creation d'une machine. Cree l'objet machine, - le sous objet interface et l'objet domain à partir de model forms. - Trop complexe, devrait être simplifié""" + """View used to create machines. + Creates the object, the underlying interface and domain objects from model + forms. Too complex, should be simplified. + """ machine = NewMachineForm(request.POST or None, user=request.user) interface = AddInterfaceForm(request.POST or None, user=request.user) domain = DomainForm(request.POST or None, user=user, initial={'name': user.get_next_domain_name()}) @@ -249,10 +250,10 @@ def new_machine(request, user, **_kwargs): @login_required @can_edit(Interface) def edit_interface(request, interface_instance, **_kwargs): - """ Edition d'une interface. Distingue suivant les droits les valeurs - de interfaces et machines que l'user peut modifier infra permet de - modifier le propriétaire""" + """View used to edit interfaces. + The values a user can change depends on their rights. + """ machine_form = EditMachineForm( request.POST or None, instance=interface_instance.machine, user=request.user ) @@ -295,7 +296,7 @@ def edit_interface(request, interface_instance, **_kwargs): @login_required @can_delete(Machine) def del_machine(request, machine, **_kwargs): - """ Supprime une machine, interfaces en mode cascade""" + """View used to delete machines, and the interfaces in cascade.""" if request.method == "POST": machine.delete() messages.success(request, _("The machine was deleted.")) @@ -311,8 +312,9 @@ def del_machine(request, machine, **_kwargs): @can_create(Interface) @can_edit(Machine) def new_interface(request, machine, **_kwargs): - """ Ajoute une interface et son domain associé à une machine existante""" - + """View used to create interfaces and the associated domains related to a + machine. + """ interface_form = AddInterfaceForm(request.POST or None, user=request.user) domain_form = DomainForm(request.POST or None, user=request.user, initial={'name': machine.user.get_next_domain_name()}) if interface_form.is_valid(): @@ -344,7 +346,7 @@ def new_interface(request, machine, **_kwargs): @login_required @can_delete(Interface) def del_interface(request, interface, **_kwargs): - """ Supprime une interface. Domain objet en mode cascade""" + """View used to delete interfaces, and the domains in cascade.""" if request.method == "POST": machine = interface.machine interface.delete() @@ -363,7 +365,7 @@ def del_interface(request, interface, **_kwargs): @can_create(Ipv6List) @can_edit(Interface) def new_ipv6list(request, interface, **_kwargs): - """Nouvelle ipv6""" + """View used to create IPv6 addresses lists.""" ipv6list_instance = Ipv6List(interface=interface) ipv6 = Ipv6ListForm( request.POST or None, instance=ipv6list_instance, user=request.user @@ -384,7 +386,7 @@ def new_ipv6list(request, interface, **_kwargs): @login_required @can_edit(Ipv6List) def edit_ipv6list(request, ipv6list_instance, **_kwargs): - """Edition d'une ipv6""" + """View used to edit IPv6 addresses lists.""" ipv6 = Ipv6ListForm( request.POST or None, instance=ipv6list_instance, user=request.user ) @@ -406,7 +408,7 @@ def edit_ipv6list(request, ipv6list_instance, **_kwargs): @login_required @can_delete(Ipv6List) def del_ipv6list(request, ipv6list, **_kwargs): - """ Supprime une ipv6""" + """View used to delete IPv6 addresses lists.""" if request.method == "POST": interfaceid = ipv6list.interface.id ipv6list.delete() @@ -423,7 +425,7 @@ def del_ipv6list(request, ipv6list, **_kwargs): @can_create(SshFp) @can_edit(Machine) def new_sshfp(request, machine, **_kwargs): - """Creates an SSHFP record associated with a machine""" + """View used to create SSHFP records associated with machines.""" sshfp_instance = SshFp(machine=machine) sshfp = SshFpForm(request.POST or None, instance=sshfp_instance) if sshfp.is_valid(): @@ -442,7 +444,7 @@ def new_sshfp(request, machine, **_kwargs): @login_required @can_edit(SshFp) def edit_sshfp(request, sshfp_instance, **_kwargs): - """Edits an SSHFP record""" + """View used to edit SSHFP records.""" sshfp = SshFpForm(request.POST or None, instance=sshfp_instance) if sshfp.is_valid(): if sshfp.changed_data: @@ -462,7 +464,7 @@ def edit_sshfp(request, sshfp_instance, **_kwargs): @login_required @can_delete(SshFp) def del_sshfp(request, sshfp, **_kwargs): - """Deletes an SSHFP record""" + """View used to delete SSHFP records.""" if request.method == "POST": machineid = sshfp.machine.id sshfp.delete() @@ -478,9 +480,10 @@ def del_sshfp(request, sshfp, **_kwargs): @login_required @can_create(IpType) def add_iptype(request): - """ Ajoute un range d'ip. Intelligence dans le models, fonction views - minimaliste""" + """View used to create IP ranges. + The view function is simple, the intelligence is in the model. + """ iptype = IpTypeForm(request.POST or None) if iptype.is_valid(): iptype.save() @@ -496,9 +499,10 @@ def add_iptype(request): @login_required @can_edit(IpType) def edit_iptype(request, iptype_instance, **_kwargs): - """ Edition d'un range. Ne permet pas de le redimensionner pour éviter - l'incohérence""" + """View used to edit IP ranges. + Changing the size of the range is not possible to prevent inconsistency. + """ iptype = EditIpTypeForm(request.POST or None, instance=iptype_instance) if iptype.is_valid(): if iptype.changed_data: @@ -515,7 +519,10 @@ def edit_iptype(request, iptype_instance, **_kwargs): @login_required @can_delete_set(IpType) def del_iptype(request, instances): - """ Suppression d'un range ip. Supprime les objets ip associés""" + """View used to delete IP ranges. + + Deletes the related IP objects. + """ iptype = DelIpTypeForm(request.POST or None, instances=instances) if iptype.is_valid(): iptype_dels = iptype.cleaned_data["iptypes"] @@ -545,7 +552,7 @@ def del_iptype(request, instances): @login_required @can_create(MachineType) def add_machinetype(request): - """ View used to add a Machinetype object """ + """View used to create machine types.""" machinetype = MachineTypeForm(request.POST or None) if machinetype.is_valid(): machinetype.save() @@ -561,7 +568,7 @@ def add_machinetype(request): @login_required @can_edit(MachineType) def edit_machinetype(request, machinetype_instance, **_kwargs): - """ View used to edit a MachineType object """ + """View used to edit machine types.""" machinetype = MachineTypeForm(request.POST or None, instance=machinetype_instance) if machinetype.is_valid(): if machinetype.changed_data: @@ -578,7 +585,7 @@ def edit_machinetype(request, machinetype_instance, **_kwargs): @login_required @can_delete_set(MachineType) def del_machinetype(request, instances): - """ View used to delete a MachineType object """ + """View used to delete machines types.""" machinetype = DelMachineTypeForm(request.POST or None, instances=instances) if machinetype.is_valid(): machinetype_dels = machinetype.cleaned_data["machinetypes"] @@ -608,7 +615,7 @@ def del_machinetype(request, instances): @login_required @can_create(Extension) def add_extension(request): - """ View used to add an Extension object """ + """View used to create extensions.""" extension = ExtensionForm(request.POST or None) if extension.is_valid(): extension.save() @@ -624,7 +631,7 @@ def add_extension(request): @login_required @can_edit(Extension) def edit_extension(request, extension_instance, **_kwargs): - """ View used to edit an Extension object """ + """View used to edit extensions.""" extension = ExtensionForm(request.POST or None, instance=extension_instance) if extension.is_valid(): if extension.changed_data: @@ -641,7 +648,7 @@ def edit_extension(request, extension_instance, **_kwargs): @login_required @can_delete_set(Extension) def del_extension(request, instances): - """ View used to delete an Extension object """ + """View used to delete extensions.""" extension = DelExtensionForm(request.POST or None, instances=instances) if extension.is_valid(): extension_dels = extension.cleaned_data["extensions"] @@ -670,7 +677,7 @@ def del_extension(request, instances): @login_required @can_create(SOA) def add_soa(request): - """ View used to add a SOA object """ + """View used to create SOA records.""" soa = SOAForm(request.POST or None) if soa.is_valid(): soa.save() @@ -686,7 +693,7 @@ def add_soa(request): @login_required @can_edit(SOA) def edit_soa(request, soa_instance, **_kwargs): - """ View used to edit a SOA object """ + """View used to edit SOA records.""" soa = SOAForm(request.POST or None, instance=soa_instance) if soa.is_valid(): if soa.changed_data: @@ -701,7 +708,7 @@ def edit_soa(request, soa_instance, **_kwargs): @login_required @can_delete_set(SOA) def del_soa(request, instances): - """ View used to delete a SOA object """ + """View used to delete SOA records.""" soa = DelSOAForm(request.POST or None, instances=instances) if soa.is_valid(): soa_dels = soa.cleaned_data["soa"] @@ -722,7 +729,7 @@ def del_soa(request, instances): @login_required @can_create(Mx) def add_mx(request): - """ View used to add a MX object """ + """View used to create MX records.""" mx = MxForm(request.POST or None) if mx.is_valid(): mx.save() @@ -738,7 +745,7 @@ def add_mx(request): @login_required @can_edit(Mx) def edit_mx(request, mx_instance, **_kwargs): - """ View used to edit a MX object """ + """View used to edit MX records.""" mx = MxForm(request.POST or None, instance=mx_instance) if mx.is_valid(): if mx.changed_data: @@ -753,7 +760,7 @@ def edit_mx(request, mx_instance, **_kwargs): @login_required @can_delete_set(Mx) def del_mx(request, instances): - """ View used to delete a MX object """ + """View used to delete MX records.""" mx = DelMxForm(request.POST or None, instances=instances) if mx.is_valid(): mx_dels = mx.cleaned_data["mx"] @@ -774,7 +781,7 @@ def del_mx(request, instances): @login_required @can_create(Ns) def add_ns(request): - """ View used to add a NS object """ + """View used to create NS records.""" ns = NsForm(request.POST or None) if ns.is_valid(): ns.save() @@ -790,7 +797,7 @@ def add_ns(request): @login_required @can_edit(Ns) def edit_ns(request, ns_instance, **_kwargs): - """ View used to edit a NS object """ + """View used to edit NS records.""" ns = NsForm(request.POST or None, instance=ns_instance) if ns.is_valid(): if ns.changed_data: @@ -805,7 +812,7 @@ def edit_ns(request, ns_instance, **_kwargs): @login_required @can_delete_set(Ns) def del_ns(request, instances): - """ View used to delete a NS object """ + """View used to delete NS records.""" nss = DelNsForm(request.POST or None, instances=instances) if nss.is_valid(): ns_dels = nss.cleaned_data["nss"] @@ -826,7 +833,7 @@ def del_ns(request, instances): @login_required @can_create(DName) def add_dname(request): - """ View used to add a DName object """ + """View used to create DNAME records.""" dname = DNameForm(request.POST or None) if dname.is_valid(): dname.save() @@ -842,7 +849,7 @@ def add_dname(request): @login_required @can_edit(DName) def edit_dname(request, dname_instance, **_kwargs): - """ View used to edit a DName object """ + """View used to edit DNAME records.""" dname = DNameForm(request.POST or None, instance=dname_instance) if dname.is_valid(): if dname.changed_data: @@ -857,7 +864,7 @@ def edit_dname(request, dname_instance, **_kwargs): @login_required @can_delete_set(DName) def del_dname(request, instances): - """ View used to delete a DName object """ + """View used to delete DNAME records.""" dname = DelDNameForm(request.POST or None, instances=instances) if dname.is_valid(): dname_dels = dname.cleaned_data["dname"] @@ -881,7 +888,7 @@ def del_dname(request, instances): @login_required @can_create(Txt) def add_txt(request): - """ View used to add a TXT object """ + """View used to create TXT records.""" txt = TxtForm(request.POST or None) if txt.is_valid(): txt.save() @@ -897,7 +904,7 @@ def add_txt(request): @login_required @can_edit(Txt) def edit_txt(request, txt_instance, **_kwargs): - """ View used to edit a TXT object """ + """View used to edit TXT records.""" txt = TxtForm(request.POST or None, instance=txt_instance) if txt.is_valid(): if txt.changed_data: @@ -912,7 +919,7 @@ def edit_txt(request, txt_instance, **_kwargs): @login_required @can_delete_set(Txt) def del_txt(request, instances): - """ View used to delete a TXT object """ + """View used to delete TXT records.""" txt = DelTxtForm(request.POST or None, instances=instances) if txt.is_valid(): txt_dels = txt.cleaned_data["txt"] @@ -933,7 +940,7 @@ def del_txt(request, instances): @login_required @can_create(Srv) def add_srv(request): - """ View used to add a SRV object """ + """View used to create SRV records.""" srv = SrvForm(request.POST or None) if srv.is_valid(): srv.save() @@ -949,7 +956,7 @@ def add_srv(request): @login_required @can_edit(Srv) def edit_srv(request, srv_instance, **_kwargs): - """ View used to edit a SRV object """ + """View used to edit SRV records.""" srv = SrvForm(request.POST or None, instance=srv_instance) if srv.is_valid(): if srv.changed_data: @@ -964,7 +971,7 @@ def edit_srv(request, srv_instance, **_kwargs): @login_required @can_delete_set(Srv) def del_srv(request, instances): - """ View used to delete a SRV object """ + """View used to delete SRV records.""" srv = DelSrvForm(request.POST or None, instances=instances) if srv.is_valid(): srv_dels = srv.cleaned_data["srv"] @@ -986,7 +993,7 @@ def del_srv(request, instances): @can_create(Domain) @can_edit(Interface) def add_alias(request, interface, interfaceid): - """ View used to add an Alias object """ + """View used to create aliases.""" alias = AliasForm(request.POST or None, user=request.user) if alias.is_valid(): alias = alias.save(commit=False) @@ -1006,7 +1013,7 @@ def add_alias(request, interface, interfaceid): @login_required @can_edit(Domain) def edit_alias(request, domain_instance, **_kwargs): - """ View used to edit an Alias object """ + """View used to edit aliases records.""" alias = AliasForm(request.POST or None, instance=domain_instance, user=request.user) if alias.is_valid(): if alias.changed_data: @@ -1026,7 +1033,7 @@ def edit_alias(request, domain_instance, **_kwargs): @login_required @can_edit(Interface) def del_alias(request, interface, interfaceid): - """ View used to delete an Alias object """ + """View used to delete aliases records.""" alias = DelAliasForm(request.POST or None, interface=interface) if alias.is_valid(): alias_dels = alias.cleaned_data["alias"] @@ -1051,7 +1058,7 @@ def del_alias(request, interface, interfaceid): @login_required @can_create(Role) def add_role(request): - """ View used to add a Role object """ + """View used to create roles.""" role = RoleForm(request.POST or None) if role.is_valid(): role.save() @@ -1067,7 +1074,7 @@ def add_role(request): @login_required @can_edit(Role) def edit_role(request, role_instance, **_kwargs): - """ View used to edit a Role object """ + """View used to edit roles.""" role = RoleForm(request.POST or None, instance=role_instance) if role.is_valid(): if role.changed_data: @@ -1082,7 +1089,7 @@ def edit_role(request, role_instance, **_kwargs): @login_required @can_delete_set(Role) def del_role(request, instances): - """ View used to delete a Service object """ + """View used to delete roles.""" role = DelRoleForm(request.POST or None, instances=instances) if role.is_valid(): role_dels = role.cleaned_data["role"] @@ -1103,7 +1110,7 @@ def del_role(request, instances): @login_required @can_create(Service) def add_service(request): - """ View used to add a Service object """ + """View used to create services.""" service = ServiceForm(request.POST or None) if service.is_valid(): service.save() @@ -1119,7 +1126,7 @@ def add_service(request): @login_required @can_edit(Service) def edit_service(request, service_instance, **_kwargs): - """ View used to edit a Service object """ + """View used to edit services.""" service = ServiceForm(request.POST or None, instance=service_instance) if service.is_valid(): if service.changed_data: @@ -1136,7 +1143,7 @@ def edit_service(request, service_instance, **_kwargs): @login_required @can_delete_set(Service) def del_service(request, instances): - """ View used to delete a Service object """ + """View used to delete services.""" service = DelServiceForm(request.POST or None, instances=instances) if service.is_valid(): service_dels = service.cleaned_data["service"] @@ -1169,7 +1176,7 @@ def regen_service(request, service, **_kwargs): @login_required @can_create(Vlan) def add_vlan(request): - """ View used to add a VLAN object """ + """View used to create VLANs.""" vlan = VlanForm(request.POST or None) if vlan.is_valid(): vlan.save() @@ -1185,7 +1192,7 @@ def add_vlan(request): @login_required @can_edit(Vlan) def edit_vlan(request, vlan_instance, **_kwargs): - """ View used to edit a VLAN object """ + """View used to edit VLANs.""" vlan = VlanForm(request.POST or None, instance=vlan_instance) if vlan.is_valid(): if vlan.changed_data: @@ -1200,7 +1207,7 @@ def edit_vlan(request, vlan_instance, **_kwargs): @login_required @can_delete_set(Vlan) def del_vlan(request, instances): - """ View used to delete a VLAN object """ + """View used to delete VLANs.""" vlan = DelVlanForm(request.POST or None, instances=instances) if vlan.is_valid(): vlan_dels = vlan.cleaned_data["vlan"] @@ -1221,7 +1228,7 @@ def del_vlan(request, instances): @login_required @can_create(Nas) def add_nas(request): - """ View used to add a NAS object """ + """View used to create NAS devices.""" nas = NasForm(request.POST or None) if nas.is_valid(): nas.save() @@ -1237,7 +1244,7 @@ def add_nas(request): @login_required @can_edit(Nas) def edit_nas(request, nas_instance, **_kwargs): - """ View used to edit a NAS object """ + """View used to edit NAS devices.""" nas = NasForm(request.POST or None, instance=nas_instance) if nas.is_valid(): if nas.changed_data: @@ -1252,7 +1259,7 @@ def edit_nas(request, nas_instance, **_kwargs): @login_required @can_delete_set(Nas) def del_nas(request, instances): - """ View used to delete a NAS object """ + """View used to delete NAS devices.""" nas = DelNasForm(request.POST or None, instances=instances) if nas.is_valid(): nas_dels = nas.cleaned_data["nas"] @@ -1273,8 +1280,8 @@ def del_nas(request, instances): @login_required @can_view_all(Machine) def index(request): - """ The home view for this app. Displays the list of registered - machines in Re2o """ + """The home view for this app. Displays the list of registered + machines in Re2o.""" pagination_large_number = GeneralOption.get_cached_value("pagination_large_number") machines_list = ( Machine.objects.select_related("user") @@ -1297,7 +1304,7 @@ def index(request): @login_required @can_view_all(IpType) def index_iptype(request): - """ View displaying the list of existing types of IP """ + """View used to display the list of existing types of IP.""" iptype_list = ( IpType.objects.select_related("extension") .select_related("vlan") @@ -1309,7 +1316,7 @@ def index_iptype(request): @login_required @can_view_all(Vlan) def index_vlan(request): - """ View displaying the list of existing VLANs """ + """View used to display the list of existing VLANs.""" vlan_list = Vlan.objects.prefetch_related("iptype_set").order_by("vlan_id") return render(request, "machines/index_vlan.html", {"vlan_list": vlan_list}) @@ -1317,7 +1324,7 @@ def index_vlan(request): @login_required @can_view_all(MachineType) def index_machinetype(request): - """ View displaying the list of existing types of machines """ + """View used to display the list of existing types of machines.""" machinetype_list = MachineType.objects.select_related("ip_type").order_by("name") return render( request, @@ -1329,7 +1336,7 @@ def index_machinetype(request): @login_required @can_view_all(Nas) def index_nas(request): - """ View displaying the list of existing NAS """ + """View used to display the list of existing NAS devices.""" nas_list = ( Nas.objects.select_related("machine_type") .select_related("nas_type") @@ -1341,10 +1348,11 @@ def index_nas(request): @login_required @can_view_all(SOA, Mx, Ns, Txt, DName, Srv, Extension) def index_extension(request): - """ View displaying the list of existing extensions, the list of + """View used to display the list of existing extensions, the list of existing SOA records, the list of existing MX records , the list of existing NS records, the list of existing TXT records and the list of - existing SRV records """ + existing SRV records. + """ extension_list = ( Extension.objects.select_related("origin") .select_related("soa") @@ -1386,7 +1394,7 @@ def index_extension(request): @login_required @can_edit(Interface) def index_alias(request, interface, interfaceid): - """ View used to display the list of existing alias of an interface """ + """View used to display the list of existing aliases of an interface.""" alias_list = Domain.objects.filter( cname=Domain.objects.filter(interface_parent=interface) ).order_by("name") @@ -1401,7 +1409,7 @@ def index_alias(request, interface, interfaceid): @can_view(Machine) def index_sshfp(request, machine, machineid): """View used to display the list of existing SSHFP records associated - with a machine""" + with a machine.""" sshfp_list = SshFp.objects.filter(machine=machine) return render( request, @@ -1413,7 +1421,7 @@ def index_sshfp(request, machine, machineid): @login_required @can_view(Interface) def index_ipv6(request, interface, interfaceid): - """ View used to display the list of existing IPv6 of an interface """ + """View used to display the list of existing IPv6 of an interface.""" ipv6_list = Ipv6List.objects.filter(interface=interface) return render( request, @@ -1425,7 +1433,7 @@ def index_ipv6(request, interface, interfaceid): @login_required @can_view_all(Role) def index_role(request): - """ View used to display the list of existing roles """ + """View used to display the list of existing roles.""" role_list = Role.objects.prefetch_related("servers__domain__extension").all() return render(request, "machines/index_role.html", {"role_list": role_list}) @@ -1433,7 +1441,7 @@ def index_role(request): @login_required @can_view_all(Service) def index_service(request): - """ View used to display the list of existing services """ + """View used to display the list of existing services.""" service_list = Service.objects.prefetch_related( "service_link_set__server__domain__extension" ).all() @@ -1452,7 +1460,7 @@ def index_service(request): @login_required @can_view_all(OuverturePortList) def index_portlist(request): - """ View used to display the list of existing port policies """ + """View used to display the list of existing port policies.""" port_list = ( OuverturePortList.objects.prefetch_related("ouvertureport_set") .prefetch_related("interface_set__domain__extension") @@ -1465,7 +1473,7 @@ def index_portlist(request): @login_required @can_edit(OuverturePortList) def edit_portlist(request, ouvertureportlist_instance, **_kwargs): - """ View used to edit a port policy """ + """View used to edit port policies.""" port_list = EditOuverturePortListForm( request.POST or None, instance=ouvertureportlist_instance ) @@ -1500,7 +1508,7 @@ def edit_portlist(request, ouvertureportlist_instance, **_kwargs): @login_required @can_delete(OuverturePortList) def del_portlist(request, port_list_instance, **_kwargs): - """ View used to delete a port policy """ + """View used to delete port policies.""" port_list_instance.delete() messages.success(request, _("The ports list was deleted.")) return redirect(reverse("machines:index-portlist")) @@ -1509,7 +1517,7 @@ def del_portlist(request, port_list_instance, **_kwargs): @login_required @can_create(OuverturePortList) def add_portlist(request): - """ View used to add a port policy """ + """View used to create port policies.""" port_list = EditOuverturePortListForm(request.POST or None) port_formset = modelformset_factory( OuverturePort, @@ -1540,8 +1548,9 @@ def add_portlist(request): @can_create(OuverturePort) @can_edit(Interface) def configure_ports(request, interface_instance, **_kwargs): - """ View to display the list of configured port policy for an - interface """ + """View to display the list of configured port policies for an + interface. + """ if not interface_instance.may_have_port_open(): messages.error( request, diff --git a/multi_op/forms.py b/multi_op/forms.py index 297c03c8..08bc9123 100644 --- a/multi_op/forms.py +++ b/multi_op/forms.py @@ -36,7 +36,7 @@ from topologie.models import Dormitory class DormitoryForm(FormRevMixin, Form): - """Select a dorm""" + """Form used to select dormitories.""" dormitory = forms.ModelMultipleChoiceField( queryset=Dormitory.objects.all(), diff --git a/multi_op/preferences/forms.py b/multi_op/preferences/forms.py index b6958546..a58410c7 100644 --- a/multi_op/preferences/forms.py +++ b/multi_op/preferences/forms.py @@ -34,7 +34,7 @@ from .models import Preferences class EditPreferencesForm(ModelForm): - """ Edit the ticket's settings""" + """Form used to edit the settings of multi_op.""" class Meta: model = Preferences diff --git a/multi_op/preferences/models.py b/multi_op/preferences/models.py index c342bf41..8a185188 100644 --- a/multi_op/preferences/models.py +++ b/multi_op/preferences/models.py @@ -19,7 +19,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Fichier définissant les administration des models de preference +multi_op preferences model. The settings are used when managing dormitories +with multiple operators. """ @@ -28,7 +29,7 @@ from django.utils.translation import ugettext_lazy as _ class Preferences(models.Model): - """ Definition of the app settings""" + """Definition of the settings of multi_op.""" enabled_dorm = models.ManyToManyField( "topologie.Dormitory", diff --git a/multi_op/views.py b/multi_op/views.py index 266c8a15..729eae27 100644 --- a/multi_op/views.py +++ b/multi_op/views.py @@ -53,7 +53,13 @@ from .preferences.forms import EditPreferencesForm def display_rooms_connection(request, dormitory=None): - """View to display global state of connection state""" + """View used to display an overview of the rooms' connection state. + + Args: + request: django request. + dormitory: Dormitory, the dormitory used to filter rooms. If no + dormitory is given, all rooms are displayed (default: None). + """ room_list = Room.objects.select_related("building__dormitory").order_by( "building_dormitory", "port" ) @@ -81,19 +87,30 @@ def display_rooms_connection(request, dormitory=None): @login_required @can_view_all(Room) def aff_state_global(request): + """View used to display the connection state of all rooms.""" return display_rooms_connection(request) @login_required @can_view(Dormitory) def aff_state_dormitory(request, dormitory, dormitoryid): + """View used to display the connection state of the rooms in the given + dormitory. + + Args: + request: django request. + dormitory: Dormitory, the dormitory used to filter rooms. + dormitoryid: int, the id of the dormitory. + """ return display_rooms_connection(dormitory=dormitory) @login_required @can_view_all(Room) def aff_pending_connection(request): - """Aff pending Rooms to connect on our network""" + """View used to display rooms pending connection to the organisation's + network. + """ room_list = ( Room.objects.select_related("building__dormitory") .filter(port__isnull=True) @@ -128,7 +145,9 @@ def aff_pending_connection(request): @login_required @can_view_all(Room) def aff_pending_disconnection(request): - """Aff pending Rooms to disconnect from our network""" + """View used to display rooms pending disconnection from the organisation's + network. + """ room_list = ( Room.objects.select_related("building__dormitory") .filter(port__isnull=False) @@ -163,7 +182,13 @@ def aff_pending_disconnection(request): @login_required @can_edit(Room) def disconnect_room(request, room, roomid): - """Action of disconnecting a room""" + """View used to disconnect a room. + + Args: + request: django request. + room: Room, the room to be disconnected. + roomid: int, the id of the room. + """ room.port_set.clear() room.save() messages.success(request, _("The room %s was disconnected.") % room) @@ -171,5 +196,7 @@ def disconnect_room(request, room, roomid): def navbar_user(): - """View to display the app in user's dropdown in the navbar""" + """View used to display a link to manage operators in the navbar (in the + dropdown menu Topology). + """ return ("topologie", render_to_string("multi_op/navbar.html")) diff --git a/preferences/admin.py b/preferences/admin.py index f4475c14..d3e4a22d 100644 --- a/preferences/admin.py +++ b/preferences/admin.py @@ -21,7 +21,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Classes admin pour les models de preferences +Admin classes for models of preferences app. """ from __future__ import unicode_literals @@ -46,79 +46,79 @@ from .models import ( class OptionalUserAdmin(VersionAdmin): - """Class admin options user""" + """Admin class for user options.""" pass class OptionalTopologieAdmin(VersionAdmin): - """Class admin options topologie""" + """Admin class for topology options.""" pass class OptionalMachineAdmin(VersionAdmin): - """Class admin options machines""" + """Admin class for machines options.""" pass class GeneralOptionAdmin(VersionAdmin): - """Class admin options générales""" + """Admin class for general options.""" pass class ServiceAdmin(VersionAdmin): - """Class admin gestion des services de la page d'accueil""" + """Admin class for services (on the homepage).""" pass class MailContactAdmin(VersionAdmin): - """Admin class for contact email adresses""" + """Admin class for contact email addresses.""" pass class AssoOptionAdmin(VersionAdmin): - """Class admin options de l'asso""" + """Admin class for organisation options.""" pass class MailMessageOptionAdmin(VersionAdmin): - """Class admin options mail""" + """Admin class for email messages options.""" pass class HomeOptionAdmin(VersionAdmin): - """Class admin options home""" + """Admin class for home options.""" pass class RadiusKeyAdmin(VersionAdmin): - """Class radiuskey""" + """Admin class for RADIUS keys options.""" pass class SwitchManagementCredAdmin(VersionAdmin): - """Class managementcred for switch""" + """Admin class for switch management credentials options.""" pass class ReminderAdmin(VersionAdmin): - """Class reminder for switch""" + """Admin class for reminder options.""" pass class DocumentTemplateAdmin(VersionAdmin): - """Admin class for DocumentTemplate""" + """Admin class for document templates.""" pass diff --git a/preferences/forms.py b/preferences/forms.py index 75491b05..2a5d5ac9 100644 --- a/preferences/forms.py +++ b/preferences/forms.py @@ -20,7 +20,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Formulaire d'edition des réglages : user, machine, topologie, asso... +Forms to edit preferences: users, machines, topology, organisation etc. """ from __future__ import unicode_literals @@ -53,7 +53,7 @@ from topologie.models import Switch class EditOptionalUserForm(ModelForm): - """Formulaire d'édition des options de l'user. (solde, telephone..)""" + """Form used to edit user preferences.""" class Meta: model = OptionalUser @@ -82,7 +82,7 @@ class EditOptionalUserForm(ModelForm): class EditOptionalMachineForm(ModelForm): - """Options machines (max de machines, etc)""" + """Form used to edit machine preferences.""" class Meta: model = OptionalMachine @@ -105,9 +105,7 @@ class EditOptionalMachineForm(ModelForm): class EditOptionalTopologieForm(ModelForm): - """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""" + """Form used to edit the configuration of switches.""" automatic_provision_switchs = forms.ModelMultipleChoiceField( Switch.objects.all(), required=False @@ -135,7 +133,7 @@ class EditOptionalTopologieForm(ModelForm): class EditGeneralOptionForm(ModelForm): - """Options générales (affichages de résultats de recherche, etc)""" + """Form used to edit general preferences.""" class Meta: model = GeneralOption @@ -165,7 +163,7 @@ class EditGeneralOptionForm(ModelForm): class EditAssoOptionForm(ModelForm): - """Options de l'asso (addresse, telephone, etc)""" + """Form used to edit information about the organisation.""" class Meta: model = AssoOption @@ -189,7 +187,7 @@ class EditAssoOptionForm(ModelForm): class EditMailMessageOptionForm(ModelForm): - """Formulaire d'edition des messages de bienvenue personnalisés""" + """Form used to edit welcome email messages.""" class Meta: model = MailMessageOption @@ -207,7 +205,9 @@ class EditMailMessageOptionForm(ModelForm): class EditHomeOptionForm(ModelForm): - """Edition forms of Home options""" + """Form used to edit the social networks information displayed on the home + page. + """ class Meta: model = HomeOption @@ -222,7 +222,7 @@ class EditHomeOptionForm(ModelForm): class EditRadiusOptionForm(ModelForm): - """Edition forms for Radius options""" + """Form used to edit RADIUS preferences.""" class Meta: model = RadiusOption @@ -242,7 +242,7 @@ class EditRadiusOptionForm(ModelForm): class EditCotisationsOptionForm(ModelForm): - """Edition forms for Cotisations options""" + """Form used to edit subscription preferences.""" class Meta: model = CotisationsOption @@ -250,7 +250,7 @@ class EditCotisationsOptionForm(ModelForm): class MandateForm(ModelForm): - """Edit Mandates""" + """Form used to add and edit mandates.""" class Meta: model = Mandate @@ -319,7 +319,7 @@ class MandateForm(ModelForm): class ServiceForm(ModelForm): - """Edition, ajout de services sur la page d'accueil""" + """Form used to add and edit services displayed on the home page.""" class Meta: model = Service @@ -335,7 +335,8 @@ class ServiceForm(ModelForm): class DelServiceForm(Form): - """Suppression de services sur la page d'accueil""" + """Form used to delete one or several services displayed on the home page. + """ services = forms.ModelMultipleChoiceField( queryset=Service.objects.none(), @@ -353,7 +354,7 @@ class DelServiceForm(Form): class ReminderForm(FormRevMixin, ModelForm): - """Edition, ajout de services sur la page d'accueil""" + """Form used to add and edit reminders.""" class Meta: model = Reminder @@ -365,7 +366,7 @@ class ReminderForm(FormRevMixin, ModelForm): class RadiusKeyForm(FormRevMixin, ModelForm): - """Edition, ajout de clef radius""" + """Form used to add and edit RADIUS keys.""" members = forms.ModelMultipleChoiceField( queryset=Switch.objects.all(), required=False @@ -389,8 +390,7 @@ class RadiusKeyForm(FormRevMixin, ModelForm): class SwitchManagementCredForm(FormRevMixin, ModelForm): - """Edition, ajout de creds de management pour gestion - et interface rest des switchs""" + """Form used to add and edit switch management credentials.""" members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) @@ -412,7 +412,7 @@ class SwitchManagementCredForm(FormRevMixin, ModelForm): class MailContactForm(ModelForm): - """Edition, ajout d'adresse de contact""" + """Form used to add and edit contact email addresses.""" class Meta: model = MailContact @@ -424,7 +424,7 @@ class MailContactForm(ModelForm): class DelMailContactForm(Form): - """Delete contact email adress""" + """Form used to delete one or several contact email addresses.""" mailcontacts = forms.ModelMultipleChoiceField( queryset=MailContact.objects.none(), @@ -442,9 +442,7 @@ class DelMailContactForm(Form): class DocumentTemplateForm(FormRevMixin, ModelForm): - """ - Form used to create a document template. - """ + """Form used to add and edit document templates.""" class Meta: model = DocumentTemplate @@ -456,10 +454,7 @@ class DocumentTemplateForm(FormRevMixin, ModelForm): 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. - """ + """Form used to delete one or several document templates.""" document_templates = forms.ModelMultipleChoiceField( queryset=DocumentTemplate.objects.none(), @@ -477,7 +472,7 @@ class DelDocumentTemplateForm(FormRevMixin, Form): class RadiusAttributeForm(ModelForm): - """Edit and add RADIUS attributes.""" + """Form used to add and edit RADIUS attributes.""" class Meta: model = RadiusAttribute @@ -489,7 +484,7 @@ class RadiusAttributeForm(ModelForm): class DelRadiusAttributeForm(Form): - """Delete RADIUS attributes""" + """Form used to delete one or several RADIUS attributes.""" attributes = forms.ModelMultipleChoiceField( queryset=RadiusAttribute.objects.none(), diff --git a/preferences/models.py b/preferences/models.py index d94b2ec8..b012b12b 100644 --- a/preferences/models.py +++ b/preferences/models.py @@ -21,7 +21,8 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Reglages généraux, machines, utilisateurs, mail, general pour l'application. +Models defining the preferences for users, machines, emails, general settings +etc. """ from __future__ import unicode_literals import os @@ -44,20 +45,22 @@ from datetime import timedelta class PreferencesModel(models.Model): - """ Base object for the Preferences objects - Defines methods to handle the cache of the settings (they should - not change a lot) """ + """Base object for the Preferences objects. + + Defines methods to handle the cache of the settings (they should not change + a lot). + """ @classmethod def set_in_cache(cls): - """ Save the preferences in a server-side cache """ + """Save the preferences in a server-side cache.""" instance, _created = cls.objects.get_or_create() cache.set(cls().__class__.__name__.lower(), instance, None) return instance @classmethod def get_cached_value(cls, key): - """ Get the preferences from the server-side cache """ + """Get the preferences from the server-side cache.""" instance = cache.get(cls().__class__.__name__.lower()) if instance is None: instance = cls.set_in_cache() @@ -68,8 +71,34 @@ class PreferencesModel(models.Model): class OptionalUser(AclMixin, PreferencesModel): - """Options pour l'user : obligation ou nom du telephone, - activation ou non du solde, autorisation du negatif, fingerprint etc""" + """User preferences: telephone number requirement, user balance activation, + creation of users by everyone etc. + + Attributes: + is_tel_mandatory: whether indicating a telephone number is mandatory. + gpg_fingerprint: whether GPG fingerprints are enabled. + all_can_create_club: whether all users can create a club. + all_can_create_adherent: whether all users can create a member. + shell_default: the default shell for users connecting to machines + managed by the organisation. + self_change_shell: whether users can edit their shell. + self_change_pseudo: whether users can edit their pseudo (username). + self_room_policy: whether users can edit the policy of their room. + local_email_accounts_enabled: whether local email accounts are enabled. + local_email_domain: the domain used for local email accounts. + max_email_address: the maximum number of local email addresses allowed + for a standard user. + delete_notyetactive: the number of days before deleting not yet active + users. + disable_emailnotyetconfirmed: the number of days before disabling users + with not yet verified email address. + self_adhesion: whether users can create their account themselves. + all_users_active: whether newly created users are active. + allow_set_password_during_user_creation: whether users can set their + password directly when creating their account. + allow_archived_connexion: whether archived users can connect on the web + interface. + """ DISABLED = "DISABLED" ONLY_INACTIVE = "ONLY_INACTIVE" @@ -158,23 +187,32 @@ class OptionalUser(AclMixin, PreferencesModel): verbose_name = _("user preferences") def clean(self): - """Clean model: - Check the mail_extension - """ + """Check the email extension.""" if self.local_email_domain[0] != "@": raise ValidationError(_("Email domain must begin with @.")) @receiver(post_save, sender=OptionalUser) def optionaluser_post_save(**kwargs): - """Ecriture dans le cache""" + """Write in the cache.""" user_pref = kwargs["instance"] user_pref.set_in_cache() class OptionalMachine(AclMixin, PreferencesModel): - """Options pour les machines : maximum de machines ou d'alias par user - sans droit, activation de l'ipv6""" + """Machines preferences: maximum number of machines per user, IPv6 + activation etc. + + Attributes: + password_machine: whether password per machine is enabled. + max_lambdauser_interfaces: the maximum number of interfaces allowed for + a standard user. + max_lambdauser_aliases: the maximum number of aliases allowed for a + standard user. + ipv6_mode: whether IPv6 mode is enabled. + create_machine: whether creation of machine is enabled. + default_dns_ttl: the default TTL for CNAME, A and AAAA records. + """ SLAAC = "SLAAC" DHCPV6 = "DHCPV6" @@ -197,7 +235,7 @@ class OptionalMachine(AclMixin, PreferencesModel): @cached_property def ipv6(self): - """ Check if the IPv6 option is activated """ + """Check if the IPv6 mode is enabled.""" return not self.get_cached_value("ipv6_mode") == "DISABLED" class Meta: @@ -207,7 +245,7 @@ class OptionalMachine(AclMixin, PreferencesModel): @receiver(post_save, sender=OptionalMachine) def optionalmachine_post_save(**kwargs): - """Synchronisation ipv6 et ecriture dans le cache""" + """Synchronise IPv6 mode and write in the cache.""" machine_pref = kwargs["instance"] machine_pref.set_in_cache() if machine_pref.ipv6_mode != "DISABLED": @@ -216,8 +254,21 @@ def optionalmachine_post_save(**kwargs): class OptionalTopologie(AclMixin, PreferencesModel): - """Reglages pour la topologie : mode d'accès radius, vlan où placer - les machines en accept ou reject""" + """Configuration of switches: automatic provision, RADIUS mode, default + VLANs etc. + + Attributes: + switchs_web_management: whether web management for automatic provision + is enabled. + switchs_web_management_ssl: whether SSL web management is required. + switchs_rest_management: whether REST management for automatic + provision is enabled. + switchs_ip_type: the IP range for the management of switches. + switchs_provision: the provision mode for switches to get their + configuration. + sftp_login: the SFTP login for switches. + sftp_pass: the SFTP password for switches. + """ MACHINE = "MACHINE" DEFINED = "DEFINED" @@ -264,7 +315,7 @@ class OptionalTopologie(AclMixin, PreferencesModel): @cached_property def provisioned_switchs(self): - """Liste des switches provisionnés""" + """Get the list of provisioned switches.""" from topologie.models import Switch return Switch.objects.filter(automatic_provision=True).order_by( @@ -273,7 +324,9 @@ class OptionalTopologie(AclMixin, PreferencesModel): @cached_property def switchs_management_interface(self): - """Return the ip of the interface that the switch have to contact to get it's config""" + """Get the interface that the switch has to contact to get its + configuration. + """ if self.switchs_ip_type: from machines.models import Role, Interface @@ -291,14 +344,16 @@ class OptionalTopologie(AclMixin, PreferencesModel): @cached_property def switchs_management_interface_ip(self): - """Same, but return the ipv4""" + """Get the IPv4 address of the interface that the switch has to contact + to get its configuration. + """ 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""" + """Get the switch credentials for SFTP provisioning.""" if self.sftp_login and self.sftp_pass: return {"login": self.sftp_login, "pass": self.sftp_pass} else: @@ -306,7 +361,9 @@ class OptionalTopologie(AclMixin, PreferencesModel): @cached_property def switchs_management_utils(self): - """Used for switch_conf, return a list of ip on vlans""" + """Get the dictionary of IP addresses for the configuration of + switches. + """ from machines.models import Role, Ipv6List, Interface def return_ips_dict(interfaces): @@ -350,8 +407,7 @@ class OptionalTopologie(AclMixin, PreferencesModel): @cached_property def provision_switchs_enabled(self): - """Return true if all settings are ok : switchs on automatic provision, - ip_type""" + """Check if all automatic provisioning settings are OK.""" return bool( self.provisioned_switchs and self.switchs_ip_type @@ -371,13 +427,20 @@ class OptionalTopologie(AclMixin, PreferencesModel): @receiver(post_save, sender=OptionalTopologie) def optionaltopologie_post_save(**kwargs): - """Ecriture dans le cache""" + """Write in the cache.""" topologie_pref = kwargs["instance"] topologie_pref.set_in_cache() class RadiusKey(AclMixin, models.Model): - """Class of a radius key""" + """Class of a RADIUS key. + + Attributes: + radius_key: the encrypted RADIUS key. + comment: a comment related to the key. + default_switch: bool, True if the key is to be used by default on + switches and False otherwise. + """ radius_key = AESEncryptedField(max_length=255, help_text=_("RADIUS key.")) comment = models.CharField( @@ -393,9 +456,7 @@ class RadiusKey(AclMixin, models.Model): verbose_name_plural = _("RADIUS keys") def clean(self): - """Clean model: - Check default switch is unique - """ + """Check if there is a unique default RADIUS key.""" if RadiusKey.objects.filter(default_switch=True).count() > 1: raise ValidationError(_("Default RADIUS key for switches already exists.")) @@ -404,7 +465,14 @@ class RadiusKey(AclMixin, models.Model): class SwitchManagementCred(AclMixin, models.Model): - """Class of a management creds of a switch, for rest management""" + """Class of a switch management credentials, for rest management. + + Attributes: + management_id: the login used to connect to switches. + management_pass: the encrypted password used to connect to switches. + default_switch: bool, True if the credentials are to be used by default + on switches and False otherwise. + """ management_id = models.CharField(max_length=63, help_text=_("Switch login.")) management_pass = AESEncryptedField(max_length=63, help_text=_("Password.")) @@ -426,9 +494,13 @@ class SwitchManagementCred(AclMixin, models.Model): 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 + """Reminder of membership's end preferences: email messages, number of days + before sending emails. + + Attributes: + days: the number of days before the membership's end to send the + reminder. + message: the content of the reminder. """ days = models.IntegerField( @@ -460,8 +532,26 @@ class Reminder(AclMixin, models.Model): class GeneralOption(AclMixin, PreferencesModel): - """Options générales : nombre de resultats par page, nom du site, - temps où les liens sont valides""" + """General preferences: number of search results per page, website name + etc. + + Attributes: + general_message_fr: general message displayed on the French version of + the website (e.g. in case of maintenance). + general_message_en: general message displayed on the English version of + the website (e.g. in case of maintenance). + search_display_page: number of results displayed (in each category) + when searching. + pagination_number: number of items per page (standard size). + pagination_large_number: number of items per page (large size). + req_expire_hrs: number of hours before expiration of the reset password + link. + site_name: website name. + email_from: email address for automatic emailing. + main_site_url: main site URL. + GTU_sum_up: summary of the General Terms of Use. + GTU: file, General Terms of Use. + """ general_message_fr = models.TextField( default="", @@ -496,14 +586,20 @@ class GeneralOption(AclMixin, PreferencesModel): @receiver(post_save, sender=GeneralOption) def generaloption_post_save(**kwargs): - """Ecriture dans le cache""" + """Write in the cache.""" general_pref = kwargs["instance"] general_pref.set_in_cache() class Service(AclMixin, models.Model): - """Liste des services affichés sur la page d'accueil : url, description, - image et nom""" + """Service displayed on the home page. + + Attributes: + name: the name of the service. + url: the URL of the service. + description: the description of the service. + image: an image to illustrate the service (e.g. logo). + """ name = models.CharField(max_length=32) url = models.URLField() @@ -520,7 +616,12 @@ class Service(AclMixin, models.Model): class MailContact(AclMixin, models.Model): - """Contact email adress with a commentary.""" + """Contact email address with a comment. + + Attributes: + address: the contact email address. + commentary: a comment used to describe the contact email address. + """ address = models.EmailField( default="contact@example.org", help_text=_("Contact email address.") @@ -549,6 +650,15 @@ class MailContact(AclMixin, models.Model): class Mandate(RevMixin, AclMixin, models.Model): + """Mandate, documenting who was the president of the organisation at a + given time. + + Attributes: + president: User, the president during the mandate. + start_date: datetime, the date when the mandate started. + end_date: datetime, the date when the mandate ended. + """ + class Meta: verbose_name = _("mandate") verbose_name_plural = _("mandates") @@ -567,7 +677,14 @@ class Mandate(RevMixin, AclMixin, models.Model): @classmethod def get_mandate(cls, date=timezone.now): - """"Find the mandate taking place at the given date.""" + """"Get the mandate taking place at the given date. + + Args: + date: the date used to find the mandate (default: timezone.now). + + Returns: + The mandate related to the given date. + """ if callable(date): date = date() mandate = ( @@ -590,7 +707,21 @@ class Mandate(RevMixin, AclMixin, models.Model): class AssoOption(AclMixin, PreferencesModel): - """Options générales de l'asso : siret, addresse, nom, etc""" + """Information about the organisation: name, address, SIRET number etc. + + Attributes: + name: the name of the organisation. + siret: the SIRET number of the organisation. + adresse1: the first line of the organisation's address, e.g. street and + number. + adresse2: the second line of the organisation's address, e.g. city and + postal code. + contact: contact email address. + telephone: contact telephone number. + pseudo: short name of the organisation. + utilisateur_asso: the user used to manage the organisation. + description: the description of the organisation. + """ name = models.CharField( default=_("Networking organisation school Something"), max_length=256 @@ -613,13 +744,20 @@ class AssoOption(AclMixin, PreferencesModel): @receiver(post_save, sender=AssoOption) def assooption_post_save(**kwargs): - """Ecriture dans le cache""" + """Write in the cache.""" asso_pref = kwargs["instance"] asso_pref.set_in_cache() class HomeOption(AclMixin, PreferencesModel): - """Settings of the home page (facebook/twitter etc)""" + """Social networks displayed on the home page (supports only Facebook and + Twitter). + + Attributes: + facebook_url: URL of the Facebook account. + twitter_url: URL of the Twitter account. + twitter_account_name: name of the Twitter account. + """ facebook_url = models.URLField(null=True, blank=True) twitter_url = models.URLField(null=True, blank=True) @@ -632,13 +770,18 @@ class HomeOption(AclMixin, PreferencesModel): @receiver(post_save, sender=HomeOption) def homeoption_post_save(**kwargs): - """Ecriture dans le cache""" + """Write in the cache.""" home_pref = kwargs["instance"] home_pref.set_in_cache() class MailMessageOption(AclMixin, models.Model): - """Reglages, mail de bienvenue et autre""" + """Welcome email messages preferences. + + Attributes: + welcome_mail_fr: the text of the welcome email in French. + welcome_mail_en: the text of the welcome email in English. + """ welcome_mail_fr = models.TextField( default="", blank=True, help_text=_("Welcome email in French.") @@ -655,6 +798,14 @@ class MailMessageOption(AclMixin, models.Model): class RadiusAttribute(RevMixin, AclMixin, models.Model): + """RADIUS attributes preferences. + + Attributes: + attribute: the name of the RADIUS attribute. + value: the value of the RADIUS attribute. + comment: the comment to document the attribute. + """ + class Meta: verbose_name = _("RADIUS attribute") verbose_name_plural = _("RADIUS attributes") @@ -677,6 +828,30 @@ class RadiusAttribute(RevMixin, AclMixin, models.Model): class RadiusOption(AclMixin, PreferencesModel): + """RADIUS preferences. + + Attributes: + radius_general_policy: the general RADIUS policy (MACHINE or DEFINED). + unknown_machine: the RADIUS policy for unknown machines. + unknown_machine_vlan: the VLAN for unknown machines if not rejected. + unknown_machine_attributes: the answer attributes for unknown machines. + unknown_port: the RADIUS policy for unknown ports. + unknown_port_vlan: the VLAN for unknown ports if not rejected; + unknown_port_attributes: the answer attributes for unknown ports. + unknown_room: the RADIUS policy for machines connecting from + unregistered rooms (relevant for ports with STRICT RADIUS mode). + unknown_room_vlan: the VLAN for unknown rooms if not rejected. + unknown_room_attributes: the answer attributes for unknown rooms. + non_member: the RADIUS policy for non members. + non_member_vlan: the VLAN for non members if not rejected. + non_member_attributes: the answer attributes for non members. + banned: the RADIUS policy for banned users. + banned_vlan: the VLAN for banned users if not rejected. + banned_attributes: the answer attributes for banned users. + vlan_decision_ok: the VLAN for accepted machines. + ok_attributes: the answer attributes for accepted machines. + """ + class Meta: verbose_name = _("RADIUS policy") verbose_name_plural = _("RADIUS policies") @@ -847,6 +1022,15 @@ def default_voucher(): class CotisationsOption(AclMixin, PreferencesModel): + """Subscription preferences. + + Attributes: + invoice_template: the template for invoices. + voucher_template: the template for vouchers. + send_voucher_mail: whether the voucher is sent by email when the + invoice is controlled. + """ + class Meta: verbose_name = _("subscription preferences") @@ -877,6 +1061,10 @@ class CotisationsOption(AclMixin, PreferencesModel): class DocumentTemplate(RevMixin, AclMixin, models.Model): """Represent a template in order to create documents such as invoice or subscription voucher. + + Attributes: + template: file, the template used to create documents. + name: the name of the template. """ template = models.FileField(upload_to="templates/", verbose_name=_("template")) @@ -892,9 +1080,8 @@ class DocumentTemplate(RevMixin, AclMixin, models.Model): @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. + """Delete the tempalte file from filesystem when the related + DocumentTemplate object is deleted. """ if instance.template: if os.path.isfile(instance.template.path): @@ -903,10 +1090,8 @@ def auto_delete_file_on_delete(sender, instance, **kwargs): @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. + """Delete the previous file from filesystem when the related + DocumentTemplate object is updated with new file. """ if not instance.pk: return False diff --git a/preferences/views.py b/preferences/views.py index 25e7c009..129e17ff 100644 --- a/preferences/views.py +++ b/preferences/views.py @@ -24,8 +24,8 @@ # Gabriel Détraz, Augustin Lemesle # Gplv2 """ -Vue d'affichage, et de modification des réglages (réglages machine, -topologie, users, service...) +Views to display and edit settings (preferences of machines, users, topology, +services etc.) """ from __future__ import unicode_literals @@ -88,7 +88,7 @@ from . import forms def edit_options_template_function(request, section, forms, models): - """ Edition des préférences générales""" + """View used to edit general preferences.""" model = getattr(models, section, None) form_instance = getattr(forms, "Edit" + section + "Form", None) if not (model or form_instance): @@ -127,8 +127,7 @@ def edit_options_template_function(request, section, forms, models): HomeOption, ) def display_options(request): - """Vue pour affichage des options (en vrac) classé selon les models - correspondants dans un tableau""" + """View used to display preferences sorted by model.""" useroptions, _created = OptionalUser.objects.get_or_create() machineoptions, _created = OptionalMachine.objects.get_or_create() topologieoptions, _created = OptionalTopologie.objects.get_or_create() @@ -188,7 +187,7 @@ def edit_options(request, section): @login_required @can_create(Service) def add_service(request): - """Ajout d'un service de la page d'accueil""" + """View used to add services displayed on the home page.""" service = ServiceForm(request.POST or None, request.FILES or None) if service.is_valid(): service.save() @@ -204,7 +203,7 @@ def add_service(request): @login_required @can_edit(Service) def edit_service(request, service_instance, **_kwargs): - """Edition des services affichés sur la page d'accueil""" + """View used to edit services displayed on the home page.""" service = ServiceForm( request.POST or None, request.FILES or None, instance=service_instance ) @@ -222,7 +221,7 @@ def edit_service(request, service_instance, **_kwargs): @login_required @can_delete(Service) def del_service(request, service_instance, **_kwargs): - """Suppression d'un service de la page d'accueil""" + """View used to delete services displayed on the home page.""" if request.method == "POST": service_instance.delete() messages.success(request, _("The service was deleted.")) @@ -237,7 +236,7 @@ def del_service(request, service_instance, **_kwargs): @login_required @can_create(Reminder) def add_reminder(request): - """Ajout d'un mail de rappel""" + """View used to add reminders.""" reminder = ReminderForm(request.POST or None, request.FILES or None) if reminder.is_valid(): reminder.save() @@ -253,7 +252,7 @@ def add_reminder(request): @login_required @can_edit(Reminder) def edit_reminder(request, reminder_instance, **_kwargs): - """Edition reminder""" + """View used to edit reminders.""" reminder = ReminderForm( request.POST or None, request.FILES or None, instance=reminder_instance ) @@ -271,7 +270,7 @@ def edit_reminder(request, reminder_instance, **_kwargs): @login_required @can_delete(Reminder) def del_reminder(request, reminder_instance, **_kwargs): - """Destruction d'un reminder""" + """View used to delete reminders.""" if request.method == "POST": reminder_instance.delete() messages.success(request, _("The reminder was deleted.")) @@ -286,7 +285,7 @@ def del_reminder(request, reminder_instance, **_kwargs): @login_required @can_create(RadiusKey) def add_radiuskey(request): - """Ajout d'une clef radius""" + """View used to add RADIUS keys.""" radiuskey = RadiusKeyForm(request.POST or None) if radiuskey.is_valid(): radiuskey.save() @@ -301,7 +300,7 @@ def add_radiuskey(request): @can_edit(RadiusKey) def edit_radiuskey(request, radiuskey_instance, **_kwargs): - """Edition des clefs radius""" + """View used to edit RADIUS keys.""" radiuskey = RadiusKeyForm(request.POST or None, instance=radiuskey_instance) if radiuskey.is_valid(): radiuskey.save() @@ -317,7 +316,7 @@ def edit_radiuskey(request, radiuskey_instance, **_kwargs): @login_required @can_delete(RadiusKey) def del_radiuskey(request, radiuskey_instance, **_kwargs): - """Destruction d'un radiuskey""" + """View used to delete RADIUS keys.""" if request.method == "POST": try: radiuskey_instance.delete() @@ -341,7 +340,7 @@ def del_radiuskey(request, radiuskey_instance, **_kwargs): @login_required @can_create(SwitchManagementCred) def add_switchmanagementcred(request): - """Ajout de creds de management""" + """View used to add switch management credentials.""" switchmanagementcred = SwitchManagementCredForm(request.POST or None) if switchmanagementcred.is_valid(): switchmanagementcred.save() @@ -356,7 +355,7 @@ def add_switchmanagementcred(request): @can_edit(SwitchManagementCred) def edit_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs): - """Edition des creds de management""" + """View used to edit switch management credentials.""" switchmanagementcred = SwitchManagementCredForm( request.POST or None, instance=switchmanagementcred_instance ) @@ -374,7 +373,7 @@ def edit_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs) @login_required @can_delete(SwitchManagementCred) def del_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs): - """Destruction d'un switchmanagementcred""" + """View used to delete switch management credentials.""" if request.method == "POST": try: switchmanagementcred_instance.delete() @@ -404,7 +403,7 @@ def del_switchmanagementcred(request, switchmanagementcred_instance, **_kwargs): @login_required @can_create(MailContact) def add_mailcontact(request): - """Add a contact email adress.""" + """View used to add contact email addresses.""" mailcontact = MailContactForm(request.POST or None, request.FILES or None) if mailcontact.is_valid(): mailcontact.save() @@ -420,7 +419,7 @@ def add_mailcontact(request): @login_required @can_edit(MailContact) def edit_mailcontact(request, mailcontact_instance, **_kwargs): - """Edit contact email adress.""" + """View used to edit contact email addresses.""" mailcontact = MailContactForm( request.POST or None, request.FILES or None, instance=mailcontact_instance ) @@ -438,7 +437,7 @@ def edit_mailcontact(request, mailcontact_instance, **_kwargs): @login_required @can_delete_set(MailContact) def del_mailcontact(request, instances): - """Delete an email adress""" + """View used to delete one or several contact email addresses.""" mailcontacts = DelMailContactForm(request.POST or None, instances=instances) if mailcontacts.is_valid(): mailcontacts_dels = mailcontacts.cleaned_data["mailcontacts"] @@ -456,9 +455,7 @@ def del_mailcontact(request, instances): @login_required @can_create(DocumentTemplate) def add_document_template(request): - """ - View used to add a document template. - """ + """View used to add document templates.""" document_template = DocumentTemplateForm( request.POST or None, request.FILES or None ) @@ -480,9 +477,7 @@ def add_document_template(request): @login_required @can_edit(DocumentTemplate) def edit_document_template(request, document_template_instance, **_kwargs): - """ - View used to edit a document_template. - """ + """View used to edit document templates.""" document_template = DocumentTemplateForm( request.POST or None, request.FILES or None, instance=document_template_instance ) @@ -505,9 +500,7 @@ def edit_document_template(request, document_template_instance, **_kwargs): @login_required @can_delete_set(DocumentTemplate) def del_document_template(request, instances): - """ - View used to delete a set of document template. - """ + """View used to delete one or several document templates.""" document_template = DelDocumentTemplateForm( request.POST or None, instances=instances ) @@ -545,7 +538,7 @@ def del_document_template(request, instances): @login_required @can_create(RadiusAttribute) def add_radiusattribute(request): - """Create a RADIUS attribute.""" + """View used to add RADIUS attributes.""" attribute = RadiusAttributeForm(request.POST or None) if attribute.is_valid(): attribute.save() @@ -561,7 +554,7 @@ def add_radiusattribute(request): @login_required @can_edit(RadiusAttribute) def edit_radiusattribute(request, radiusattribute_instance, **_kwargs): - """Edit a RADIUS attribute.""" + """View used to edit RADIUS attributes.""" attribute = RadiusAttributeForm( request.POST or None, instance=radiusattribute_instance ) @@ -579,7 +572,7 @@ def edit_radiusattribute(request, radiusattribute_instance, **_kwargs): @login_required @can_delete(RadiusAttribute) def del_radiusattribute(request, radiusattribute_instance, **_kwargs): - """Delete a RADIUS attribute.""" + """View used to delete RADIUS attributes.""" if request.method == "POST": radiusattribute_instance.delete() messages.success(request, _("The attribute was deleted.")) @@ -594,7 +587,7 @@ def del_radiusattribute(request, radiusattribute_instance, **_kwargs): @login_required @can_create(Mandate) def add_mandate(request): - """Create a mandate.""" + """View used to add mandates.""" mandate = MandateForm(request.POST or None) if mandate.is_valid(): mandate.save() @@ -610,7 +603,7 @@ def add_mandate(request): @login_required @can_edit(Mandate) def edit_mandate(request, mandate_instance, **_kwargs): - """Edit a mandate.""" + """View used to edit mandates.""" mandate = MandateForm(request.POST or None, instance=mandate_instance) if mandate.is_valid(): mandate.save() @@ -626,7 +619,7 @@ def edit_mandate(request, mandate_instance, **_kwargs): @login_required @can_delete(Mandate) def del_mandate(request, mandate_instance, **_kwargs): - """Delete a mandate.""" + """View used to delete mandates.""" if request.method == "POST": mandate_instance.delete() messages.success(request, _("The mandate was deleted.")) diff --git a/re2o/base.py b/re2o/base.py index d9e3efa5..c2863643 100644 --- a/re2o/base.py +++ b/re2o/base.py @@ -21,9 +21,7 @@ # -*- coding: utf-8 -*- """ -Regroupe les fonctions transversales utiles - -Et non corrélées/dépendantes des autres applications +Global independant usefull functions """ import smtplib diff --git a/re2o/context_processors.py b/re2o/context_processors.py index 4bda6712..b6c54e22 100644 --- a/re2o/context_processors.py +++ b/re2o/context_processors.py @@ -19,7 +19,7 @@ # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -"""Fonction de context, variables renvoyées à toutes les vues""" +"""Context functions, runs and results sends globaly to all templates""" from __future__ import unicode_literals @@ -34,8 +34,12 @@ from re2o.settings_local import OPTIONNAL_APPS_RE2O def context_user(request): - """Fonction de context lorsqu'un user est logué (ou non), - renvoie les infos sur l'user, la liste de ses droits, ses machines""" + """Global Context function + + Returns: + dict:Containing user's interfaces and himself if logged, else None + + """ user = request.user if get_language() == "fr": global_message = GeneralOption.get_cached_value("general_message_fr") @@ -61,8 +65,13 @@ def context_user(request): def context_optionnal_apps(request): - """Fonction de context pour générer la navbar en fonction des - apps optionnels""" + """Context functions. Called to add optionnal apps buttons in navbari + + Returns: + dict:Containing optionnal template list of functions for navbar found + in optional apps + + """ optionnal_apps = [import_module(app) for app in OPTIONNAL_APPS_RE2O] optionnal_templates_navbar_user_list = [ app.views.navbar_user() diff --git a/re2o/field_permissions.py b/re2o/field_permissions.py index 90a904be..b55248b3 100644 --- a/re2o/field_permissions.py +++ b/re2o/field_permissions.py @@ -85,7 +85,13 @@ class FieldPermissionModelMixin: class FieldPermissionFormMixin: """ - Construit le formulaire et retire les champs interdits + Build a form, and remove all forbiden fields + + Parameters: + user:Build-in with a Django Form instance, and parameter user in kwargs, + representing calling user for this form. Then test if a field is forbiden + or not with has_field_paremeter model function + """ def __init__(self, *args, **kwargs): diff --git a/re2o/login.py b/re2o/login.py index e7b385b1..1dae9073 100644 --- a/re2o/login.py +++ b/re2o/login.py @@ -45,7 +45,14 @@ DIGEST_LEN = 20 def makeSecret(password): - """ Build a hashed and salted version of the password """ + """ Build a hashed and salted version of the password with SSHA + + Parameters: + password (string): Password to hash + + Returns: + string: Hashed password + """ salt = os.urandom(4) h = hashlib.sha1(password.encode()) h.update(salt) @@ -53,13 +60,30 @@ def makeSecret(password): def hashNT(password): - """ Build a md4 hash of the password to use as the NT-password """ + """ Build a md4 hash of the password to use as the NT-password + + Parameters: + password (string): Password to hash + + Returns: + string: Hashed password + + """ hash_str = hashlib.new("md4", password.encode("utf-16le")).digest() return binascii.hexlify(hash_str).upper() def checkPassword(challenge_password, password): - """ Check if a given password match the hash of a stored password """ + """Check if a given password match the hash of a stored password + + Parameters: + challenge_password (string): Password to verify with hash + password (string): Hashed password to verify + + Returns: + boolean: True if challenge_password and password match + + """ challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode()) digest = challenge_bytes[:DIGEST_LEN] salt = challenge_bytes[DIGEST_LEN:] @@ -69,7 +93,15 @@ def checkPassword(challenge_password, password): def hash_password_salt(hashed_password): - """ Extract the salt from a given hashed password """ + """ Extract the salt from a given hashed password + + Parameters: + hashed_password (string): Hashed password to extract salt + + Returns: + string: Salt of the password + + """ if hashed_password.upper().startswith("{CRYPT}"): hashed_password = hashed_password[7:] if hashed_password.startswith("$"): @@ -243,6 +275,14 @@ class SSHAPasswordHasher(hashers.BasePasswordHasher): class RecryptBackend(ModelBackend): + """Function for legacy users. During auth, if their hash password is different from SSHA or ntlm + password is empty, rehash in SSHA or NTLM + + Returns: + model user instance: Instance of the user logged + + """ + def authenticate(self, username=None, password=None): # we obtain from the classical auth backend the user user = super(RecryptBackend, self).authenticate(None, username, password) diff --git a/re2o/mail_utils.py b/re2o/mail_utils.py index 69d0cfff..2e104ec5 100644 --- a/re2o/mail_utils.py +++ b/re2o/mail_utils.py @@ -22,7 +22,7 @@ # -*- coding: utf-8 -*- # Jean-Romain Garnier """ -Regroupe les fonctions en lien avec les mails +All functions linked with emails here. Non model or app dependant """ from django.utils.translation import ugettext_lazy as _ diff --git a/re2o/mixins.py b/re2o/mixins.py index 77382933..c091a6d9 100644 --- a/re2o/mixins.py +++ b/re2o/mixins.py @@ -93,16 +93,27 @@ class AclMixin(object): @classmethod def get_instance(cls, object_id, *_args, **kwargs): - """Récupère une instance - :return: Une instance de la classe évidemment""" + """Get an instance from its id. + + Parameters: + object_id (int): Id of the instance to find + + Returns: + Django instance: Instance of this class + """ return cls.objects.get(pk=object_id) @classmethod def can_create(cls, user_request, *_args, **_kwargs): - """Verifie que l'user a les bons droits pour créer - un object - :param user_request: instance utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if a user has the right to create an object + + Parameters: + user_request: User calling for this action + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ permission = cls.get_modulename() + ".add_" + cls.get_classname() can = user_request.has_perm(permission) return ( @@ -114,11 +125,16 @@ class AclMixin(object): ) def can_edit(self, user_request, *_args, **_kwargs): - """Verifie que l'user a les bons droits pour editer - cette instance - :param self: Instance à editer - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if a user has the right to edit an instance + + Parameters: + user_request: User calling for this action + self: Instance to edit + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ permission = self.get_modulename() + ".change_" + self.get_classname() can = user_request.has_perm(permission) return ( @@ -130,11 +146,16 @@ class AclMixin(object): ) def can_delete(self, user_request, *_args, **_kwargs): - """Verifie que l'user a les bons droits pour delete - cette instance - :param self: Instance à delete - :param user_request: Utilisateur qui fait la requête - :return: soit True, soit False avec la raison de l'échec""" + """Check if a user has the right to delete an instance + + Parameters: + user_request: User calling for this action + self: Instance to delete + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ permission = self.get_modulename() + ".delete_" + self.get_classname() can = user_request.has_perm(permission) return ( @@ -147,10 +168,15 @@ class AclMixin(object): @classmethod def can_view_all(cls, user_request, *_args, **_kwargs): - """Vérifie qu'on peut bien afficher l'ensemble des objets, - droit particulier view objet correspondant - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if a user can view all instances of an object + + Parameters: + user_request: User calling for this action + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ permission = cls.get_modulename() + ".view_" + cls.get_classname() can = user_request.has_perm(permission) return ( @@ -162,11 +188,16 @@ class AclMixin(object): ) def can_view(self, user_request, *_args, **_kwargs): - """Vérifie qu'on peut bien voir cette instance particulière avec - droit view objet - :param self: instance à voir - :param user_request: instance user qui fait l'edition - :return: True ou False avec la raison de l'échec le cas échéant""" + """Check if a user can view an instance of an object + + Parameters: + user_request: User calling for this action + self: Instance to view + + Returns: + Boolean: True if user_request has the right access to do it, else + false with reason for reject authorization + """ permission = self.get_modulename() + ".view_" + self.get_classname() can = user_request.has_perm(permission) return ( diff --git a/re2o/script_utils.py b/re2o/script_utils.py index c6adbeab..667e38d3 100644 --- a/re2o/script_utils.py +++ b/re2o/script_utils.py @@ -47,7 +47,15 @@ application = get_wsgi_application() def get_user(pseudo): - """Cherche un utilisateur re2o à partir de son pseudo""" + """Find a user from its pseudo + + Parameters: + pseudo (string): pseudo of this user + + Returns: + user instance:Instance of user + + """ user = User.objects.filter(pseudo=pseudo) if len(user) == 0: raise CommandError("Invalid user.") @@ -59,17 +67,20 @@ def get_user(pseudo): def get_system_user(): - """Retourne l'utilisateur système ayant lancé la commande""" + """Find the system user login who used the command + """ return pwd.getpwuid(int(os.getenv("SUDO_UID") or os.getuid())).pw_name def form_cli(Form, user, action, *args, **kwargs): """ - Remplit un formulaire à partir de la ligne de commande - Form : le formulaire (sous forme de classe) à remplir - user : l'utilisateur re2o faisant la modification - action : l'action réalisée par le formulaire (pour les logs) - Les arguments suivants sont transmis tels quels au formulaire. + Fill-in a django form from cli + + Parameters + Form : a django class form to fill-in + user : a re2o user doign the modification + action: the action done with that form, for logs purpose + """ data = {} dumb_form = Form(user=user, *args, **kwargs) diff --git a/re2o/utils.py b/re2o/utils.py index d41bcf9d..7d43e883 100644 --- a/re2o/utils.py +++ b/re2o/utils.py @@ -24,12 +24,12 @@ # -*- coding: utf-8 -*- # David Sinquin, Gabriel Détraz, Lara Kermarec """ -Regroupe les fonctions transversales utiles +A group of very usefull functions for re2o core -Fonction : - - récupérer tous les utilisateurs actifs - - récupérer toutes les machines - - récupérer tous les bans +Functions: + - find all active users + - find all active interfaces + - find all bans etc """ @@ -47,7 +47,14 @@ from preferences.models import AssoOption def get_group_having_permission(*permission_name): - """Returns every group having the permission `permission_name` + """Return all django groups having this permission + + Parameters: + permission name (string): Permission name + + Returns: + re2o groups: Groups having this permission + """ groups = set() for name in permission_name: @@ -60,10 +67,19 @@ def get_group_having_permission(*permission_name): def all_adherent(search_time=None, including_asso=True): - """ Fonction renvoyant tous les users adherents. Optimisee pour n'est - qu'une seule requete sql - Inspecte les factures de l'user et ses cotisation, regarde si elles - sont posterieur à now (end_time)""" + """Return all people who have a valid membership at org. Optimised to make only one + sql query. Build a filter and then apply it to User. Check for each user if a valid + membership is registered at the desired search_time. + + Parameters: + search_time (django datetime): Datetime to perform this search, + if not provided, search_time will be set à timezone.now() + including_asso (boolean): Decide if org itself is included in results + + Returns: + django queryset: Django queryset containing all users with valid membership + + """ if search_time is None: search_time = timezone.now() filter_user = Q( @@ -86,7 +102,18 @@ def all_adherent(search_time=None, including_asso=True): def all_baned(search_time=None): - """ Fonction renvoyant tous les users bannis """ + """Return all people who are banned at org. Optimised to make only one + sql query. Build a filter and then apply it to User. Check for each user + banned at the desired search_time. + + Parameters: + search_time (django datetime): Datetime to perform this search, + if not provided, search_time will be set à timezone.now() + + Returns: + django queryset: Django queryset containing all users banned + + """ if search_time is None: search_time = timezone.now() return User.objects.filter( @@ -97,7 +124,18 @@ def all_baned(search_time=None): def all_whitelisted(search_time=None): - """ Fonction renvoyant tous les users whitelistes """ + """Return all people who have a free access at org. Optimised to make only one + sql query. Build a filter and then apply it to User. Check for each user with a + whitelisted free access at the desired search_time. + + Parameters: + search_time (django datetime): Datetime to perform this search, + if not provided, search_time will be set à timezone.now() + + Returns: + django queryset: Django queryset containing all users whitelisted + + """ if search_time is None: search_time = timezone.now() return User.objects.filter( @@ -108,11 +146,19 @@ def all_whitelisted(search_time=None): def all_has_access(search_time=None, including_asso=True): - """ 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""" + """Return all people who have an valid internet access at org. Optimised to make + only one sql query. Build a filter and then apply it to User. Return users + with a whitelist, or a valid paid access, except banned users. + + Parameters: + search_time (django datetime): Datetime to perform this search, + if not provided, search_time will be set à timezone.now() + including_asso (boolean): Decide if org itself is included in results + + Returns: + django queryset: Django queryset containing all valid connection users + + """ if search_time is None: search_time = timezone.now() filter_user = ( @@ -153,7 +199,20 @@ def all_has_access(search_time=None, including_asso=True): def filter_active_interfaces(interface_set): - """Filtre les machines autorisées à sortir sur internet dans une requête""" + """Return a filter for filtering all interfaces of people who have an valid + internet access at org. + Call all_active_interfaces and then apply filter of theses active users on an + interfaces_set + + Parameters: + interface_set (django queryset): A queryset of interfaces to perform filter + + Returns: + django filter: Django filter to apply to an interfaces queryset, + will return when applied all active interfaces, related with + a user with valid membership + + """ return ( interface_set.filter( machine__in=Machine.objects.filter(user__in=all_has_access()).filter( @@ -171,12 +230,38 @@ def filter_active_interfaces(interface_set): def filter_complete_interfaces(interface_set): - """Appel la fonction précédente avec un prefetch_related ipv6 en plus""" + """Return a filter for filtering all interfaces of people who have an valid + internet access at org. + Call all_active_interfaces and then apply filter of theses active users on an + interfaces_set. Less efficient than filter_active_interfaces, with a prefetch_related + on ipv6 + + Parameters: + interface_set (django queryset): A queryset of interfaces to perform filter + + Returns: + django filter: Django filter to apply to an interfaces queryset, + will return when applied all active interfaces, related with + a user with valid membership + + """ return filter_active_interfaces(interface_set).prefetch_related("ipv6list") def all_active_interfaces(full=False): - """Renvoie l'ensemble des machines autorisées à sortir sur internet """ + """Return a filter for filtering all interfaces of people who have an valid + internet access at org. + Call filter_active_interfaces or filter_complete_interfaces. + + Parameters: + full (boolean): A queryset of interfaces to perform filter. If true, will perform + a complete filter with filter_complete_interfaces + + Returns: + django queryset: Django queryset containing all active interfaces, related with + a user with valid membership + + """ if full: return filter_complete_interfaces(Interface.objects) else: @@ -184,13 +269,30 @@ def all_active_interfaces(full=False): def all_active_assigned_interfaces(full=False): - """ Renvoie l'ensemble des machines qui ont une ipv4 assignées et - disposant de l'accès internet""" + """Return all interfaces of people who have an valid internet access at org, + and with valid ipv4. + Call filter_active_interfaces or filter_complete_interfaces, with parameter full. + + Parameters: + full (boolean): A queryset of interfaces to perform filter. If true, will perform + a complete filter with filter_complete_interfaces + + Returns: + django queryset: Django queryset containing all active interfaces, related with + a user with valid membership, and with valid assigned ipv4 address + + """ return all_active_interfaces(full=full).filter(ipv4__isnull=False) def all_active_interfaces_count(): - """ Version light seulement pour compter""" + """Counts all interfaces of people who have an valid internet access at org. + + Returns: + int: Number of all active interfaces, related with + a user with valid membership. + + """ return Interface.objects.filter( machine__in=Machine.objects.filter(user__in=all_has_access()).filter( active=True @@ -199,12 +301,26 @@ def all_active_interfaces_count(): def all_active_assigned_interfaces_count(): - """ Version light seulement pour compter""" + """Counts all interfaces of people who have an valid internet access at org, + and with valid ipv4. + + Returns: + int: Number of all active interfaces, related with + a user with valid membership, and with valid assigned ipv4 address + + """ return all_active_interfaces_count().filter(ipv4__isnull=False) def remove_user_room(room, force=True): - """ Déménage de force l'ancien locataire de la chambre """ + """Remove the previous user of that room. If force, will not perform a check + of membership on him before doing it + + Parameters: + room (Room instance): Room to make free of user + force (boolean): If true, bypass membership check + + """ try: user = Adherent.objects.get(room=room) except Adherent.DoesNotExist: diff --git a/re2o/views.py b/re2o/views.py index 1a4caf15..6a65bcec 100644 --- a/re2o/views.py +++ b/re2o/views.py @@ -21,8 +21,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Fonctions de la page d'accueil et diverses fonctions utiles pour tous -les views +Welcom main page view, and several template widely used in re2o views """ from __future__ import unicode_literals @@ -50,14 +49,29 @@ from re2o.settings_local import OPTIONNAL_APPS_RE2O def form(ctx, template, request): - """Form générique, raccourci importé par les fonctions views du site""" + """Global template function, used in all re2o views, for building a render with context, + template and request. Adding csrf. + + Parameters: + ctx (dict): Dict of values to transfer to template + template (django template): The django template of this view + request (django request) + + Returns: + Django render: Django render complete view with template, context and request + """ context = ctx context.update(csrf(request)) return render(request, template, context) def index(request): - """Affiche la liste des services sur la page d'accueil de re2o""" + """Display all services provided on main page + + Returns: a form with all services linked and description, and social media + link if provided. + + """ services = [[], [], []] for indice, serv in enumerate(Service.objects.all()): services[indice % 3].append(serv) diff --git a/search/engine.py b/search/engine.py index e0ed1018..c1adc501 100644 --- a/search/engine.py +++ b/search/engine.py @@ -45,21 +45,43 @@ from re2o.base import SortTable, re2o_paginator class Query: """Class representing a query. It can contain the user-entered text, the operator for the query, - and a list of subqueries""" + and a list of subqueries. + + Attributes: + text: the string written by the user in a query. + operator: character used to link subqueries, e.g. "+". + subqueries: list of Query objects when the current query is split in + several parts. + """ def __init__(self, text="", case_sensitive=False): - self.text = text # Content of the query - self.operator = None # Whether a special char (ex "+") was used - self.subqueries = None # When splitting the query in subparts + """Initialise an instance of Query. + + Args: + text: the content of the query (default: ""). + case_sensitive: bool, True if the query is case sensitive and + False if not (default: False). + """ + self.text = text + self.operator = None + self.subqueries = None self.case_sensitive = case_sensitive def add_char(self, char): - """Add the given char to the query's text""" + """Add the given character to the query's text. + + Args: + char: the character to be added. + """ self.text += char def add_operator(self, operator): """Consider a new operator was entered, and that it must be processed. The query's current text is moved to self.subqueries in the form - of a plain Query object""" + of a plain Query object. + + Args: + operator: the operator to be added. + """ self.operator = operator if self.subqueries is None: @@ -71,7 +93,7 @@ class Query: @property def plaintext(self): - """Returns a textual representation of the query's content""" + """Return the textual representation of the query's content.""" if self.operator is not None: return self.operator.join([q.plaintext for q in self.subqueries]) @@ -82,7 +104,7 @@ class Query: def filter_fields(): - """Return the list of fields the search applies to""" + """Return the list of fields the search applies to.""" return ["users", "clubs", "machines", @@ -95,12 +117,12 @@ def filter_fields(): def empty_filters(): - """Build empty filters used by Django""" + """Build empty filters used by Django.""" return {f: Q() for f in filter_fields()} def is_int(variable): - """ Check if the variable can be casted to an integer """ + """Check if the variable can be cast to an integer.""" try: int(variable) except ValueError: @@ -111,8 +133,18 @@ def is_int(variable): def finish_results(request, results, col, order): """Sort the results by applying filters and then limit them to the - number of max results. Finally add the info of the nmax number of results - to the dict""" + number of max results. Finally add the info of the maximum number of + results to the dictionary. + + Args: + request: django request, corresponding to the search. + results: dict, the results of the search. + col: the column used to sort the results. + order: the order used to sort the results. + + Returns: + The dictionary of results sorted and paginated. + """ results["users"] = SortTable.sort( results["users"], col, order, SortTable.USERS_INDEX ) @@ -156,7 +188,16 @@ def finish_results(request, results, col, order): def contains_filter(attribute, word, case_sensitive=False): """Create a django model filtering whether the given attribute - contains the specified value.""" + contains the specified value. + + Args: + attribute: the attribute used to check if it contains the given word or + not. + word: the word used to check if it is contained in the attribute or + not. + case_sensitive: bool, True if the check is case sensitive and + False if not (default: False). + """ if case_sensitive: attr = "{}__{}".format(attribute, "contains") else: @@ -168,12 +209,13 @@ def contains_filter(attribute, word, case_sensitive=False): def search_single_word(word, filters, user, start, end, user_state, email_state, aff, case_sensitive=False): - """ Construct the correct filters to match differents fields of some models + """Construct the correct filters to match differents fields of some models with the given query according to the given filters. - The match field are either CharField or IntegerField that will be displayed + The match fields are either CharField or IntegerField that will be displayed on the results page (else, one might not see why a result has matched the query). IntegerField are matched against the query only if it can be casted - to an int.""" + to an int. + """ # Users if "0" in aff: @@ -333,7 +375,7 @@ def search_single_word(word, filters, user, start, end, def apply_filters(filters, user, aff): - """ Apply the filters constructed by search_single_query. + """Apply the filters constructed by search_single_query. It also takes into account the visual filters defined during the search query. """ @@ -406,8 +448,8 @@ def apply_filters(filters, user, aff): def search_single_query(query, filters, user, start, end, user_state, email_state, aff): - """ Handle different queries an construct the correct filters using - search_single_word""" + """Handle different queries an construct the correct filters using + search_single_word.""" if query.operator == "+": # Special queries with "+" operators should use & rather than | newfilters = empty_filters() diff --git a/search/forms.py b/search/forms.py index 2c495b5a..ef049ffc 100644 --- a/search/forms.py +++ b/search/forms.py @@ -62,7 +62,7 @@ def initial_choices(choice_set): class SearchForm(Form): - """The form for a simple search""" + """Form used to do a simple search.""" q = forms.CharField( label=_("Search"), @@ -78,7 +78,7 @@ class SearchForm(Form): class SearchFormPlus(Form): - """The form for an advanced search (with filters)""" + """Form used to do an advanced search (with filters).""" q = forms.CharField( label=_("Search"), diff --git a/search/views.py b/search/views.py index 00141ed1..6f71000c 100644 --- a/search/views.py +++ b/search/views.py @@ -83,7 +83,7 @@ def get_results(query, request, params): @login_required @can_view_all(User, Machine, Cotisation) def search(request): - """ La page de recherche standard """ + """View used to display the simple search page.""" search_form = SearchForm(request.GET or None) if search_form.is_valid(): return render( @@ -101,7 +101,7 @@ def search(request): @login_required @can_view_all(User, Machine, Cotisation) def searchp(request): - """ La page de recherche avancée """ + """View used to display the advanced search page.""" search_form = SearchFormPlus(request.GET or None) if search_form.is_valid(): return render( diff --git a/tickets/forms.py b/tickets/forms.py index 3001de8a..86f1a953 100644 --- a/tickets/forms.py +++ b/tickets/forms.py @@ -35,7 +35,7 @@ from .models import Ticket, CommentTicket class NewTicketForm(FormRevMixin, ModelForm): - """ Creation of a ticket""" + """Form used to create tickets.""" class Meta: model = Ticket @@ -53,7 +53,7 @@ class NewTicketForm(FormRevMixin, ModelForm): class EditTicketForm(FormRevMixin, ModelForm): - """ Creation of a ticket""" + """Form used to edit tickets.""" class Meta: model = Ticket @@ -65,7 +65,7 @@ class EditTicketForm(FormRevMixin, ModelForm): class CommentTicketForm(FormRevMixin, ModelForm): - """Edit and create comment to a ticket""" + """Form used to create and edit comments of a ticket.""" class Meta: model = CommentTicket diff --git a/tickets/models.py b/tickets/models.py index 470d9375..c36fa6e0 100644 --- a/tickets/models.py +++ b/tickets/models.py @@ -46,7 +46,23 @@ from .preferences.models import TicketOption class Ticket(AclMixin, models.Model): - """Model of a ticket""" + """Model of a ticket. + + Attributes: + user: User, the user creating the ticket. + title: the title of the ticket, chosen by the user. + description: the main content of the ticket, written by the user to + explain their problem. + date: datetime, the date of creation of the ticket. + email: the email address used to reply to the ticket. + solved: boolean, True if the problem explained in the ticket has been + solved, False otherwise. It is used to see easily which tickets + still require attention. + language: the language of the ticket, used to select the appropriate + template when sending automatic emails, e.g. ticket creation. + request: the request displayed if there is an error when sending emails + related to the ticket. + """ user = models.ForeignKey( "users.User", @@ -86,7 +102,7 @@ class Ticket(AclMixin, models.Model): @cached_property def opened_by(self): - """Return full name of this ticket opener""" + """Get the full name of the user who opened the ticket.""" if self.user: return self.user.get_full_name() else: @@ -94,10 +110,13 @@ class Ticket(AclMixin, models.Model): @cached_property def get_mail(self): - """Return the mail of the owner of this ticket""" + """Get the email address of the user who opened the ticket.""" return self.email or self.user.get_mail def publish_mail(self): + """Send an email for a newly opened ticket to the address set in the + preferences. + """ site_url = GeneralOption.get_cached_value("main_site_url") to_addr = TicketOption.get_cached_value("publish_address") context = {"ticket": self, "site_url": site_url} @@ -120,8 +139,8 @@ class Ticket(AclMixin, models.Model): def can_view(self, user_request, *_args, **_kwargs): - """ Check that the user has the right to view the ticket - or that it is the author""" + """Check that the user has the right to view the ticket + or that it is the author.""" if ( not user_request.has_perm("tickets.view_ticket") and self.user != user_request @@ -136,7 +155,7 @@ class Ticket(AclMixin, models.Model): @staticmethod def can_view_all(user_request, *_args, **_kwargs): - """ Check that the user has access to the list of all tickets""" + """Check that the user has access to the list of all tickets.""" can = user_request.has_perm("tickets.view_ticket") return ( can, @@ -147,12 +166,23 @@ class Ticket(AclMixin, models.Model): ) def can_create(user_request, *_args, **_kwargs): - """ Authorise all users to open tickets """ + """Authorise all users to open tickets.""" return True, None, None class CommentTicket(AclMixin, models.Model): - """A comment of a ticket""" + """A comment of a ticket. + + Attributes: + date: datetime, the date of creation of the comment. + comment: the text written as a comment to a ticket. + parent_ticket: the ticket which is commented. + created_at: datetime, the date of creation of the comment. + created_by: the user who wrote the comment. + request: the request used if there is an error when sending emails + related to the comment. + """ + date = models.DateTimeField(auto_now_add=True) comment = models.TextField( max_length=4095, @@ -180,8 +210,8 @@ class CommentTicket(AclMixin, models.Model): return CommentTicket.objects.filter(parent_ticket=self.parent_ticket, pk__lt=self.pk).count() + 1 def can_view(self, user_request, *_args, **_kwargs): - """ Check that the user has the right to view the ticket comment - or that it is the author""" + """Check that the user has the right to view the ticket comment + or that it is the author.""" if ( not user_request.has_perm("tickets.view_commentticket") and self.parent_ticket.user != user_request @@ -195,8 +225,8 @@ class CommentTicket(AclMixin, models.Model): return True, None, None def can_edit(self, user_request, *_args, **_kwargs): - """ Check that the user has the right to edit the ticket comment - or that it is the author""" + """Check that the user has the right to edit the ticket comment + or that it is the author.""" if ( not user_request.has_perm("tickets.change_commentticket") and (self.parent_ticket.user != user_request or self.parent_ticket.user != self.created_by) @@ -211,7 +241,7 @@ class CommentTicket(AclMixin, models.Model): @staticmethod def can_view_all(user_request, *_args, **_kwargs): - """ Check that the user has access to the list of all tickets comments""" + """Check that the user has access to the list of all tickets comments.""" can = user_request.has_perm("tickets.view_commentticket") return ( can, @@ -225,7 +255,9 @@ class CommentTicket(AclMixin, models.Model): return "Comment " + str(self.comment_id) + " on " + str(self.parent_ticket) def publish_mail(self): - """Send mail to user and admin after new comment""" + """Send an email for a newly written comment to the ticket's author and + to the address set in the preferences. + """ site_url = GeneralOption.get_cached_value("main_site_url") to_addr = TicketOption.get_cached_value("publish_address") context = {"comment": self, "site_url": site_url} @@ -246,7 +278,7 @@ class CommentTicket(AclMixin, models.Model): @receiver(post_save, sender=Ticket) def ticket_post_save(**kwargs): - """ Send the mail to publish the new ticket """ + """Call the method to publish an email when a ticket is created.""" if kwargs["created"]: if TicketOption.get_cached_value("publish_address"): ticket = kwargs["instance"] @@ -255,7 +287,7 @@ def ticket_post_save(**kwargs): @receiver(post_save, sender=CommentTicket) def comment_post_save(**kwargs): - """ Send the mail to publish the new comment """ + """Call the method to publish an email when a comment is created.""" if kwargs["created"]: if TicketOption.get_cached_value("publish_address"): comment = kwargs["instance"] diff --git a/tickets/preferences/forms.py b/tickets/preferences/forms.py index 04845ab8..4d3f16d3 100644 --- a/tickets/preferences/forms.py +++ b/tickets/preferences/forms.py @@ -32,7 +32,7 @@ from .models import TicketOption class EditTicketOptionForm(FormRevMixin, ModelForm): - """ Edit the ticket's settings""" + """Form used to edit the settings of tickets.""" class Meta: model = TicketOption diff --git a/tickets/preferences/models.py b/tickets/preferences/models.py index d02202f3..221c04a6 100644 --- a/tickets/preferences/models.py +++ b/tickets/preferences/models.py @@ -32,7 +32,7 @@ from preferences.models import PreferencesModel class TicketOption(AclMixin, PreferencesModel): - """ Definition of the ticket's settings""" + """Definition of the settings of tickets.""" publish_address = models.EmailField( help_text=_( diff --git a/tickets/preferences/views.py b/tickets/preferences/views.py index 70637db8..732a7cbe 100644 --- a/tickets/preferences/views.py +++ b/tickets/preferences/views.py @@ -42,7 +42,7 @@ from . import models def aff_preferences(request): - """ View to display the settings of the tickets in the preferences page""" + """View used to display the settings of tickets in the preferences page.""" pref, created = models.TicketOption.objects.get_or_create() context = { "preferences": pref, diff --git a/tickets/views.py b/tickets/views.py index aab6aeae..c1796f8d 100644 --- a/tickets/views.py +++ b/tickets/views.py @@ -20,10 +20,6 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -# App de gestion des users pour re2o -# Lara Kermarec, Gabriel Détraz, Lemesle Augustin -# Gplv2 - from django.contrib import messages from django.contrib.auth.decorators import login_required from django.shortcuts import render, redirect @@ -52,7 +48,7 @@ from .forms import NewTicketForm, EditTicketForm, CommentTicketForm def new_ticket(request): - """ Ticket creation view""" + """View used to display the creation form of tickets.""" ticketform = NewTicketForm(request.POST or None, request=request) if ticketform.is_valid(): ticketform.save() @@ -76,7 +72,7 @@ def new_ticket(request): @login_required @can_view(Ticket) def aff_ticket(request, ticket, ticketid): - """View to display only one ticket""" + """View used to display a single ticket.""" comments = CommentTicket.objects.filter(parent_ticket=ticket) return render( request, @@ -88,7 +84,7 @@ def aff_ticket(request, ticket, ticketid): @login_required @can_edit(Ticket) def change_ticket_status(request, ticket, ticketid): - """View to edit ticket state""" + """View used to change a ticket's status.""" ticket.solved = not ticket.solved ticket.save() return redirect( @@ -99,7 +95,7 @@ def change_ticket_status(request, ticket, ticketid): @login_required @can_edit(Ticket) def edit_ticket(request, ticket, ticketid): - """ Ticket creation view""" + """View used to display the edit form of tickets.""" ticketform = EditTicketForm(request.POST or None, instance=ticket) if ticketform.is_valid(): ticketform.save() @@ -120,7 +116,7 @@ def edit_ticket(request, ticket, ticketid): @login_required @can_view(Ticket) def add_comment(request, ticket, ticketid): - """ Add a comment to a ticket""" + """View used to add a comment to a ticket.""" commentticket = CommentTicketForm(request.POST or None, request=request) if commentticket.is_valid(): commentticket = commentticket.save(commit=False) @@ -139,7 +135,7 @@ def add_comment(request, ticket, ticketid): @login_required @can_edit(CommentTicket) def edit_comment(request, commentticket_instance, **_kwargs): - """ Edit a comment of a ticket""" + """View used to edit a comment of a ticket.""" commentticket = CommentTicketForm(request.POST or None, instance=commentticket_instance) if commentticket.is_valid(): ticketid = commentticket_instance.parent_ticket.id @@ -157,7 +153,7 @@ def edit_comment(request, commentticket_instance, **_kwargs): @login_required @can_delete(CommentTicket) def del_comment(request, commentticket, **_kwargs): - """Delete a comment of a ticket""" + """View used to delete a comment of a ticket.""" if request.method == "POST": ticketid = commentticket.parent_ticket.id commentticket.delete() @@ -173,7 +169,7 @@ def del_comment(request, commentticket, **_kwargs): @login_required @can_view_all(Ticket) def aff_tickets(request): - """ View to display all the tickets """ + """View used to display all tickets.""" tickets_list = Ticket.objects.all().order_by("-date") nbr_tickets = tickets_list.count() nbr_tickets_unsolved = tickets_list.filter(solved=False).count() @@ -196,9 +192,9 @@ def aff_tickets(request): return render(request, "tickets/index.html", context=context) -# views cannoniques des apps optionnels +# Canonic views for optional apps def profil(request, user): - """ View to display the ticket's module on the profil""" + """View used to display the tickets on a user's profile.""" tickets_list = Ticket.objects.filter(user=user).all().order_by("-date") nbr_tickets = tickets_list.count() nbr_tickets_unsolved = tickets_list.filter(solved=False).count() @@ -223,16 +219,19 @@ def profil(request, user): def contact(request): - """View to display a contact address on the contact page - used here to display a link to open a ticket""" + """View used to display contact addresses to be used for tickets on the + contact page. + """ return render_to_string("tickets/contact.html") def navbar_user(): - """View to display the ticket link in thet user's dropdown in the navbar""" + """View used to display a link to tickets in the navbar (in the dropdown + menu Users). + """ return ("users", render_to_string("tickets/navbar.html")) def navbar_logout(): - """View to display the ticket link to log out users""" + """View used to display a link to open tickets for logged out users.""" return render_to_string("tickets/navbar_logout.html") diff --git a/topologie/admin.py b/topologie/admin.py index bc4ca81e..bd5a61c4 100644 --- a/topologie/admin.py +++ b/topologie/admin.py @@ -20,8 +20,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. -""" -Fichier définissant les administration des models dans l'interface admin +"""topologie.admin +The objects, fields and datastructures visible in the Django admin view. """ from __future__ import unicode_literals @@ -45,67 +45,67 @@ from .models import ( class StackAdmin(VersionAdmin): - """Administration d'une stack de switches (inclus des switches)""" + """Admin class of stacks (includes switches).""" pass class SwitchAdmin(VersionAdmin): - """Administration d'un switch""" + """Admin class of switches.""" pass class PortAdmin(VersionAdmin): - """Administration d'un port de switches""" + """Admin class of switch ports.""" pass class AccessPointAdmin(VersionAdmin): - """Administration d'une borne""" + """Admin class of APs.""" pass class RoomAdmin(VersionAdmin): - """Administration d'un chambre""" + """Admin class of rooms.""" pass class ModelSwitchAdmin(VersionAdmin): - """Administration d'un modèle de switch""" + """Admin class of switch models.""" pass class ConstructorSwitchAdmin(VersionAdmin): - """Administration d'un constructeur d'un switch""" + """Admin class of switch constructors.""" pass class SwitchBayAdmin(VersionAdmin): - """Administration d'une baie de brassage""" + """Admin class of switch bays.""" pass class BuildingAdmin(VersionAdmin): - """Administration d'un batiment""" + """Admin class of buildings.""" pass class DormitoryAdmin(VersionAdmin): - """Administration d'une residence""" + """Admin class of dormitories.""" pass class PortProfileAdmin(VersionAdmin): - """Administration of a port profile""" + """Admin class of port profiles.""" pass diff --git a/topologie/forms.py b/topologie/forms.py index 91cc8367..205be5fb 100644 --- a/topologie/forms.py +++ b/topologie/forms.py @@ -20,14 +20,12 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Un forms le plus simple possible pour les objets topologie de re2o. +Forms for the topologie app of re2o. -Permet de créer et supprimer : un Port de switch, relié à un switch. - -Permet de créer des stacks et d'y ajouter des switchs (StackForm) - -Permet de créer, supprimer et editer un switch (EditSwitchForm, -NewSwitchForm) +The forms are used to: + * create and delete switch ports, related to a switch. + * create stacks and add switches to them (StackForm). + * create, edit and delete a switch (NewSwitchForm, EditSwitchForm). """ from __future__ import unicode_literals @@ -59,8 +57,7 @@ from .models import ( class PortForm(FormRevMixin, ModelForm): - """Formulaire pour la création d'un port d'un switch - Relié directement au modèle port""" + """Form used to manage a switch's port.""" class Meta: model = Port @@ -72,14 +69,11 @@ class PortForm(FormRevMixin, ModelForm): class EditPortForm(FormRevMixin, ModelForm): - """Form pour l'édition d'un port de switche : changement des reglages - radius ou vlan, ou attribution d'une chambre, autre port ou machine + """Form used to edit a switch's port: change in RADIUS or VLANs settings, + assignement to a room, port or machine. - Un port est relié à une chambre, un autre port (uplink) ou une machine - (serveur ou borne), mutuellement exclusif - Optimisation sur les queryset pour machines et port_related pour - optimiser le temps de chargement avec select_related (vraiment - lent sans)""" + A port is related to either a room, another port (uplink) or a machine (server or AP). + """ class Meta(PortForm.Meta): fields = [ @@ -106,8 +100,7 @@ class EditPortForm(FormRevMixin, ModelForm): class AddPortForm(FormRevMixin, ModelForm): - """Permet d'ajouter un port de switch. Voir EditPortForm pour plus - d'informations""" + """Form used to add a switch's port. See EditPortForm.""" class Meta(PortForm.Meta): fields = [ @@ -139,8 +132,7 @@ class AddPortForm(FormRevMixin, ModelForm): class StackForm(FormRevMixin, ModelForm): - """Permet d'edition d'une stack : stack_id, et switches membres - de la stack""" + """Form used to create and edit stacks.""" class Meta: model = Stack @@ -152,8 +144,7 @@ class StackForm(FormRevMixin, ModelForm): class AddAccessPointForm(NewMachineForm): - """Formulaire pour la création d'une borne - Relié directement au modèle borne""" + """Form used to create access points.""" class Meta: model = AccessPoint @@ -161,7 +152,7 @@ class AddAccessPointForm(NewMachineForm): class EditAccessPointForm(EditMachineForm): - """Edition d'une borne. Edition complète""" + """Form used to edit access points.""" class Meta: model = AccessPoint @@ -169,7 +160,7 @@ class EditAccessPointForm(EditMachineForm): class EditSwitchForm(EditMachineForm): - """Permet d'éditer un switch : nom et nombre de ports""" + """Form used to edit switches.""" class Meta: model = Switch @@ -177,15 +168,14 @@ class EditSwitchForm(EditMachineForm): class NewSwitchForm(NewMachineForm): - """Permet de créer un switch : emplacement, paramètres machine, - membre d'un stack (option), nombre de ports (number)""" + """Form used to create a switch.""" class Meta(EditSwitchForm.Meta): fields = ["name", "switchbay", "number", "stack", "stack_member_id"] class EditRoomForm(FormRevMixin, ModelForm): - """Permet d'éediter le nom et commentaire d'une prise murale""" + """Form used to edit a room.""" class Meta: model = Room @@ -197,14 +187,14 @@ class EditRoomForm(FormRevMixin, ModelForm): class CreatePortsForm(forms.Form): - """Permet de créer une liste de ports pour un switch.""" + """Form used to create switch ports lists.""" begin = forms.IntegerField(label=_("Start:"), min_value=0) end = forms.IntegerField(label=_("End:"), min_value=0) class EditModelSwitchForm(FormRevMixin, ModelForm): - """Permet d'éediter un modèle de switch : nom et constructeur""" + """Form used to edit switch models.""" members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) @@ -226,7 +216,7 @@ class EditModelSwitchForm(FormRevMixin, ModelForm): class EditConstructorSwitchForm(FormRevMixin, ModelForm): - """Permet d'éediter le nom d'un constructeur""" + """Form used to edit switch constructors.""" class Meta: model = ConstructorSwitch @@ -238,7 +228,7 @@ class EditConstructorSwitchForm(FormRevMixin, ModelForm): class EditSwitchBayForm(FormRevMixin, ModelForm): - """Permet d'éditer une baie de brassage""" + """Form used to edit switch bays.""" members = forms.ModelMultipleChoiceField(Switch.objects.all(), required=False) @@ -260,7 +250,7 @@ class EditSwitchBayForm(FormRevMixin, ModelForm): class EditBuildingForm(FormRevMixin, ModelForm): - """Permet d'éditer le batiment""" + """Form used to edit buildings.""" class Meta: model = Building @@ -272,7 +262,7 @@ class EditBuildingForm(FormRevMixin, ModelForm): class EditDormitoryForm(FormRevMixin, ModelForm): - """Enable dormitory edition""" + """Form used to edit dormitories.""" class Meta: model = Dormitory @@ -284,7 +274,7 @@ class EditDormitoryForm(FormRevMixin, ModelForm): class EditPortProfileForm(FormRevMixin, ModelForm): - """Form to edit a port profile""" + """Form used to edit port profiles.""" class Meta: model = PortProfile @@ -296,7 +286,7 @@ class EditPortProfileForm(FormRevMixin, ModelForm): class EditModuleForm(FormRevMixin, ModelForm): - """Add and edit module instance""" + """Form used to add and edit switch modules.""" class Meta: model = ModuleSwitch @@ -308,7 +298,7 @@ class EditModuleForm(FormRevMixin, ModelForm): class EditSwitchModuleForm(FormRevMixin, ModelForm): - """Add/edit a switch to a module""" + """Form used to add and edit modules related to a switch.""" class Meta: model = ModuleOnSwitch diff --git a/topologie/models.py b/topologie/models.py index a184bde4..85f6d188 100644 --- a/topologie/models.py +++ b/topologie/models.py @@ -21,18 +21,15 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Definition des modèles de l'application topologie. +Definition of models for the 'topologie' app. -On défini les models suivants : - -- stack (id, id_min, id_max et nom) regrouppant les switches -- switch : nom, nombre de port, et interface -machine correspondante (mac, ip, etc) (voir machines.models.interface) -- Port: relié à un switch parent par foreign_key, numero du port, -relié de façon exclusive à un autre port, une machine -(serveur ou borne) ou une prise murale -- room : liste des prises murales, nom et commentaire de l'état de -la prise +The following models are defined: + * stack (grouping switches): id, id_min, id_max and name + * switch: name, number of ports, related interface and machine (MAC + address, IP address etc.) (see machines.models.interface) + * port: related to a switch by foreign_key, number of the port, related + exclusively to another port, machine (server or AP) or room outlets + * room: list of outlets, name and comments about the plug's state """ from __future__ import unicode_literals @@ -56,9 +53,15 @@ from re2o.mixins import AclMixin, RevMixin class Stack(AclMixin, RevMixin, models.Model): - """Un objet stack. Regrouppe des switchs en foreign key - ,contient une id de stack, un switch id min et max dans - le stack""" + """Switch stack. + + Attributes: + name: the name of the stack. + stack_id: the ID of the stack, as a text chosen by the user. + details: the description to provide details about the stack. + member_id_min: the minimum switch ID in the stack. + member_id_max: the maximum switch ID in the stack. + """ name = models.CharField(max_length=32, blank=True, null=True) stack_id = models.CharField(max_length=32, unique=True) @@ -81,7 +84,7 @@ class Stack(AclMixin, RevMixin, models.Model): super(Stack, self).save(*args, **kwargs) def clean(self): - """ Verification que l'id_max < id_min""" + """Check if id_max < id_min.""" if self.member_id_max < self.member_id_min: raise ValidationError( {"member_id_max": _("The maximum ID is less than the minimum ID.")} @@ -89,9 +92,10 @@ class Stack(AclMixin, RevMixin, models.Model): class AccessPoint(Machine): - """Define a wireless AP. Inherit from machines.interfaces + """Wireless Access Point. Inherits from machines.interfaces. - Definition pour une borne wifi , hérite de machines.interfaces + Attributes: + location: the text to provide details about the AP's location. """ location = models.CharField( @@ -107,17 +111,16 @@ class AccessPoint(Machine): verbose_name_plural = _("access points") def port(self): - """Return the queryset of ports for this device""" + """Return the queryset of ports for this device.""" return Port.objects.filter(machine_interface__machine=self) def switch(self): - """Return the switch where this is plugged""" + """Return the switch where this is plugged.""" return Switch.objects.filter(ports__machine_interface__machine=self) def building(self): - """ - Return the building of the AP/Server (building of the switchs - connected to...) + """Return the building of the AP/Server (building of the switches + connected to...). """ return Building.objects.filter(switchbay__switch=self.switch()) @@ -127,7 +130,14 @@ class AccessPoint(Machine): @classmethod def all_ap_in(cls, building_instance): - """Get a building as argument, returns all ap of a building""" + """Get all the APs of the given building. + + Args: + building_instance: the building used to find APs. + + Returns: + The queryset of all APs in the given building. + """ return cls.objects.filter( interface__port__switch__switchbay__building=building_instance ) @@ -158,25 +168,24 @@ class AccessPoint(Machine): class Server(Machine): - """ - Dummy class, to retrieve servers of a building, or get switch of a server + """Dummy class, to retrieve servers of a building, or get switch of a + server. """ class Meta: proxy = True def port(self): - """Return the queryset of ports for this device""" + """Return the queryset of ports for this device.""" return Port.objects.filter(machine_interface__machine=self) def switch(self): - """Return the switch where this is plugged""" + """Return the switch where this is plugged.""" return Switch.objects.filter(ports__machine_interface__machine=self) def building(self): - """ - Return the building of the AP/Server - (building of the switchs connected to...) + """Return the building of the AP/Server (building of the switches + connected to...). """ return Building.objects.filter(switchbay__switch=self.switch()) @@ -186,7 +195,14 @@ class Server(Machine): @classmethod def all_server_in(cls, building_instance): - """Get a building as argument, returns all server of a building""" + """Get all the servers of the given building. + + Args: + building_instance: the building used to find servers. + + Returns: + The queryset of all servers in the given building. + """ return cls.objects.filter( interface__port__switch__switchbay__building=building_instance ).exclude(accesspoint__isnull=False) @@ -217,17 +233,19 @@ class Server(Machine): class Switch(Machine): - """ Definition d'un switch. Contient un nombre de ports (number), - un emplacement (location), un stack parent (optionnel, stack) - et un id de membre dans le stack (stack_member_id) - relié en onetoone à une interface - Pourquoi ne pas avoir fait hériter switch de interface ? - Principalement par méconnaissance de la puissance de cette façon de faire. - Ceci étant entendu, django crée en interne un onetoone, ce qui a un - effet identique avec ce que l'on fait ici + """Switch. - Validation au save que l'id du stack est bien dans le range id_min - id_max de la stack parente""" + Attributes: + number: the number of ports of the switch. + stack: the stack the switch is a part of. + stack_member_id: the ID of the switch in the related stack. + model: the model of the switch. + switchbay: the bay in which the switch is located. + radius_key: the RADIUS key of the switch. + management_creds: the management credentials of the switch. + automatic_provision: whether automatic provision is enabled for the + switch. + """ number = models.PositiveIntegerField(help_text=_("Number of ports.")) stack = models.ForeignKey( @@ -269,8 +287,9 @@ class Switch(Machine): verbose_name_plural = _("switches") def clean(self): - """ Verifie que l'id stack est dans le bon range - Appelle également le clean de la classe parente""" + """Check if the stack member ID is in the range of the stack's IDs and + calls the clean of the parent class. + """ super(Switch, self).clean() if self.stack is not None: if self.stack_member_id is not None: @@ -291,8 +310,12 @@ class Switch(Machine): ) def create_ports(self, begin, end): - """ Crée les ports de begin à end si les valeurs données - sont cohérentes. """ + """Create ports for the switch if the values are consistent. + + Args: + begin: the number of the start port. + end: the number of the end port. + """ if end < begin: raise ValidationError(_("The end port is less than the start port.")) ports_to_create = range(begin, end + 1) @@ -313,8 +336,7 @@ class Switch(Machine): ) def main_interface(self): - """ Returns the 'main' interface of the switch - It must the the management interface for that device""" + """Get the main interface of the switch (the management interface).""" switch_iptype = OptionalTopologie.get_cached_value("switchs_ip_type") if switch_iptype: return ( @@ -329,12 +351,14 @@ class Switch(Machine): @cached_property def get_radius_key(self): - """Retourne l'objet de la clef radius de ce switch""" + """Get the RADIUS key object related to the switch.""" return self.radius_key or RadiusKey.objects.filter(default_switch=True).first() @cached_property def get_radius_key_value(self): - """Retourne la valeur en str de la clef radius, none si il n'y en a pas""" + """Get the RADIUS key as a string, or None if there are no RADIUS key + related to the switch. + """ if self.get_radius_key: return self.get_radius_key.radius_key else: @@ -362,7 +386,7 @@ class Switch(Machine): @cached_property def get_management_cred(self): - """Retourne l'objet des creds de managament de ce switch""" + """Get the management credentials objects of the switch.""" return ( self.management_creds or SwitchManagementCred.objects.filter(default_switch=True).first() @@ -370,7 +394,9 @@ class Switch(Machine): @cached_property def get_management_cred_value(self): - """Retourne un dict des creds de management du switch""" + """Get the management credentials as a dictionary, or None if there are + no management credentials related to the switch. + """ if self.get_management_cred: return { "id": self.get_management_cred.management_id, @@ -401,17 +427,19 @@ class Switch(Machine): @cached_property def ipv4(self): - """Return the switch's management ipv4""" + """Get the IPv4 address of the switch's management interface.""" return str(self.main_interface().ipv4) @cached_property def ipv6(self): - """Returne the switch's management ipv6""" + """Get the IPv6 address of the switch's management interface.""" return str(self.main_interface().ipv6().first()) @cached_property def interfaces_subnet(self): - """Return dict ip:subnet for all ip of the switch""" + """Get a dictionary of IPv4 addresses:subnets of all the switch's + interfaces. + """ return dict( ( str(interface.ipv4), @@ -423,7 +451,9 @@ class Switch(Machine): @cached_property def interfaces6_subnet(self): - """Return dict ip6:subnet for all ipv6 of the switch""" + """Get a dictionary of IPv6 addresses:subnets of all the switch's + interfaces. + """ return dict( ( str(interface.ipv6().first()), @@ -434,7 +464,9 @@ class Switch(Machine): @cached_property def list_modules(self): - """Return modules of that switch, list of dict (rank, reference)""" + """Get the list of dictionaries (rank, reference) of modules related to + the switch. + """ modules = [] if getattr(self.model, "is_modular", None): if self.model.is_itself_module: @@ -445,7 +477,7 @@ class Switch(Machine): @cached_property def get_dormitory(self): - """Returns the dormitory of that switch""" + """Get the dormitory in which the switch is located.""" if self.switchbay: return self.switchbay.building.dormitory else: @@ -453,19 +485,19 @@ class Switch(Machine): @classmethod def nothing_profile(cls): - """Return default nothing port profile""" + """Return default nothing port profile.""" nothing_profile, _created = PortProfile.objects.get_or_create( profil_default="nothing", name="nothing", radius_type="NO" ) return nothing_profile def profile_type_or_nothing(self, profile_type): - """Return the profile for a profile_type of this switch + """Return the profile for a profile_type of this switch. - If exists, returns the defined default profile for a profile type on the dormitory which - the switch belongs - - Otherwise, returns the nothing profile""" + If it exists, return the defined default profile for a profile type on + the dormitory which the switch belongs. + Otherwise, return the nothing profile. + """ profile_queryset = PortProfile.objects.filter(profil_default=profile_type) if self.get_dormitory: port_profile = ( @@ -478,22 +510,22 @@ class Switch(Machine): @cached_property def default_uplink_profile(self): - """Default uplink profile for that switch -- in cache""" + """Default uplink profile for that switch -- in cache.""" return self.profile_type_or_nothing("uplink") @cached_property def default_access_point_profile(self): - """Default ap profile for that switch -- in cache""" + """Default AP profile for that switch -- in cache.""" return self.profile_type_or_nothing("access_point") @cached_property def default_room_profile(self): - """Default room profile for that switch -- in cache""" + """Default room profile for that switch -- in cache.""" return self.profile_type_or_nothing("room") @cached_property def default_asso_machine_profile(self): - """Default asso machine profile for that switch -- in cache""" + """Default asso machine profile for that switch -- in cache.""" return self.profile_type_or_nothing("asso_machine") def __str__(self): @@ -522,7 +554,16 @@ class Switch(Machine): class ModelSwitch(AclMixin, RevMixin, models.Model): - """Un modèle (au sens constructeur) de switch""" + """Switch model. + + Attributes: + reference: the reference of the switch model. + commercial_name: the commercial name of the switch model. + constructor: the constructor of the switch model. + firmware: the firmware of the switch model. + is_modular: whether the switch model is modular. + is_itself_module: whether the switch is considered as a module. + """ reference = models.CharField(max_length=255) commercial_name = models.CharField(max_length=255, null=True, blank=True) @@ -550,7 +591,12 @@ class ModelSwitch(AclMixin, RevMixin, models.Model): class ModuleSwitch(AclMixin, RevMixin, models.Model): - """A module of a switch""" + """Switch module. + + Attributes: + reference: the reference of the switch module. + comment: the comment to describe the switch module. + """ reference = models.CharField( max_length=255, @@ -575,7 +621,13 @@ class ModuleSwitch(AclMixin, RevMixin, models.Model): class ModuleOnSwitch(AclMixin, RevMixin, models.Model): - """Link beetween module and switch""" + """Link beetween module and switch. + + Attributes: + module: the switch module related to the link. + switch: the switch related to the link. + slot: the slot on the switch related to the link. + """ module = models.ForeignKey("ModuleSwitch", on_delete=models.CASCADE) switch = models.ForeignKey("Switch", on_delete=models.CASCADE) @@ -601,7 +653,11 @@ class ModuleOnSwitch(AclMixin, RevMixin, models.Model): class ConstructorSwitch(AclMixin, RevMixin, models.Model): - """Un constructeur de switch""" + """Switch constructor. + + Attributes: + name: the name of the switch constructor. + """ name = models.CharField(max_length=255) @@ -617,7 +673,13 @@ class ConstructorSwitch(AclMixin, RevMixin, models.Model): class SwitchBay(AclMixin, RevMixin, models.Model): - """Une baie de brassage""" + """Switch bay. + + Attributes: + name: the name of the switch bay. + building: the building in which the switch bay is located. + info: the information to describe to switch bay. + """ name = models.CharField(max_length=255) building = models.ForeignKey("Building", on_delete=models.PROTECT) @@ -633,8 +695,11 @@ class SwitchBay(AclMixin, RevMixin, models.Model): class Dormitory(AclMixin, RevMixin, models.Model): - """A student accomodation/dormitory - Une résidence universitaire""" + """Dormitory. + + Attributes: + name: the name of the dormitory. + """ name = models.CharField(max_length=255) @@ -644,7 +709,7 @@ class Dormitory(AclMixin, RevMixin, models.Model): verbose_name_plural = _("dormitories") def all_ap_in(self): - """Returns all ap of the dorms""" + """Get all the APs in the dormitory.""" return AccessPoint.all_ap_in(self.building_set.all()) @classmethod @@ -660,8 +725,13 @@ class Dormitory(AclMixin, RevMixin, models.Model): class Building(AclMixin, RevMixin, models.Model): - """A building of a dormitory - Un batiment""" + """Building. + + Attributes: + name: the name of the building. + dormitory: the dormitory of the building (a Dormitory can contain + multiple dormitories). + """ name = models.CharField(max_length=255) dormitory = models.ForeignKey("Dormitory", on_delete=models.PROTECT) @@ -672,7 +742,7 @@ class Building(AclMixin, RevMixin, models.Model): verbose_name_plural = _("buildings") def all_ap_in(self): - """Returns all ap of the building""" + """Get all the APs in the building.""" return AccessPoint.all_ap_in(self) def get_name(self): @@ -690,21 +760,32 @@ class Building(AclMixin, RevMixin, models.Model): class Port(AclMixin, RevMixin, models.Model): - """ Definition d'un port. Relié à un switch(foreign_key), - un port peut etre relié de manière exclusive à : - - une chambre (room) - - une machine (serveur etc) (machine_interface) - - un autre port (uplink) (related) - Champs supplémentaires : - - RADIUS (mode STRICT : connexion sur port uniquement si machine - d'un adhérent à jour de cotisation et que la chambre est également à - jour de cotisation - mode COMMON : vérification uniquement du statut de la machine - mode NO : accepte toute demande venant du port et place sur le vlan normal - mode BLOQ : rejet de toute authentification - - vlan_force : override la politique générale de placement vlan, permet - de forcer un port sur un vlan particulier. S'additionne à la politique - RADIUS""" + """Port of a switch. + + A port is related exclusively to either: + * a room + * a machine, e.g. server + * another port + Behaviour according to the RADIUS mode: + * STRICT: connection only if the machine and room have access + * COMMON: check only the machine's state + * NO: accept only request coming from the port and set on the standard + VLAN. + * BLOQ: reject all requests. + The VLAN can be forced to override the general policy for VLAN setting. + This enables to force a port to a particular VLAN. It adds to the RADIUS + policy. + + Attributes: + switch: the switch to which the port belongs. + port: the port number on the switch for the Port object. + room: the room to which the port is related. + machine_interface: the machine to which the port is related + related: the other port to which is port is related. + custom_profile: the port profile of the port. + state: whether the port is active. + details: the details to describre the port. + """ switch = models.ForeignKey("Switch", related_name="ports", on_delete=models.CASCADE) port = models.PositiveIntegerField() @@ -733,7 +814,7 @@ class Port(AclMixin, RevMixin, models.Model): @cached_property def pretty_name(self): - """More elaborated name for label on switch conf""" + """More elaborated name for label on switch configuration.""" if self.related: return _("Uplink: ") + self.related.switch.short_name elif self.machine_interface: @@ -745,14 +826,13 @@ class Port(AclMixin, RevMixin, models.Model): @cached_property def get_port_profile(self): - """Return the config profil for this port - :returns: the profile of self (port) - - If is defined a custom profile, returns it - elIf a default profile is defined for its dormitory, returns it - Else, returns the global default profil - If not exists, create a nothing profile""" + """Get the configuration profile for this port. + Returns: + The custom profile if it exists, else the default profile of the + dormitory if it exists, else the global default profile, else the + nothing profile. + """ if self.custom_profile: return self.custom_profile elif self.related: @@ -779,26 +859,25 @@ class Port(AclMixin, RevMixin, models.Model): ) def make_port_related(self): - """ Synchronise le port distant sur self""" + """Synchronise the related port with self.""" related_port = self.related related_port.related = self related_port.save() def clean_port_related(self): - """ Supprime la relation related sur self""" + """Delete the related relation on self.""" related_port = self.related_port related_port.related = None related_port.save() def clean(self): - """ Verifie que un seul de chambre, interface_parent et related_port - est rempli. Verifie que le related n'est pas le port lui-même.... - Verifie que le related n'est pas déjà occupé par une machine ou une - chambre. Si ce n'est pas le cas, applique la relation related - Si un port related point vers self, on nettoie la relation - A priori pas d'autre solution que de faire ça à la main. A priori - tout cela est dans un bloc transaction, donc pas de problème de - cohérence""" + """ + Check if the port is only related exclusively to either a room, a + machine or another port. + Check if the related port is not self and applies the relation to the + related port if the relation is correct. + Delete the relation if it points to self. + """ if hasattr(self, "switch"): if self.port > self.switch.number: raise ValidationError( @@ -835,7 +914,13 @@ class Port(AclMixin, RevMixin, models.Model): class Room(AclMixin, RevMixin, models.Model): - """Une chambre/local contenant une prise murale""" + """Room. + + Attributes: + name: the name of the room. + details: the details describing the room. + building: the building in which the room is located. + """ name = models.CharField(max_length=255) details = models.CharField(max_length=255, blank=True) @@ -853,7 +938,28 @@ class Room(AclMixin, RevMixin, models.Model): class PortProfile(AclMixin, RevMixin, models.Model): - """Contains the information of the ports' configuration for a switch""" + """Port profile. + + Contains the information of the ports' configuration for a switch. + + Attributes: + name: the name of the port profile. + profil_default: the type of default profile (room, AP, uplink etc.). + on_dormitory: the dormitory with this default port profile. + vlan_untagged: the VLAN untagged of the port profile. + vlan_tagged: the VLAN(s) tagged of the port profile. + radius_type: the type of RADIUS authentication (inactive, MAC-address + or 802.1X) of the port profile. + radius_mode: the RADIUS mode of the port profile. + speed: the port speed limit of the port profile. + mac_limit: the MAC limit of the port profile. + flow_control: whether flow control is enabled. + dhcp_snooping: whether DHCP snooping is enabled. + dhcpv6_snooping: whether DHCPv6 snooping is enabled. + arp_protect: whether ARP protection is enabled. + ra_guard: whether RA guard is enabled. + loop_protect: whether loop protection is enabled. + """ TYPES = (("NO", "NO"), ("802.1X", "802.1X"), ("MAC-radius", _("MAC-RADIUS"))) MODES = (("STRICT", "STRICT"), ("COMMON", "COMMON")) @@ -983,7 +1089,7 @@ class PortProfile(AclMixin, RevMixin, models.Model): return ",".join(self.security_parameters_enabled) def clean(self): - """ Check that there is only one generic profil default""" + """Check that there is only one generic profile default.""" super(PortProfile, self).clean() if ( self.profil_default @@ -1007,21 +1113,21 @@ class PortProfile(AclMixin, RevMixin, models.Model): @receiver(post_save, sender=AccessPoint) def ap_post_save(**_kwargs): - """Regeneration des noms des bornes vers le controleur""" + """Regenerate the AP names towards the controller.""" regen("unifi-ap-names") regen("graph_topo") @receiver(post_delete, sender=AccessPoint) def ap_post_delete(**_kwargs): - """Regeneration des noms des bornes vers le controleur""" + """Regenerate the AP names towards the controller.""" regen("unifi-ap-names") regen("graph_topo") @receiver(post_delete, sender=Stack) def stack_post_delete(**_kwargs): - """Vide les id des switches membres d'une stack supprimée""" + """Empty the stack member ID of switches when a stack is deleted.""" Switch.objects.filter(stack=None).update(stack_member_id=None) diff --git a/topologie/urls.py b/topologie/urls.py index c204cb39..4baf8234 100644 --- a/topologie/urls.py +++ b/topologie/urls.py @@ -19,11 +19,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. -""" -Definition des urls de l'application topologie. -Inclu dans urls de re2o. - -Fait référence aux fonctions du views +"""topologie.urls +The defined URLs for topologie app. Included in re2o.urls. """ from __future__ import unicode_literals diff --git a/topologie/views.py b/topologie/views.py index 3d3f742a..3b1aad30 100644 --- a/topologie/views.py +++ b/topologie/views.py @@ -20,18 +20,17 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Page des vues de l'application topologie +Views for the 'topologie' app of re2o. -Permet de créer, modifier et supprimer : -- un port (add_port, edit_port, del_port) -- un switch : les vues d'ajout et d'édition font appel aux forms de creation -de switch, mais aussi aux forms de machines.forms (domain, interface et -machine). Le views les envoie et les save en même temps. TODO : rationaliser -et faire que la creation de machines (interfaces, domain etc) soit gérée -coté models et forms de topologie -- une chambre (new_room, edit_room, del_room) -- une stack -- l'historique de tous les objets cités +They are used to create, edit and delete: + * a port (add_port, edit_port, del_port) + * a switch: the views call forms for switches but also machines (domain, + interface and machine), send and save them at the same time. + TODO rationalise, enforce the creation of machines (interfaces, domains + etc.) in models and forms from 'topologie' + * a room (new_room, edit_room, del_room) + * a stack + * histories of all objects mentioned. """ from __future__ import unicode_literals @@ -105,7 +104,7 @@ from os.path import isfile @login_required @can_view_all(Switch) def index(request): - """ Vue d'affichage de tous les swicthes""" + """View used to display all switches.""" switch_list = ( Switch.objects.prefetch_related( Prefetch( @@ -171,7 +170,7 @@ def index_port_profile(request): @can_view_all(Port) @can_view(Switch) def index_port(request, switch, switchid): - """ Affichage de l'ensemble des ports reliés à un switch particulier""" + """View used to display all ports related to the given switch.""" port_list = ( Port.objects.filter(switch=switch) .select_related("room__building__dormitory") @@ -204,7 +203,7 @@ def index_port(request, switch, switchid): @login_required @can_view_all(Room) def index_room(request): - """ Affichage de l'ensemble des chambres""" + """View used to display all rooms.""" room_list = Room.objects.select_related("building__dormitory") room_list = SortTable.sort( room_list, @@ -220,7 +219,7 @@ def index_room(request): @login_required @can_view_all(AccessPoint) def index_ap(request): - """ Affichage de l'ensemble des bornes""" + """View used to display all APs.""" ap_list = AccessPoint.objects.prefetch_related( Prefetch( "interface_set", @@ -245,7 +244,7 @@ def index_ap(request): @login_required @can_view_all(Stack, Building, Dormitory, SwitchBay) def index_physical_grouping(request): - """Affichage de la liste des stacks (affiche l'ensemble des switches)""" + """View used to display the list of stacks (display all switches).""" stack_list = Stack.objects.prefetch_related( "switch_set__interface_set__domain__extension" ) @@ -293,7 +292,7 @@ def index_physical_grouping(request): @login_required @can_view_all(ModelSwitch, ConstructorSwitch) def index_model_switch(request): - """ Affichage de l'ensemble des modèles de switches""" + """View used to display all switch models.""" model_switch_list = ModelSwitch.objects.select_related( "constructor" ).prefetch_related("switch_set__interface_set__domain") @@ -323,7 +322,7 @@ def index_model_switch(request): @login_required @can_view_all(ModuleSwitch) def index_module(request): - """Display all modules of switchs""" + """View used to display all switch modules.""" module_list = ModuleSwitch.objects.all() modular_switchs = ( Switch.objects.filter(model__is_modular=True) @@ -342,7 +341,7 @@ def index_module(request): @login_required @can_edit(Vlan) def edit_vlanoptions(request, vlan_instance, **_kwargs): - """ View used to edit options for switch of VLAN object """ + """View used to edit options for switch of VLAN object.""" vlan = EditOptionVlanForm(request.POST or None, instance=vlan_instance) if vlan.is_valid(): if vlan.changed_data: @@ -357,7 +356,7 @@ def edit_vlanoptions(request, vlan_instance, **_kwargs): @login_required @can_create(Port) def new_port(request, switchid): - """ Nouveau port""" + """View used to create ports.""" try: switch = Switch.objects.get(pk=switchid) except Switch.DoesNotExist: @@ -383,9 +382,10 @@ def new_port(request, switchid): @login_required @can_edit(Port) def edit_port(request, port_object, **_kwargs): - """ Edition d'un port. Permet de changer le switch parent et - l'affectation du port""" + """View used to edit ports. + It enables to change the related switch and the port assignment. + """ port = EditPortForm(request.POST or None, instance=port_object) if port.is_valid(): if port.changed_data: @@ -410,7 +410,7 @@ def edit_port(request, port_object, **_kwargs): @login_required @can_delete(Port) def del_port(request, port, **_kwargs): - """ Supprime le port""" + """View used to delete ports.""" if request.method == "POST": try: port.delete() @@ -435,7 +435,7 @@ def del_port(request, port, **_kwargs): @login_required @can_create(Stack) def new_stack(request): - """Ajoute un nouveau stack : stackid_min, max, et nombre de switches""" + """View used to create stacks.""" stack = StackForm(request.POST or None) if stack.is_valid(): stack.save() @@ -449,7 +449,7 @@ def new_stack(request): @login_required @can_edit(Stack) def edit_stack(request, stack, **_kwargs): - """Edition d'un stack (nombre de switches, nom...)""" + """View used to edit stacks.""" stack = StackForm(request.POST or None, instance=stack) if stack.is_valid(): if stack.changed_data: @@ -464,7 +464,7 @@ def edit_stack(request, stack, **_kwargs): @login_required @can_delete(Stack) def del_stack(request, stack, **_kwargs): - """Supprime un stack""" + """View used to delete stacks.""" if request.method == "POST": try: stack.delete() @@ -487,8 +487,7 @@ def del_stack(request, stack, **_kwargs): @login_required @can_edit(Stack) def edit_switchs_stack(request, stack, **_kwargs): - """Permet d'éditer la liste des switches dans une stack et l'ajouter""" - + """View used to edit the list of switches of the given stack.""" if request.method == "POST": pass else: @@ -500,9 +499,12 @@ def edit_switchs_stack(request, stack, **_kwargs): @login_required @can_create(Switch) def new_switch(request): - """ Creation d'un switch. Cree en meme temps l'interface et la machine - associée. Vue complexe. Appelle successivement les 4 models forms - adaptés : machine, interface, domain et switch""" + """View used to create switches. + + At the same time, it creates the related interface and machine. The view + successively calls the 4 appropriate forms: machine, interface, domain and + switch. + """ switch = NewSwitchForm(request.POST or None, user=request.user) interface = AddInterfaceForm(request.POST or None, user=request.user) domain = DomainForm(request.POST or None, user=request.user) @@ -549,7 +551,7 @@ def new_switch(request): @login_required @can_create(Port) def create_ports(request, switchid): - """ Création d'une liste de ports pour un switch.""" + """View used to create port lists for the given switch.""" try: switch = Switch.objects.get(pk=switchid) except Switch.DoesNotExist: @@ -578,9 +580,11 @@ def create_ports(request, switchid): @login_required @can_edit(Switch) def edit_switch(request, switch, switchid): - """ Edition d'un switch. Permet de chambre nombre de ports, - place dans le stack, interface et machine associée""" + """View used to edit switches. + It enables to change the number of ports, location in the stack, or the + related interface and machine. + """ switch_form = EditSwitchForm( request.POST or None, instance=switch, user=request.user ) @@ -622,9 +626,11 @@ def edit_switch(request, switch, switchid): @login_required @can_create(AccessPoint) def new_ap(request): - """ Creation d'une ap. Cree en meme temps l'interface et la machine - associée. Vue complexe. Appelle successivement les 3 models forms - adaptés : machine, interface, domain et switch""" + """View used to create APs. + + At the same time, it creates the related interface and machine. The view + successively calls the 3 appropriate forms: machine, interface, domain. + """ ap = AddAccessPointForm(request.POST or None, user=request.user) interface = AddInterfaceForm(request.POST or None, user=request.user) domain = DomainForm(request.POST or None, user=request.user) @@ -671,8 +677,7 @@ def new_ap(request): @login_required @can_edit(AccessPoint) def edit_ap(request, ap, **_kwargs): - """ Edition d'un switch. Permet de chambre nombre de ports, - place dans le stack, interface et machine associée""" + """View used to edit APs.""" interface_form = EditInterfaceForm( request.POST or None, user=request.user, instance=ap.interface_set.first() ) @@ -723,7 +728,7 @@ def edit_ap(request, ap, **_kwargs): @login_required @can_create(Room) def new_room(request): - """Nouvelle chambre """ + """View used to create rooms.""" room = EditRoomForm(request.POST or None) if room.is_valid(): room.save() @@ -737,7 +742,7 @@ def new_room(request): @login_required @can_edit(Room) def edit_room(request, room, **_kwargs): - """ Edition numero et details de la chambre""" + """View used to edit rooms.""" room = EditRoomForm(request.POST or None, instance=room) if room.is_valid(): if room.changed_data: @@ -752,7 +757,7 @@ def edit_room(request, room, **_kwargs): @login_required @can_delete(Room) def del_room(request, room, **_kwargs): - """ Suppression d'un chambre""" + """View used to delete rooms.""" if request.method == "POST": try: room.delete() @@ -777,7 +782,7 @@ def del_room(request, room, **_kwargs): @login_required @can_create(ModelSwitch) def new_model_switch(request): - """Nouveau modèle de switch""" + """View used to create switch models.""" model_switch = EditModelSwitchForm(request.POST or None) if model_switch.is_valid(): model_switch.save() @@ -793,8 +798,7 @@ def new_model_switch(request): @login_required @can_edit(ModelSwitch) def edit_model_switch(request, model_switch, **_kwargs): - """ Edition d'un modèle de switch""" - + """View used to edit switch models.""" model_switch = EditModelSwitchForm(request.POST or None, instance=model_switch) if model_switch.is_valid(): if model_switch.changed_data: @@ -811,7 +815,7 @@ def edit_model_switch(request, model_switch, **_kwargs): @login_required @can_delete(ModelSwitch) def del_model_switch(request, model_switch, **_kwargs): - """ Suppression d'un modèle de switch""" + """View used to delete switch models.""" if request.method == "POST": try: model_switch.delete() @@ -838,7 +842,7 @@ def del_model_switch(request, model_switch, **_kwargs): @login_required @can_create(SwitchBay) def new_switch_bay(request): - """Nouvelle baie de switch""" + """View used to create switch bays.""" switch_bay = EditSwitchBayForm(request.POST or None) if switch_bay.is_valid(): switch_bay.save() @@ -854,7 +858,7 @@ def new_switch_bay(request): @login_required @can_edit(SwitchBay) def edit_switch_bay(request, switch_bay, **_kwargs): - """ Edition d'une baie de switch""" + """View used to edit switch bays.""" switch_bay = EditSwitchBayForm(request.POST or None, instance=switch_bay) if switch_bay.is_valid(): if switch_bay.changed_data: @@ -871,7 +875,7 @@ def edit_switch_bay(request, switch_bay, **_kwargs): @login_required @can_delete(SwitchBay) def del_switch_bay(request, switch_bay, **_kwargs): - """ Suppression d'une baie de switch""" + """View used to delete switch bays.""" if request.method == "POST": try: switch_bay.delete() @@ -898,8 +902,7 @@ def del_switch_bay(request, switch_bay, **_kwargs): @login_required @can_create(Building) def new_building(request): - """New Building of a dorm - Nouveau batiment""" + """View used to create buildings.""" building = EditBuildingForm(request.POST or None) if building.is_valid(): building.save() @@ -915,8 +918,7 @@ def new_building(request): @login_required @can_edit(Building) def edit_building(request, building, **_kwargs): - """Edit a building - Edition d'un batiment""" + """View used to edit buildings.""" building = EditBuildingForm(request.POST or None, instance=building) if building.is_valid(): if building.changed_data: @@ -931,8 +933,7 @@ def edit_building(request, building, **_kwargs): @login_required @can_delete(Building) def del_building(request, building, **_kwargs): - """Delete a building - Suppression d'un batiment""" + """View used to delete buildings.""" if request.method == "POST": try: building.delete() @@ -959,8 +960,7 @@ def del_building(request, building, **_kwargs): @login_required @can_create(Dormitory) def new_dormitory(request): - """A new dormitory - Nouvelle residence""" + """View used to create dormitories.""" dormitory = EditDormitoryForm(request.POST or None) if dormitory.is_valid(): dormitory.save() @@ -976,8 +976,7 @@ def new_dormitory(request): @login_required @can_edit(Dormitory) def edit_dormitory(request, dormitory, **_kwargs): - """Edit a dormitory - Edition d'une residence""" + """View used to edit dormitories.""" dormitory = EditDormitoryForm(request.POST or None, instance=dormitory) if dormitory.is_valid(): if dormitory.changed_data: @@ -994,8 +993,7 @@ def edit_dormitory(request, dormitory, **_kwargs): @login_required @can_delete(Dormitory) def del_dormitory(request, dormitory, **_kwargs): - """Delete a dormitory - Suppression d'une residence""" + """View used to delete dormitories.""" if request.method == "POST": try: dormitory.delete() @@ -1022,7 +1020,7 @@ def del_dormitory(request, dormitory, **_kwargs): @login_required @can_create(ConstructorSwitch) def new_constructor_switch(request): - """Nouveau constructeur de switch""" + """View used to create switch constructors.""" constructor_switch = EditConstructorSwitchForm(request.POST or None) if constructor_switch.is_valid(): constructor_switch.save() @@ -1038,8 +1036,7 @@ def new_constructor_switch(request): @login_required @can_edit(ConstructorSwitch) def edit_constructor_switch(request, constructor_switch, **_kwargs): - """ Edition d'un constructeur de switch""" - + """View used to edit switch constructors.""" constructor_switch = EditConstructorSwitchForm( request.POST or None, instance=constructor_switch ) @@ -1058,7 +1055,7 @@ def edit_constructor_switch(request, constructor_switch, **_kwargs): @login_required @can_delete(ConstructorSwitch) def del_constructor_switch(request, constructor_switch, **_kwargs): - """ Suppression d'un constructeur de switch""" + """View used to delete switch constructors.""" if request.method == "POST": try: constructor_switch.delete() @@ -1085,7 +1082,7 @@ def del_constructor_switch(request, constructor_switch, **_kwargs): @login_required @can_create(PortProfile) def new_port_profile(request): - """Create a new port profile""" + """View used to create port profiles.""" port_profile = EditPortProfileForm(request.POST or None) if port_profile.is_valid(): port_profile.save() @@ -1101,7 +1098,7 @@ def new_port_profile(request): @login_required @can_edit(PortProfile) def edit_port_profile(request, port_profile, **_kwargs): - """Edit a port profile""" + """View used to edit port profiles.""" port_profile = EditPortProfileForm(request.POST or None, instance=port_profile) if port_profile.is_valid(): if port_profile.changed_data: @@ -1118,7 +1115,7 @@ def edit_port_profile(request, port_profile, **_kwargs): @login_required @can_delete(PortProfile) def del_port_profile(request, port_profile, **_kwargs): - """Delete a port profile""" + """View used to delete port profiles.""" if request.method == "POST": try: port_profile.delete() @@ -1136,7 +1133,7 @@ def del_port_profile(request, port_profile, **_kwargs): @login_required @can_create(ModuleSwitch) def add_module(request): - """ View used to add a Module object """ + """View used to create switch modules.""" module = EditModuleForm(request.POST or None) if module.is_valid(): module.save() @@ -1150,7 +1147,7 @@ def add_module(request): @login_required @can_edit(ModuleSwitch) def edit_module(request, module_instance, **_kwargs): - """ View used to edit a Module object """ + """View used to edit switch modules.""" module = EditModuleForm(request.POST or None, instance=module_instance) if module.is_valid(): if module.changed_data: @@ -1165,7 +1162,7 @@ def edit_module(request, module_instance, **_kwargs): @login_required @can_delete(ModuleSwitch) def del_module(request, module, **_kwargs): - """Compleete delete a module""" + """View used to delete switch modules.""" if request.method == "POST": try: module.delete() @@ -1190,7 +1187,7 @@ def del_module(request, module, **_kwargs): @login_required @can_create(ModuleOnSwitch) def add_module_on(request): - """Add a module to a switch""" + """View used to add a module to a switch.""" module_switch = EditSwitchModuleForm(request.POST or None) if module_switch.is_valid(): module_switch.save() @@ -1206,7 +1203,7 @@ def add_module_on(request): @login_required @can_edit(ModuleOnSwitch) def edit_module_on(request, module_instance, **_kwargs): - """ View used to edit a Module object """ + """View used to edit a module on a switch.""" module = EditSwitchModuleForm(request.POST or None, instance=module_instance) if module.is_valid(): if module.changed_data: @@ -1221,7 +1218,7 @@ def edit_module_on(request, module_instance, **_kwargs): @login_required @can_delete(ModuleOnSwitch) def del_module_on(request, module, **_kwargs): - """Compleete delete a module""" + """View used to delete a module on a switch.""" if request.method == "POST": try: module.delete() @@ -1244,9 +1241,7 @@ def del_module_on(request, module, **_kwargs): def make_machine_graph(): - """ - Create the graph of switchs, machines and access points. - """ + """Create the graph of switches, machines and access points.""" dico = { "subs": [], "links": [], @@ -1284,7 +1279,7 @@ def make_machine_graph(): "machines": [], } ) - # Visit all switchs in this building + # Visit all switches in this building for switch in ( Switch.objects.filter(switchbay__building=building) .prefetch_related( @@ -1311,7 +1306,7 @@ def make_machine_graph(): "ports": [], } ) - # visit all ports of this switch and add the switchs linked to it + # visit all ports of this switch and add the switches linked to it for port in switch.ports.filter(related__isnull=False).select_related( "related__switch" ): @@ -1365,29 +1360,29 @@ def make_machine_graph(): links, new_detected = recursive_switchs(missing[0], None, [missing[0]]) for link in links: dico["links"].append(link) - # Update the lists of missings and already detected switchs + # Update the lists of missings and already detected switches missing = [i for i in missing if i not in new_detected] detected += new_detected - # If the switch have no ports, don't explore it and hop to the next one + # If the switch has no ports, don't explore it and hop to the next one else: del missing[0] - # Switchs that are not connected or not in a building + # Switches that are not connected or not in a building for switch in Switch.objects.filter(switchbay__isnull=True).exclude( ports__related__isnull=False ): dico["alone"].append({"id": switch.id, "name": switch.get_name}) - # generate the dot file + # Generate the dot file dot_data = generate_dot(dico, "topologie/graph_switch.dot") # Create a temporary file to store the dot data f = tempfile.NamedTemporaryFile(mode="w+", encoding="utf-8", delete=False) with f: f.write(dot_data) - unflatten = Popen( # unflatten the graph to make it look better + unflatten = Popen( # Unflatten the graph to make it look better ["unflatten", "-l", "3", f.name], stdout=PIPE ) - Popen( # pipe the result of the first command into the second + Popen( # Pipe the result of the first command into the second ["dot", "-Tpng", "-o", MEDIA_ROOT + "/images/switchs.png"], stdin=unflatten.stdout, stdout=PIPE, @@ -1395,10 +1390,15 @@ def make_machine_graph(): def generate_dot(data, template): - """create the dot file - :param data: dictionary passed to the template - :param template: path to the dot template - :return: all the lines of the dot file""" + """Generate a dot file from the data and template given. + + Args: + data: dictionary passed to the template. + template: path to the dot template. + + Returns: + All the lines of the dot file. + """ t = loader.get_template(template) if not isinstance(t, Template) and not ( hasattr(t, "template") and isinstance(t.template, Template) @@ -1415,18 +1415,19 @@ def generate_dot(data, template): def recursive_switchs(switch_start, switch_before, detected): - """Visit the switch and travel to the switchs linked to it. - :param switch_start: the switch to begin the visit on - :param switch_before: the switch that you come from. - None if switch_start is the first one - :param detected: list of all switchs already visited. - None if switch_start is the first one - :return: A list of all the links found and a list of - all the switchs visited + """Visit the switch and travel to the switches linked to it. + + Args: + switch_start: the switch to begin the visit on. + switch_before: the switch that you come from. None if switch_start is the first one. + detected: list of all switches already visited. None if switch_start is the first one. + + Returns: + A list of all the links found and a list of all the switches visited. """ detected.append(switch_start) - links_return = [] # list of dictionaries of the links to be detected - # create links to every switchs below + links_return = [] # List of dictionaries of the links to be detected + # Create links to every switches below for port in switch_start.ports.filter(related__isnull=False): # Not the switch that we come from, not the current switch if ( @@ -1440,11 +1441,11 @@ def recursive_switchs(switch_start, switch_before, detected): } links_return.append(links) # Add current and below levels links - # go down on every related switchs + # Go down on every related switches for port in switch_start.ports.filter(related__isnull=False): # The switch at the end of this link has not been visited if port.related.switch not in detected: - # explore it and get the results + # Explore it and get the results links_down, detected = recursive_switchs( port.related.switch, switch_start, detected ) diff --git a/users/admin.py b/users/admin.py index 400cb1cd..bc4d9e61 100644 --- a/users/admin.py +++ b/users/admin.py @@ -21,8 +21,10 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Definition des vues pour les admin. Classique, sauf pour users, -où on fait appel à UserChange et ServiceUserChange, forms custom +Admin views basic definition, include basic definition of admin view. + +Except for Admin edition and creation of users and services users; +with AdherentAdmin, ClubAdmin and ServiceUserAdmin. """ from __future__ import unicode_literals @@ -56,15 +58,26 @@ from .forms import ( class LdapUserAdmin(admin.ModelAdmin): - """Administration du ldapuser""" + """LdapUser Admin view. Can't change password, manage + by User General model. + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ list_display = ("name", "uidNumber", "login_shell") exclude = ("user_password", "sambat_nt_password") search_fields = ("name",) class LdapServiceUserAdmin(admin.ModelAdmin): - """Administration du ldapserviceuser""" + """LdapServiceUser Admin view. Can't change password, manage + by User General model. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ list_display = ("name",) exclude = ("user_password",) @@ -72,63 +85,123 @@ class LdapServiceUserAdmin(admin.ModelAdmin): class LdapUserGroupAdmin(admin.ModelAdmin): - """Administration du ldapusergroupe""" + """LdapUserGroup Admin view. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ list_display = ("name", "members", "gid") search_fields = ("name",) class LdapServiceUserGroupAdmin(admin.ModelAdmin): - """Administration du ldap serviceusergroup""" + """LdapServiceUserGroup Admin view. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ list_display = ("name",) search_fields = ("name",) class SchoolAdmin(VersionAdmin): - """Administration, gestion des écoles""" + """School Admin view and management. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ pass class ListRightAdmin(VersionAdmin): - """Gestion de la liste des droits existants - Ne permet pas l'edition du gid (primarykey pour ldap)""" + """ListRight and groups Admin view and management. + Even if it is possible, gid should NOT be changed + as it is the ldap primary key. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ list_display = ("unix_name",) class ListShellAdmin(VersionAdmin): - """Gestion de la liste des shells coté admin""" + """Users Shell Admin view and management. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ pass class RequestAdmin(admin.ModelAdmin): - """Gestion des request objet, ticket pour lien de reinit mot de passe""" + """User Request Admin view and management, for + change password and email validation. + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ list_display = ("user", "type", "created_at", "expires_at") class BanAdmin(VersionAdmin): - """Gestion des bannissements""" + """Ban Admin view and management, for + User Ban + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ pass class EMailAddressAdmin(VersionAdmin): - """Gestion des alias mail""" + """EmailAddress Admin view and management, for + auxiliary and local email addresses + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ pass class WhitelistAdmin(VersionAdmin): - """Gestion des whitelist""" + """Whitelist Admin view and management, for + free access whitelisted users + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ pass class AdherentAdmin(VersionAdmin, BaseUserAdmin): + """Adherent Admin view and management, for + Adherent fields : password, pseudo, etc, admin can + edit all fields on user instance. + Inherit from django BaseUserAdmin + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ + # The forms to add and change user instances add_form = UserAdminForm @@ -179,6 +252,15 @@ class AdherentAdmin(VersionAdmin, BaseUserAdmin): class ClubAdmin(VersionAdmin, BaseUserAdmin): + """Club Admin view and management, for + Club fields : password, pseudo, etc, admin can + edit all fields on user instance. + Inherit from django BaseUserAdmin + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ # The forms to add and change user instances add_form = UserAdminForm form = UserAdminForm @@ -225,8 +307,15 @@ class ClubAdmin(VersionAdmin, BaseUserAdmin): class ServiceUserAdmin(VersionAdmin, BaseUserAdmin): - """Gestion d'un service user admin : champs personnels, - mot de passe; etc""" + """ServiceUser Admin view and management, for + User fields : password, pseudo, etc, admin can + edit all fields on user instance. + Inherit from django BaseUserAdmin + + Parameters: + Django ModelAdmin: Apply on django ModelAdmin + + """ # The forms to add and change user instances form = ServiceUserAdminForm diff --git a/users/forms.py b/users/forms.py index f057373d..72475267 100644 --- a/users/forms.py +++ b/users/forms.py @@ -23,14 +23,20 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Definition des forms pour l'application users. +Forms for the 'users' app of re2o. It highly depends on +:users:models and is mainly used by :users:views. -Modification, creation de : - - un user (informations personnelles) - - un bannissement - - le mot de passe d'un user - - une whiteliste - - un user de service +The following forms are mainly used to create, edit or delete +anything related to 'users' : + * Adherent (personnal data) + * Club + * Ban + * ServiceUser + * Whitelists + * ... + +See the details for each of these operations in the documentation +of each of the method. """ from __future__ import unicode_literals @@ -72,62 +78,16 @@ from .models import ( ) -class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): - """Formulaire de changement de mot de passe. Verifie que les 2 - nouveaux mots de passe renseignés sont identiques et respectent - une norme""" - - selfpasswd = forms.CharField( - label=_("Current password"), max_length=255, widget=forms.PasswordInput - ) - passwd1 = forms.CharField( - label=_("New password"), - max_length=255, - widget=forms.PasswordInput, - help_text=password_validators_help_text_html() - ) - passwd2 = forms.CharField( - label=_("New password confirmation"), - max_length=255, - widget=forms.PasswordInput, - ) - - class Meta: - model = User - fields = [] - - def clean_passwd2(self): - """Verifie que passwd1 et 2 sont identiques""" - # Check that the two password entries match - password1 = self.cleaned_data.get("passwd1") - password2 = self.cleaned_data.get("passwd2") - if password1 and password2 and password1 != password2: - raise forms.ValidationError(_("The new passwords don't match.")) - validate_password(password1, user=self.instance) - return password2 - - def clean_selfpasswd(self): - """Verifie si il y a lieu que le mdp self est correct""" - if not self.instance.check_password(self.cleaned_data.get("selfpasswd")): - raise forms.ValidationError(_("The current password is incorrect.")) - return - - def save(self, commit=True): - """Changement du mot de passe""" - user = super(PassForm, self).save(commit=False) - user.set_password(self.cleaned_data.get("passwd1")) - user.state = User.STATE_NOT_YET_ACTIVE - user.set_active() - user.save() +#### Django Admin Custom Views class UserAdminForm(FormRevMixin, forms.ModelForm): """A form for creating new and editing users. Includes all the required fields, plus a repeated password. - Formulaire pour la création d'un user. N'est utilisé que pour - l'admin, lors de la creation d'un user par admin. Inclu tous les - champs obligatoires""" + Parameters: + DjangoForm : Inherit from basic django form + """ password1 = forms.CharField( label=_("Password"), @@ -152,8 +112,14 @@ class UserAdminForm(FormRevMixin, forms.ModelForm): fields = ("pseudo", "surname", "name", "email", "is_superuser") def clean_password2(self): - """Verifie que password1 et 2 sont identiques""" - # Check that the two password entries match + """Clean password 2, check if passwd1 and 2 values match. + + Parameters: + self : Apply on a django Form UserCreationForm instance + + Returns: + password2 (string): The password2 value if all tests returned True + """ password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2: @@ -163,6 +129,13 @@ class UserAdminForm(FormRevMixin, forms.ModelForm): return password2 def save(self, commit=True): + """Save function. Call standard "set_password" django function, + from provided value for new password, for making hash. + + Parameters: + self : Apply on a django Form UserCreationForm instance + commit : If False, don't make the real save in database + """ # Save the provided password in hashed format user = super(UserAdminForm, self).save(commit=False) if self.cleaned_data["password1"]: @@ -172,11 +145,12 @@ class UserAdminForm(FormRevMixin, forms.ModelForm): class ServiceUserAdminForm(FormRevMixin, forms.ModelForm): - """A form for creating new users. Includes all the required - fields, plus a repeated password. + """A form for creating new service users. Includes all the required + fields, plus a repeated password. For Admin view purpose only. - Formulaire pour la creation de nouveaux serviceusers. - Requiert seulement un mot de passe; et un pseudo""" + Parameters: + DjangoForm : Inherit from basic django form + """ password1 = forms.CharField( label=_("Password"), @@ -198,8 +172,14 @@ class ServiceUserAdminForm(FormRevMixin, forms.ModelForm): fields = ("pseudo",) def clean_password2(self): - """Verifie que password1 et 2 sont identiques""" - # Check that the two password entries match + """Clean password 2, check if passwd1 and 2 values match. + + Parameters: + self : Apply on a django Form UserCreationForm instance + + Returns: + password2 (string): The password2 value if all tests returned True + """ password1 = self.cleaned_data.get("password1") password2 = self.cleaned_data.get("password2") if password1 and password2 and password1 != password2: @@ -207,25 +187,111 @@ class ServiceUserAdminForm(FormRevMixin, forms.ModelForm): return password2 def save(self, commit=True): - # Save the provided password in hashed format + """Save function. Call standard "set_password" django function, + from provided value for new password, for making hash. + + Parameters: + self : Apply on a django Form ServiceUserAdminForm instance + commit : If False, don't make the real save in database + """ user = super(ServiceUserAdminForm, self).save(commit=False) user.set_password(self.cleaned_data["password1"]) user.save() return user +### Classic Django View + + +class PassForm(FormRevMixin, FieldPermissionFormMixin, forms.ModelForm): + """Django form for changing password, check if 2 passwords are the same, + and validate password for django base password validators provided in + settings_local. + + Parameters: + DjangoForm : Inherit from basic django form + + """ + selfpasswd = forms.CharField( + label=_("Current password"), max_length=255, widget=forms.PasswordInput + ) + passwd1 = forms.CharField( + label=_("New password"), + max_length=255, + widget=forms.PasswordInput, + help_text=password_validators_help_text_html() + ) + passwd2 = forms.CharField( + label=_("New password confirmation"), + max_length=255, + widget=forms.PasswordInput, + ) + + class Meta: + model = User + fields = [] + + def clean_passwd2(self): + """Clean password 2, check if passwd1 and 2 values match, and + apply django validator with validate_password function. + + Parameters: + self : Apply on a django Form PassForm instance + + Returns: + password2 (string): The password2 value if all tests returned True + """ + password1 = self.cleaned_data.get("passwd1") + password2 = self.cleaned_data.get("passwd2") + if password1 and password2 and password1 != password2: + raise forms.ValidationError(_("The new passwords don't match.")) + validate_password(password1, user=self.instance) + return password2 + + def clean_selfpasswd(self): + """Clean selfpassword, check if provided original user password match + with the stored value. + + Parameters: + self : Apply on a django Form PassForm instance + """ + if not self.instance.check_password(self.cleaned_data.get("selfpasswd")): + raise forms.ValidationError(_("The current password is incorrect.")) + return + + def save(self, commit=True): + """Save function. Call standard "set_password" django function, + and call set_active for set user in active state if needed. + + Parameters: + self : Apply on a django Form PassForm instance + commit : If False, don't make the real save in database + """ + user = super(PassForm, self).save(commit=False) + user.set_password(self.cleaned_data.get("passwd1")) + user.state = User.STATE_NOT_YET_ACTIVE + user.set_active() + user.save() + + class ResetPasswordForm(forms.Form): - """Formulaire de demande de reinitialisation de mot de passe, - mdp oublié""" + """A form for asking to reset password. + + Parameters: + DjangoForm : Inherit from basic django form + """ pseudo = forms.CharField(label=_("Username"), max_length=255) email = forms.EmailField(max_length=255) class MassArchiveForm(forms.Form): - """Formulaire d'archivage des users inactif. Prend en argument - du formulaire la date de depart avant laquelle archiver les - users""" + """A form for archiving a lot de users. Get a start date + for start archiving. + + Parameters: + DjangoForm : Inherit from basic django form + """ date = forms.DateTimeField(help_text="%d/%m/%y") full_archive = forms.BooleanField( @@ -251,9 +317,12 @@ class MassArchiveForm(forms.Form): class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Formulaire de base d'edition d'un user. Formulaire de base, utilisé - pour l'edition de self par self ou un cableur. On formate les champs - avec des label plus jolis""" + """Adherent Edition Form, base form used for editing user by himself + or another user. Labels are provided for help purposes. + + Parameters: + DjangoForm : Inherit from basic django form + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -288,24 +357,42 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): ) def clean_telephone(self): - """Verifie que le tel est présent si 'option est validée - dans preferences""" + """Clean telephone, check if telephone is made mandatory, and + raise error if not provided + + Parameters: + self : Apply on a django Form AdherentForm instance + + Returns: + telephone (string): The telephone string if clean is True + """ telephone = self.cleaned_data["telephone"] if not telephone and OptionalUser.get_cached_value("is_tel_mandatory"): raise forms.ValidationError(_("A valid telephone number is required.")) return telephone def clean_force(self): - """On supprime l'ancien user de la chambre si et seulement si la - case est cochée""" + """Clean force, remove previous user from room if needed. + + Parameters: + self : Apply on a django Form AdherentForm instance + """ room = self.cleaned_data.get("room") if self.cleaned_data.get("force", False) and room: remove_user_room(room) return def clean_room(self): - """On supprime l'ancien user de la chambre si l'option est activée, - et que l'ancien user a une connexion désactivée""" + """Clean room, based on room policy provided by preferences. + If needed, call remove_user_room to make the room empty before + saving self.instance into that room. + + Parameters: + self : Apply on a django Form AdherentForm instance + + Returns: + room (string): The room instance + """ # Handle case where regular users can force move room = self.cleaned_data.get("room") room_policy = OptionalUser.get_cached_value("self_room_policy") @@ -320,10 +407,13 @@ class AdherentForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class AdherentCreationForm(AdherentForm): - """Formulaire de création d'un user. - AdherentForm auquel on ajoute une checkbox afin d'éviter les - doublons d'utilisateurs et, optionnellement, - un champ mot de passe""" + """AdherentCreationForm. Inherit from AdherentForm, base form used for creating + user by himself or another user. Labels are provided for help purposes. + Add some instructions, and validation for initial creation. + + Parameters: + DjangoForm : Inherit from basic django form + """ # Champ pour choisir si un lien est envoyé par mail pour le mot de passe init_password_by_mail_info = _( "If this options is set, you will receive a link to set" @@ -407,7 +497,15 @@ class AdherentCreationForm(AdherentForm): self.fields.pop("password2") def clean_password2(self): - """Verifie que password1 et 2 sont identiques (si nécessaire)""" + """Clean password 2, check if passwd1 and 2 values match, and + apply django validator with validate_password function. + + Parameters: + self : Apply on a django Form AdherentCreationForm instance + + Returns: + password2 (string): The password2 value if all tests returned True + """ send_email = self.cleaned_data.get("init_password_by_mail") if send_email: return None @@ -421,9 +519,14 @@ class AdherentCreationForm(AdherentForm): return password2 def save(self, commit=True): - """Set the user's password, if entered - Returns the user and a bool indicating whether - an email to init the password should be sent""" + """Save function. If password has been set during creation, + call standard "set_password" django function from provided value + for new password, for making hash. + + Parameters: + self : Apply on a django Form AdherentCreationForm instance + commit : If False, don't make the real save in database + """ # Save the provided password in hashed format user = super(AdherentForm, self).save(commit=False) @@ -437,8 +540,13 @@ class AdherentCreationForm(AdherentForm): class AdherentEditForm(AdherentForm): - """Formulaire d'édition d'un user. - AdherentForm incluant la modification des champs gpg et shell""" + """AdherentEditForm. Inherit from AdherentForm, base form used for editing + user by himself or another user. Labels are provided for help purposes. + Add some instructions, and validation, fields depends on editing user rights. + + Parameters: + DjangoForm : Inherit from basic django form + """ def __init__(self, *args, **kwargs): super(AdherentEditForm, self).__init__(*args, **kwargs) @@ -469,9 +577,13 @@ class AdherentEditForm(AdherentForm): class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Formulaire de base d'edition d'un user. Formulaire de base, utilisé - pour l'edition de self par self ou un cableur. On formate les champs - avec des label plus jolis""" + """ClubForm. For editing club by himself or another user. Labels are provided for + help purposes. Add some instructions, and validation, fields depends + on editing user rights. + + Parameters: + DjangoForm : Inherit from basic django form + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -503,8 +615,15 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): ] def clean_telephone(self): - """Verifie que le tel est présent si 'option est validée - dans preferences""" + """Clean telephone, check if telephone is made mandatory, and + raise error if not provided + + Parameters: + self : Apply on a django Form ClubForm instance + + Returns: + telephone (string): The telephone string if clean is True + """ telephone = self.cleaned_data["telephone"] if not telephone and OptionalUser.get_cached_value("is_tel_mandatory"): raise forms.ValidationError(_("A valid telephone number is required.")) @@ -512,8 +631,12 @@ class ClubForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class ClubAdminandMembersForm(FormRevMixin, ModelForm): - """Permet d'éditer la liste des membres et des administrateurs - d'un club""" + """ClubAdminandMembersForm. Only For editing administrators of a club by himself + or another user. + + Parameters: + DjangoForm : Inherit from basic django form + """ class Meta: model = Club @@ -525,8 +648,11 @@ class ClubAdminandMembersForm(FormRevMixin, ModelForm): class PasswordForm(FormRevMixin, ModelForm): - """ Formulaire de changement brut de mot de passe. - Ne pas utiliser sans traitement""" + """PasswordForm. Do not use directly in views without extra validations. + + Parameters: + DjangoForm : Inherit from basic django form + """ class Meta: model = User @@ -538,8 +664,12 @@ class PasswordForm(FormRevMixin, ModelForm): class ServiceUserForm(FormRevMixin, ModelForm): - """Service user creation - force initial password set""" + """ServiceUserForm, used for creating a service user, require + a password and set it. + + Parameters: + DjangoForm : Inherit from basic django form + """ password = forms.CharField( label=_("New password"), @@ -558,7 +688,14 @@ class ServiceUserForm(FormRevMixin, ModelForm): super(ServiceUserForm, self).__init__(*args, prefix=prefix, **kwargs) def save(self, commit=True): - """Password change""" + """Save function. If password has been changed and provided, + call standard "set_password" django function from provided value + for new password, for making hash. + + Parameters: + self : Apply on a django Form ServiceUserForm instance + commit : If False, don't make the real save in database + """ user = super(ServiceUserForm, self).save(commit=False) if self.cleaned_data["password"]: user.set_password(self.cleaned_data.get("password")) @@ -566,8 +703,12 @@ class ServiceUserForm(FormRevMixin, ModelForm): class EditServiceUserForm(ServiceUserForm): - """Formulaire d'edition de base d'un service user. Ne permet - d'editer que son group d'acl et son commentaire""" + """EditServiceUserForm, used for editing a service user, can + edit password, access_group and comment. + + Parameters: + DjangoForm : Inherit from basic django form + """ password = forms.CharField( label=_("New password"), @@ -582,7 +723,12 @@ class EditServiceUserForm(ServiceUserForm): class StateForm(FormRevMixin, ModelForm): - """Change state of an user, and if its main email is verified or not""" + """StateForm, Change state of an user, and if + its main email is verified or not + + Parameters: + DjangoForm : Inherit from basic django form + """ class Meta: model = User @@ -596,7 +742,11 @@ class StateForm(FormRevMixin, ModelForm): class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): - """ Gestion des groupes d'un user""" + """GroupForm, form used for editing user groups. + + Parameters: + DjangoForm : Inherit from basic django form + """ groups = forms.ModelMultipleChoiceField( Group.objects.all(), widget=forms.CheckboxSelectMultiple, required=False @@ -614,7 +764,11 @@ class GroupForm(FieldPermissionFormMixin, FormRevMixin, ModelForm): class SchoolForm(FormRevMixin, ModelForm): - """Edition, creation d'un école""" + """SchoolForm, form used for creating or editing school. + + Parameters: + DjangoForm : Inherit from basic django form + """ class Meta: model = School @@ -627,7 +781,11 @@ class SchoolForm(FormRevMixin, ModelForm): class ShellForm(FormRevMixin, ModelForm): - """Edition, creation d'un école""" + """ShellForm, form used for creating or editing shell. + + Parameters: + DjangoForm : Inherit from basic django form + """ class Meta: model = ListShell @@ -640,8 +798,13 @@ class ShellForm(FormRevMixin, ModelForm): class ListRightForm(FormRevMixin, ModelForm): - """Edition, d'un groupe , équivalent à un droit - Ne permet pas d'editer le gid, car il sert de primary key""" + """ListRightForm, form used for editing a listright, + related with django group object. Gid, primary key, can't + be edited. + + Parameters: + DjangoForm : Inherit from basic django form + """ permissions = forms.ModelMultipleChoiceField( Permission.objects.all().select_related("content_type"), @@ -660,7 +823,12 @@ class ListRightForm(FormRevMixin, ModelForm): class NewListRightForm(ListRightForm): - """Ajout d'un groupe/list de droit """ + """ListRightForm, form used for creating a listright, + related with django group object. + + Parameters: + DjangoForm : Inherit from basic django form + """ class Meta(ListRightForm.Meta): fields = ("name", "unix_name", "gid", "critical", "permissions", "details") @@ -673,7 +841,12 @@ class NewListRightForm(ListRightForm): class DelListRightForm(Form): - """Suppression d'un ou plusieurs groupes""" + """DelListRightForm, form for deleting one or several ListRight + instances. + + Parameters: + DjangoForm : Inherit from basic django form + """ listrights = forms.ModelMultipleChoiceField( queryset=ListRight.objects.none(), @@ -691,7 +864,12 @@ class DelListRightForm(Form): class DelSchoolForm(Form): - """Suppression d'une ou plusieurs écoles""" + """DelSchoolForm, form for deleting one or several School + instances. + + Parameters: + DjangoForm : Inherit from basic django form + """ schools = forms.ModelMultipleChoiceField( queryset=School.objects.none(), @@ -709,7 +887,11 @@ class DelSchoolForm(Form): class BanForm(FormRevMixin, ModelForm): - """Creation, edition d'un objet bannissement""" + """BanForm, form used for creating or editing a ban instance. + + Parameters: + DjangoForm : Inherit from basic django form + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -724,7 +906,11 @@ class BanForm(FormRevMixin, ModelForm): class WhitelistForm(FormRevMixin, ModelForm): - """Creation, edition d'un objet whitelist""" + """WhitelistForm, form used for creating or editing a whitelist instance. + + Parameters: + DjangoForm : Inherit from basic django form + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -739,7 +925,12 @@ class WhitelistForm(FormRevMixin, ModelForm): class EMailAddressForm(FormRevMixin, ModelForm): - """Create and edit a local email address""" + """EMailAddressForm, form used for creating or editing a local + email for a user. + + Parameters: + DjangoForm : Inherit from basic django form + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -756,7 +947,11 @@ class EMailAddressForm(FormRevMixin, ModelForm): class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): - """Edit email-related settings""" + """EMailSettingsForm, form used for editing email settings for a user. + + Parameters: + DjangoForm : Inherit from basic django form + """ def __init__(self, *args, **kwargs): prefix = kwargs.pop("prefix", self.Meta.model.__name__) @@ -775,6 +970,12 @@ class EmailSettingsForm(FormRevMixin, FieldPermissionFormMixin, ModelForm): class InitialRegisterForm(forms.Form): + """InitialRegisterForm, form used for auto-register of room and mac-address + with captive-portal. + + Parameters: + DjangoForm : Inherit from basic django form + """ register_room = forms.BooleanField(required=False) register_machine = forms.BooleanField(required=False) @@ -818,6 +1019,13 @@ class InitialRegisterForm(forms.Form): self.fields.pop("register_machine") def clean_register_room(self): + """Clean room, call remove_user_room to make the room empty before + saving self.instance into that room. + + Parameters: + self : Apply on a django Form InitialRegisterForm instance + + """ if self.cleaned_data["register_room"]: if self.user.is_class_adherent: remove_user_room(self.new_room) @@ -830,6 +1038,12 @@ class InitialRegisterForm(forms.Form): user.save() def clean_register_machine(self): + """Clean register room, autoregister machine from user request mac_address. + + Parameters: + self : Apply on a django Form InitialRegisterForm instance + + """ if self.cleaned_data["register_machine"]: if self.mac_address and self.nas_type: self.user.autoregister_machine(self.mac_address, self.nas_type) diff --git a/users/models.py b/users/models.py index e4ddb18f..645bfa63 100755 --- a/users/models.py +++ b/users/models.py @@ -23,25 +23,30 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Models de l'application users. +The database models for the 'users' app of re2o. -On défini ici des models django classiques: -- users, qui hérite de l'abstract base user de django. Permet de définit -un utilisateur du site (login, passwd, chambre, adresse, etc) -- les whiteslist -- les bannissements -- les établissements d'enseignement (school) -- les droits (right et listright) -- les utilisateurs de service (pour connexion automatique) +The goal is to keep the main actions here, i.e. the 'clean' and 'save' +function are higly reposnsible for the changes, checking the coherence of the +data and the good behaviour in general for not breaking the database. -On défini aussi des models qui héritent de django-ldapdb : -- ldapuser -- ldapgroup -- ldapserviceuser +For further details on each of those models, see the documentation details for +each. -Ces utilisateurs ldap sont synchronisés à partir des objets -models sql classiques. Seuls certains champs essentiels sont -dupliqués. +Here are defined the following django models : + * Users : Adherent and Club (which inherit from Base User Abstract of django). + * Whitelists + * Bans + * Schools (teaching structures) + * Rights (Groups and ListRight) + * ServiceUser (for ldap connexions) + +Also define django-ldapdb models : + * LdapUser + * LdapGroup + * LdapServiceUser + +These objects are sync from django regular models as auxiliary models from +sql data into ldap. """ @@ -94,18 +99,32 @@ from preferences.models import OptionalMachine, MailMessageOption from PIL import Image from io import BytesIO import sys -# Utilitaires généraux +# General utilities def linux_user_check(login): - """ Validation du pseudo pour respecter les contraintes unix""" + """Check if a login comply with unix base login policy + + Parameters: + login (string): Login to check + + Returns: + boolean: True if login comply with policy + """ UNIX_LOGIN_PATTERN = re.compile("^[a-z][a-z0-9-]*[$]?$") return UNIX_LOGIN_PATTERN.match(login) def linux_user_validator(login): - """ Retourne une erreur de validation si le login ne respecte - pas les contraintes unix (maj, min, chiffres ou tiret)""" + """Check if a login comply with unix base login policy, returns + a standard Django ValidationError if login is not correct + + Parameters: + login (string): Login to check + + Returns: + ValidationError if login comply with policy + """ if not linux_user_check(login): raise forms.ValidationError( _("The username \"%(label)s\" contains forbidden characters."), @@ -114,7 +133,11 @@ def linux_user_validator(login): def get_fresh_user_uid(): - """ Renvoie le plus petit uid non pris. Fonction très paresseuse """ + """Return a fresh unused uid. + + Returns: + uid (int): The fresh uid available + """ uids = list(range(int(min(UID_RANGES["users"])), int(max(UID_RANGES["users"])))) try: used_uids = list(User.objects.values_list("uid_number", flat=True)) @@ -125,7 +148,11 @@ def get_fresh_user_uid(): def get_fresh_gid(): - """ Renvoie le plus petit gid libre """ + """Return a fresh unused gid. + + Returns: + uid (int): The fresh gid available + """ gids = list(range(int(min(GID_RANGES["posix"])), int(max(GID_RANGES["posix"])))) used_gids = list(ListRight.objects.values_list("gid", flat=True)) free_gids = [id for id in gids if id not in used_gids] @@ -174,9 +201,28 @@ class UserManager(BaseUserManager): class User( RevMixin, FieldPermissionModelMixin, AbstractBaseUser, PermissionsMixin, AclMixin ): - """ Definition de l'utilisateur de base. - Champs principaux : name, surnname, pseudo, email, room, password - Herite du django BaseUser et du système d'auth django""" + """Base re2o User model + + Attributes: + surname: surname of the user + pseudo: login of the user + email: The main email of the user + local_email_redirect: Option for redirection of all emails to the main email + local_email_enabled: If True, enable a local email account + school: Optional field, the school of the user + shell: User shell linux + comment: Optionnal comment field + pwd_ntlm: Hash password in ntlm for freeradius + state: State of the user, can be active, not yet active, etc (see below) + email_state: State of the main email (if confirmed or not) + registered: Date of initial creation + telephone: Phone number + uid_number: Linux uid of this user + legacy_uid: Optionnal legacy user id + shortcuts_enabled : Option for js shortcuts + email_change_date: Date of the last email change + profile_image: Optionnal image profile + """ STATE_ACTIVE = 0 STATE_DISABLED = 1 @@ -261,7 +307,7 @@ class User( ) email_change_date = models.DateTimeField(auto_now_add=True) profile_image = models.ImageField(upload_to='profile_image', blank=True) - + USERNAME_FIELD = "pseudo" REQUIRED_FIELDS = ["surname", "email"] @@ -285,9 +331,19 @@ class User( verbose_name = _("user (member or club)") verbose_name_plural = _("users (members or clubs)") + ###### Shortcuts and methods for user instance ###### + @cached_property def name(self): - """Si il s'agit d'un adhérent, on renvoie le prénom""" + """Shortcuts, returns name attribute if the user is linked with + an adherent instance. + + Parameters: + self (user instance): user to return infos + + Returns: + name (string): Name value if available + """ if self.is_class_adherent: return self.adherent.name else: @@ -295,7 +351,15 @@ class User( @cached_property def room(self): - """Alias vers room """ + """Shortcuts, returns room attribute; unique for adherent + and multiple (queryset) for club. + + Parameters: + self (user instance): user to return infos + + Returns: + room (room instance): Room instance + """ if self.is_class_adherent: return self.adherent.room elif self.is_class_club: @@ -305,13 +369,30 @@ class User( @cached_property def get_mail_addresses(self): + """Shortcuts, returns all local email address queryset only if local_email + global option is enabled. + + Parameters: + self (user instance): user to return infos + + Returns: + emailaddresse_set (queryset): All Email address of the local account + """ if self.local_email_enabled: return self.emailaddress_set.all() return None @cached_property def get_mail(self): - """Return the mail address choosen by the user""" + """Shortcuts, returns the email address to use to contact the instance user self. + Depends on if local_email account has been activated, otherwise returns self.email. + + Parameters: + self (user instance): user to return infos + + Returns: + email (string): The correct email to use + """ if ( not OptionalUser.get_cached_value("local_email_accounts_enabled") or not self.local_email_enabled @@ -323,7 +404,15 @@ class User( @cached_property def class_type(self): - """Returns the type of that user; returns database keyname""" + """Shortcuts, returns the class string "Adherent" of "Club", related with the + self instance. + + Parameters: + self (user instance): user to return infos + + Returns: + class (string): The class "Adherent" or "Club" + """ if hasattr(self, "adherent"): return "Adherent" elif hasattr(self, "club"): @@ -333,7 +422,15 @@ class User( @cached_property def class_display(self): - """Returns the typename of that user to display for user interface""" + """Shortcuts, returns the pretty string "Member" of "Club", related with the + self instance. + + Parameters: + self (user instance): user to return infos + + Returns: + class (string): "Member" or "Club" + """ if hasattr(self, "adherent"): return _("Member") elif hasattr(self, "club"): @@ -343,24 +440,68 @@ class User( @cached_property def gid_number(self): - """renvoie le gid par défaut des users""" + """Shortcuts, returns the main and default gid for users, + from settings file + + Parameters: + self (user instance): user to return infos + + Returns: + gid (int): Default gid number + """ return int(LDAP["user_gid"]) + @cached_property + def gid(self): + """Shortcuts, returns the main and default gid for users, + from settings file + + Parameters: + self (user instance): user to return infos + + Returns: + gid (int): Default gid number + """ + return LDAP["user_gid"] + @cached_property def is_class_club(self): - """ Returns True if the object is a Club (subclassing User) """ - # TODO : change to isinstance (cleaner) + """Shortcuts, returns if the instance related with user is + a club. + + Parameters: + self (user instance): user to return infos + + Returns: + boolean : Returns true if this user is a club + """ return hasattr(self, "club") @cached_property def is_class_adherent(self): - """ Returns True if the object is a Adherent (subclassing User) """ - # TODO : change to isinstance (cleaner) + """Shortcuts, returns if the instance related with user is + an adherent. + + Parameters: + self (user instance): user to return infos + + Returns: + boolean : Returns true if this user is an adherent + """ return hasattr(self, "adherent") @property def is_active(self): - """ Renvoie si l'user est à l'état actif""" + """Shortcuts, used by django for allowing connection from this user. + Returns True if this user has state active, or not yet active, + or if preferences allows connection for archived users. + + Parameters: + self (user instance): user to return infos + + Returns: + boolean : Returns true if this user is allow to connect. + """ allow_archived = OptionalUser.get_cached_value("allow_archived_connexion") return ( self.state == self.STATE_ACTIVE @@ -371,34 +512,43 @@ class User( ) ) - def set_active(self): - """Enable this user if he subscribed successfully one time before - Reenable it if it was archived - Do nothing if disabled or waiting for email confirmation""" - if self.state == self.STATE_NOT_YET_ACTIVE: - if self.facture_set.filter(valid=True).filter( - Q(vente__type_cotisation="All") | Q(vente__type_cotisation="Adhesion") - ).exists() or OptionalUser.get_cached_value("all_users_active"): - self.state = self.STATE_ACTIVE - self.save() - if self.state == self.STATE_ARCHIVE or self.state == self.STATE_FULL_ARCHIVE: - self.state = self.STATE_ACTIVE - self.unarchive() - self.save() - @property def is_staff(self): - """ Fonction de base django, renvoie si l'user est admin""" + """Shortcuts, used by django for admin pannel access, shortcuts to + is_admin. + + Parameters: + self (user instance): user to return infos + + Returns: + boolean : Returns true if this user is_staff. + """ return self.is_admin @property def is_admin(self): - """ Renvoie si l'user est admin""" + """Shortcuts, used by django for admin pannel access. Test if user + instance is_superuser or member of admin group. + + Parameters: + self (user instance): user to return infos + + Returns: + boolean : Returns true if this user is allow to access to admin pannel. + """ admin, _ = Group.objects.get_or_create(name="admin") return self.is_superuser or admin in self.groups.all() def get_full_name(self): - """ Renvoie le nom complet de l'user formaté nom/prénom""" + """Shortcuts, returns pretty full name to display both in case of user + is a club or an adherent. + + Parameters: + self (user instance): user to return infos + + Returns: + full_name (string) : Returns full name, name + surname. + """ name = self.name if name: return "%s %s" % (name, self.surname) @@ -406,142 +556,71 @@ class User( return self.surname def get_short_name(self): - """ Renvoie seulement le nom""" - return self.surname + """Shortcuts, returns short name to display both in case of user is + a club or an adherent. - @cached_property - def gid(self): - """return the default gid of user""" - return LDAP["user_gid"] + Parameters: + self (user instance): user to return infos + + Returns: + surname (string) : Returns surname. + """ + return self.surname @property def get_shell(self): - """ A utiliser de préférence, prend le shell par défaut - si il n'est pas défini""" + """Shortcuts, returns linux user shell to use for this user if + provided, otherwise the default shell defined in preferences. + + Parameters: + self (user instance): user to return infos + + Returns: + shell (linux shell) : Returns linux shell. + """ return self.shell or OptionalUser.get_cached_value("shell_default") @cached_property def home_directory(self): + """Shortcuts, returns linux user home directory to use. + + Parameters: + self (user instance): user to return infos + + Returns: + home dir (string) : Returns home directory. + """ return "/home/" + self.pseudo @cached_property def get_shadow_expire(self): - """Return the shadow_expire value for the user""" + """Shortcuts, returns the shadow expire value : 0 if this account is + disabled or if the email has not been verified to block the account + access. + + Parameters: + self (user instance): user to return infos + + Returns: + shadow_expire (int) : Shadow expire value. + """ if self.state == self.STATE_DISABLED or self.email_state == self.EMAIL_STATE_UNVERIFIED: return str(0) else: return None - def end_adhesion(self): - """ Renvoie la date de fin d'adhésion d'un user. Examine les objets - cotisation""" - date_max = ( - Cotisation.objects.filter( - vente__in=Vente.objects.filter( - facture__in=Facture.objects.filter(user=self).exclude(valid=False) - ) - ) - .filter(Q(type_cotisation="All") | Q(type_cotisation="Adhesion")) - .aggregate(models.Max("date_end"))["date_end__max"] - ) - return date_max - - def end_connexion(self): - """ Renvoie la date de fin de connexion d'un user. Examine les objets - cotisation""" - date_max = ( - Cotisation.objects.filter( - vente__in=Vente.objects.filter( - facture__in=Facture.objects.filter(user=self).exclude(valid=False) - ) - ) - .filter(Q(type_cotisation="All") | Q(type_cotisation="Connexion")) - .aggregate(models.Max("date_end"))["date_end__max"] - ) - return date_max - - def is_adherent(self): - """ Renvoie True si l'user est adhérent : si - self.end_adhesion()>now""" - end = self.end_adhesion() - if not end: - return False - elif end < timezone.now(): - return False - else: - return True - - def is_connected(self): - """ Renvoie True si l'user est adhérent : si - self.end_adhesion()>now et end_connexion>now""" - end = self.end_connexion() - if not end: - return False - elif end < timezone.now(): - return False - else: - return self.is_adherent() - - def end_ban(self): - """ Renvoie la date de fin de ban d'un user, False sinon """ - date_max = Ban.objects.filter(user=self).aggregate(models.Max("date_end"))[ - "date_end__max" - ] - return date_max - - def end_whitelist(self): - """ Renvoie la date de fin de whitelist d'un user, False sinon """ - date_max = Whitelist.objects.filter(user=self).aggregate( - models.Max("date_end") - )["date_end__max"] - return date_max - - def is_ban(self): - """ Renvoie si un user est banni ou non """ - end = self.end_ban() - if not end: - return False - elif end < timezone.now(): - return False - else: - return True - - def is_whitelisted(self): - """ Renvoie si un user est whitelisté ou non """ - end = self.end_whitelist() - if not end: - return False - elif end < timezone.now(): - return False - else: - return True - - def has_access(self): - """ Renvoie si un utilisateur a accès à internet """ - return ( - self.state == User.STATE_ACTIVE - and self.email_state != User.EMAIL_STATE_UNVERIFIED - and not self.is_ban() - and (self.is_connected() or self.is_whitelisted()) - ) or self == AssoOption.get_cached_value("utilisateur_asso") - - def end_access(self): - """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)""" - if not self.end_connexion(): - if not self.end_whitelist(): - return None - else: - return self.end_whitelist() - else: - if not self.end_whitelist(): - return self.end_connexion() - else: - return max(self.end_connexion(), self.end_whitelist()) - @cached_property def solde(self): - """ Renvoie le solde d'un user. - Somme les crédits de solde et retire les débit payés par solde""" + """Shortcuts, calculate and returns the balance for this user, as a + dynamic balance beetween debiti (-) and credit (+) "Vente" objects + flaged as balance operations. + + Parameters: + self (user instance): user to return infos + + Returns: + solde (float) : The balance of the user. + """ solde_objects = Paiement.objects.filter(is_balance=True) somme_debit = ( Vente.objects.filter( @@ -573,10 +652,240 @@ class User( ) return somme_credit - somme_debit + @property + def image_url(self): + """Shortcuts, returns the url associated with the user profile_image, + if an image has been set. + + Parameters: + self (user instance): user to return infos + + Returns: + profile_image_url (url) : Returns the url of this profile image. + """ + if self.profile_image and hasattr(self.profile_image, 'url'): + return self.profile_image.url + + @cached_property + def email_address(self): + """Shortcuts, returns all the email addresses (queryset) associated + with the local account, if the account has been activated, + otherwise return a none queryset. + + Parameters: + self (user instance): user to return infos + + Returns: + email_address (django queryset) : Returns a queryset containing + EMailAddress of this user. + """ + if ( + OptionalUser.get_cached_value("local_email_accounts_enabled") + and self.local_email_enabled + ): + return self.emailaddress_set.all() + return EMailAddress.objects.none() + + def end_adhesion(self): + """Methods, calculate and returns the end of membership value date of + this user with aggregation of Cotisation objects linked to user + instance. + + Parameters: + self (user instance): user to return infos + + Returns: + end_adhesion (date) : Date of the end of the membership. + """ + date_max = ( + Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter(user=self).exclude(valid=False) + ) + ) + .filter(Q(type_cotisation="All") | Q(type_cotisation="Adhesion")) + .aggregate(models.Max("date_end"))["date_end__max"] + ) + return date_max + + def end_connexion(self): + """Methods, calculate and returns the end of connection subscription value date + of this user with aggregation of Cotisation objects linked to user instance. + + Parameters: + self (user instance): user to return infos + + Returns: + end_adhesion (date) : Date of the end of the connection subscription. + """ + date_max = ( + Cotisation.objects.filter( + vente__in=Vente.objects.filter( + facture__in=Facture.objects.filter(user=self).exclude(valid=False) + ) + ) + .filter(Q(type_cotisation="All") | Q(type_cotisation="Connexion")) + .aggregate(models.Max("date_end"))["date_end__max"] + ) + return date_max + + def is_adherent(self): + """Methods, calculate and returns if the user has a valid membership by testing + if end_adherent is after now or not. + + Parameters: + self (user instance): user to return infos + + Returns: + is_adherent (boolean) : True is user has a valid membership. + """ + end = self.end_adhesion() + if not end: + return False + elif end < timezone.now(): + return False + else: + return True + + def is_connected(self): + """Methods, calculate and returns if the user has a valid membership AND a + valid connection subscription by testing if end_connexion is after now or not. + If true, returns is_adherent() method value. + + Parameters: + self (user instance): user to return infos + + Returns: + is_connected (boolean) : True is user has a valid membership and a valid connexion. + """ + end = self.end_connexion() + if not end: + return False + elif end < timezone.now(): + return False + else: + return self.is_adherent() + + def end_ban(self): + """Methods, calculate and returns the end of a ban value date + of this user with aggregation of ban objects linked to user instance. + + Parameters: + self (user instance): user to return infos + + Returns: + end_ban (date) : Date of the end of the bans objects. + """ + date_max = Ban.objects.filter(user=self).aggregate(models.Max("date_end"))[ + "date_end__max" + ] + return date_max + + def end_whitelist(self): + """Methods, calculate and returns the end of a whitelist value date + of this user with aggregation of whitelists objects linked to user instance. + + Parameters: + self (user instance): user to return infos + + Returns: + end_whitelist (date) : Date of the end of the whitelists objects. + """ + date_max = Whitelist.objects.filter(user=self).aggregate( + models.Max("date_end") + )["date_end__max"] + return date_max + + def is_ban(self): + """Methods, calculate and returns if the user is banned by testing + if end_ban is after now or not. + + parameters: + self (user instance): user to return infos + + returns: + is_ban (boolean) : true if user is under a ban sanction decision. + """ + end = self.end_ban() + if not end: + return False + elif end < timezone.now(): + return False + else: + return True + + def is_whitelisted(self): + """Methods, calculate and returns if the user has a whitelist free connection + if end_whitelist is after now or not. + + parameters: + self (user instance): user to return infos + + returns: + is_whitelisted (boolean) : true if user has a whitelist connection. + """ + end = self.end_whitelist() + if not end: + return False + elif end < timezone.now(): + return False + else: + return True + + def has_access(self): + """Methods, returns if the user has an internet access. + Return True if user is active and has a verified email, is not under a ban + decision and has a valid membership and connection or a whitelist. + + parameters: + self (user instance): user to return infos + + returns: + has_access (boolean) : true if user has an internet connection. + """ + return ( + self.state == User.STATE_ACTIVE + and self.email_state != User.EMAIL_STATE_UNVERIFIED + and not self.is_ban() + and (self.is_connected() or self.is_whitelisted()) + ) or self == AssoOption.get_cached_value("utilisateur_asso") + + def end_access(self): + """Methods, returns the date of the end of the connection for this user, + as the maximum date beetween connection (membership objects) and whitelists. + + parameters: + self (user instance): user to return infos + + returns: + end_access (datetime) : Returns the date of the end_access connection. + """ + if not self.end_connexion(): + if not self.end_whitelist(): + return None + else: + return self.end_whitelist() + else: + if not self.end_whitelist(): + return self.end_connexion() + else: + return max(self.end_connexion(), self.end_whitelist()) + @classmethod def users_interfaces(cls, users, active=True, all_interfaces=False): - """ Renvoie toutes les interfaces dont les machines appartiennent à - self. Par defaut ne prend que les interfaces actives""" + """Class method, returns all interfaces related/belonging to users + contained in query_sert "users". + + Parameters: + users (list of users queryset): users which interfaces + have to be returned + active (boolean): If true, filter on interfaces + all_interfaces (boolean): If true, returns all interfaces + + returns: + interfaces (queryset): Queryset of interfaces instances + + """ if all_interfaces: return Interface.objects.filter( machine__in=Machine.objects.filter(user__in=users) @@ -587,21 +896,90 @@ class User( ).select_related("domain__extension") def user_interfaces(self, active=True, all_interfaces=False): - """ Renvoie toutes les interfaces dont les machines appartiennent à - self. Par defaut ne prend que les interfaces actives""" + """Method, returns all interfaces related/belonging to an user. + + Parameters: + self (user instance): user which interfaces + have to be returned + active (boolean): If true, filter on interfaces + all_interfaces (boolean): If true, returns all interfaces + + returns: + interfaces (queryset): Queryset of interfaces instances + + """ return self.users_interfaces( [self], active=active, all_interfaces=all_interfaces ) + ###### Methods and user edition functions, modify user attributes ###### + + def set_active(self): + """Method, make this user active. Called in post-saved of subscription, + set the state value active if state is not_yet_active, with + a valid membership. + Also make an archived user fully active. + + Parameters: + self (user instance): user to set active + + """ + if self.state == self.STATE_NOT_YET_ACTIVE: + if self.facture_set.filter(valid=True).filter( + Q(vente__type_cotisation="All") | Q(vente__type_cotisation="Adhesion") + ).exists() or OptionalUser.get_cached_value("all_users_active"): + self.state = self.STATE_ACTIVE + self.save() + if self.state == self.STATE_ARCHIVE or self.state == self.STATE_FULL_ARCHIVE: + self.state = self.STATE_ACTIVE + self.unarchive() + self.save() + + def set_password(self, password): + """Method, overload the basic set_password inherited from django BaseUser. + Called when setting a new password, to set the classic django password + hashed, and also the NTLM hashed pwd_ntlm password. + + Parameters: + self (user instance): user to set password + password (string): new password (cleatext) to set. + + """ + from re2o.login import hashNT + + super().set_password(password) + self.pwd_ntlm = hashNT(password) + return + + def confirm_mail(self): + """Method, set the email_state to VERIFIED when the email has been verified. + + Parameters: + self (user instance): user to set password + + """ + self.email_state = self.EMAIL_STATE_VERIFIED + def assign_ips(self): - """ Assign une ipv4 aux machines d'un user """ + """Method, assigns ipv4 to all interfaces related to a user. + + Parameters: + self (user instance): user which interfaces have to be assigned + + """ interfaces = self.user_interfaces() with transaction.atomic(), reversion.create_revision(): Interface.mass_assign_ipv4(interfaces) reversion.set_comment("IPv4 assignment") def unassign_ips(self): - """ Désassigne les ipv4 aux machines de l'user""" + """Method, unassigns and remove ipv4 to all interfaces related to a user. + (set ipv4 field to null) + + Parameters: + self (user instance): user which interfaces have to be assigned + + """ interfaces = self.user_interfaces() with transaction.atomic(), reversion.create_revision(): Interface.mass_unassign_ipv4(interfaces) @@ -609,48 +987,119 @@ class User( @classmethod def mass_unassign_ips(cls, users_list): + """Class method, unassigns and remove ipv4 to all interfaces related + to a list of users. + + Parameters: + users_list (list of users or queryset): users which interfaces + have to be unassigned + + """ interfaces = cls.users_interfaces(users_list) with transaction.atomic(), reversion.create_revision(): Interface.mass_unassign_ipv4(interfaces) reversion.set_comment("IPv4 assignment") - @classmethod - def mass_disable_email(cls, queryset_users): - """Disable email account and redirection""" - queryset_users.update(local_email_enabled=False) - queryset_users.update(local_email_redirect=False) - - @classmethod - def mass_delete_data(cls, queryset_users): - """This users will be completely archived, so only keep mandatory data""" - cls.mass_disable_email(queryset_users) - Machine.mass_delete(Machine.objects.filter(user__in=queryset_users)) - cls.ldap_delete_users(queryset_users) - def disable_email(self): - """Disable email account and redirection""" + """Method, disable email account and email redirection for + an user. + + Parameters: + self (user instance): user to disabled email. + + """ self.local_email_enabled = False self.local_email_redirect = False + @classmethod + def mass_disable_email(cls, queryset_users): + """Class method, disable email accounts and email redirection for + a list of users (or queryset). + + Parameters: + users_list (list of users or queryset): users which email + account to disable. + + """ + queryset_users.update(local_email_enabled=False) + queryset_users.update(local_email_redirect=False) + def delete_data(self): - """This user will be completely archived, so only keep mandatory data""" + """Method, delete non mandatory data, delete machine, + and disable email accounts for a list of users (or queryset). + Called during full archive process. + + Parameters: + self (user instance): user to delete data. + + """ self.disable_email() self.machine_set.all().delete() + @classmethod + def mass_delete_data(cls, queryset_users): + """Class method, delete non mandatory data, delete machine + and disable email accounts for a list of users (or queryset). + Called during full archive process. + + Parameters: + users_list (list of users or queryset): users to perform + delete data. + + """ + cls.mass_disable_email(queryset_users) + Machine.mass_delete(Machine.objects.filter(user__in=queryset_users)) + cls.ldap_delete_users(queryset_users) + + def archive(self): + """Method, archive user by unassigning ips. + + Parameters: + self (user instance): user to archive. + + """ + self.unassign_ips() + @classmethod def mass_archive(cls, users_list): - """Mass Archive several users, take a queryset - Copy Queryset to avoid eval problem with queryset update""" + """Class method, mass archive a queryset of users. + Called during archive process, unassign ip and set to + archive state. + + Parameters: + users_list (list of users queryset): users to perform + mass archive. + + """ # Force eval of queryset bool(users_list) users_list = users_list.all() cls.mass_unassign_ips(users_list) users_list.update(state=User.STATE_ARCHIVE) + def full_archive(self): + """Method, full archive an user by unassigning ips, deleting data + and ldap deletion. + + Parameters: + self (user instance): user to full archive. + + """ + self.archive() + self.delete_data() + self.ldap_del() + @classmethod def mass_full_archive(cls, users_list): - """Mass Archive several users, take a queryset - Copy Queryset to avoid eval problem with queryset update""" + """Class method, mass full archive a queryset of users. + Called during full archive process, unassign ip, delete + non mandatory data and set to full archive state. + + Parameters: + users_list (list of users queryset): users to perform + mass full archive. + + """ # Force eval of queryset bool(users_list) users_list = users_list.all() @@ -658,23 +1107,25 @@ class User( cls.mass_delete_data(users_list) users_list.update(state=User.STATE_FULL_ARCHIVE) - def archive(self): - """ Filling the user; no more active""" - self.unassign_ips() - - def full_archive(self): - """Full Archive = Archive + Service access complete deletion""" - self.archive() - self.delete_data() - self.ldap_del() - def unarchive(self): - """Unfilling the user""" + """Method, unarchive an user by assigning ips, and recreating + ldap user associated. + + Parameters: + self (user instance): user to unarchive. + + """ self.assign_ips() self.ldap_sync() def state_sync(self): - """Archive, or unarchive, if the user was not active/or archived before""" + """Master Method, call unarchive, full_archive or archive method + on an user when state is changed, based on previous state. + + Parameters: + self (user instance): user to sync state. + + """ if ( self.__original_state != self.STATE_ACTIVE and self.state == self.STATE_ACTIVE @@ -694,15 +1145,26 @@ class User( def ldap_sync( self, base=True, access_refresh=True, mac_refresh=True, group_refresh=False ): - """ Synchronisation du ldap. Synchronise dans le ldap les attributs de - self - Options : base : synchronise tous les attributs de base - nom, prenom, - mail, password, shell, home - access_refresh : synchronise le dialup_access notant si l'user a accès - aux services - mac_refresh : synchronise les machines de l'user - group_refresh : synchronise les group de l'user - Si l'instance n'existe pas, on crée le ldapuser correspondant""" + """Method ldap_sync, sync in ldap with self user attributes. + Each User instance is copy into ldap, via a LdapUser virtual objects. + This method performs a copy of several attributes (name, surname, mail, + hashed SSHA password, ntlm password, shell, homedirectory). + + Update, or create if needed a ldap entry related with the User instance. + + Parameters: + self (user instance): user to sync in ldap. + base (boolean): Default true, if base is true, perform a basic + sync of basic attributes. + access_refresh (boolean): Default true, if access_refresh is true, + update the dialup_access attributes based on has_access (is this user + has a valid internet access). + mac_refresh (boolean): Default true, if mac_refresh, update the mac_address + list of the user. + group_refresh (boolean): Default False, if true, update the groups membership + of this user. Onerous option, call ldap_sync() on every groups of the user. + + """ if sys.version_info[0] >= 3 and ( self.state == self.STATE_ACTIVE or self.state == self.STATE_ARCHIVE @@ -759,7 +1221,12 @@ class User( user_ldap.save() def ldap_del(self): - """ Supprime la version ldap de l'user""" + """Method, delete an user in ldap. + + Parameters: + self (user instance): user to delete in Ldap. + + """ try: user_ldap = LdapUser.objects.get(name=self.pseudo) user_ldap.delete() @@ -768,13 +1235,29 @@ class User( @classmethod def ldap_delete_users(cls, queryset_users): - """Delete multiple users in ldap""" + """Class method, delete several users in ldap (queryset). + + Parameters: + queryset_users (list of users queryset): users to delete + in ldap. + """ LdapUser.objects.filter( name__in=list(queryset_users.values_list("pseudo", flat=True)) ) + ###### Send mail functions ###### + def notif_inscription(self, request=None): - """ Prend en argument un objet user, envoie un mail de bienvenue """ + """Method/function, send an email 'welcome' to user instance, after + successfull register. + + Parameters: + self (user instance): user to send the welcome email + request (optional request): Specify request + + Returns: + email: Welcome email after user register + """ template = loader.get_template("users/email_welcome") mailmessageoptions, _created = MailMessageOption.objects.get_or_create() context = { @@ -797,8 +1280,17 @@ class User( ) def reset_passwd_mail(self, request): - """ Prend en argument un request, envoie un mail de - réinitialisation de mot de pass """ + """Method/function, makes a Request class instance, and send + an email to user instance for password change in case of initial + password set or forget password form. + + Parameters: + self (user instance): user to send the welcome email + request: Specify request, mandatory to build the reset link + + Returns: + email: Reset password email for user instance + """ req = Request() req.type = Request.PASSWD req.user = self @@ -826,11 +1318,18 @@ class User( ) def send_confirm_email_if_necessary(self, request): - """Update the user's email state: + """Method/function, check if a confirmation by email is needed, + and trigger send. * If the user changed email, it needs to be confirmed * If they're not fully archived, send a confirmation email - Returns whether an email was sent""" + Parameters: + self (user instance): user to send the confirmation email + request: Specify request, mandatory to build the reset link + + Returns: + boolean: True if a confirmation of the mail is needed + """ # Only update the state if the email changed if self.__original_email == self.email: return False @@ -855,7 +1354,17 @@ class User( return True def trigger_email_changed_state(self, request): - """Trigger an email, and changed values after email_state been manually updated""" + """Method/function, update the value of the last email change, + and call and send the confirm email link. + Function called only after a manual of email_state by an admin. + + Parameters: + self (user instance): user to send the confirmation email + request: Specify request, mandatory to build the reset link + + Returns: + boolean: True if a confirmation of the mail is needed + """ if self.email_state == self.EMAIL_STATE_VERIFIED: return False @@ -866,6 +1375,16 @@ class User( return True def confirm_email_before_date(self): + """Method/function, calculate the maximum date for confirmation + of the new email address + + Parameters: + self (user instance): user to calculate maximum date + for confirmation + + Returns: + date: Date of the maximum time to perform email confirmation + """ if self.email_state == self.EMAIL_STATE_VERIFIED: return None @@ -873,8 +1392,18 @@ class User( return self.email_change_date + timedelta(days=days) def confirm_email_address_mail(self, request): - """Prend en argument un request, envoie un mail pour - confirmer l'adresse""" + """Method/function, makes a Request class instance, and send + an email to user instance to confirm a new email address. + * If the user changed email, it needs to be confirmed + * If they're not fully archived, send a confirmation email + + Parameters: + self (user instance): user to send the confirmation email + request: Specify request, mandatory to build the reset link + + Returns: + email: An email with a link to confirm the new email address + """ # Delete all older requests for this user, that aren't for this email filter = Q(user=self) & Q(type=Request.EMAIL) & ~Q(email=self.email) Request.objects.filter(filter).delete() @@ -912,8 +1441,19 @@ class User( return def autoregister_machine(self, mac_address, nas_type, request=None): - """ Fonction appellée par freeradius. Enregistre la mac pour - une machine inconnue sur le compte de l'user""" + """Function, register a new interface on the user instance account. + Called automaticaly mainly by freeradius python backend, for autoregister. + + Parameters: + self (user instance): user to register new interface + mac_address (string): New mac address to add on the new interface + nas_type (Django Nas object instance): The nas object calling + request: Optional django request + + Returns: + interface (Interface instance): The new interface registered + + """ allowed, _message, _rights = Machine.can_create(self, self.id) if not allowed: return False, _("Maximum number of registered machines reached.") @@ -944,8 +1484,16 @@ class User( return interface_cible, _("OK") def notif_auto_newmachine(self, interface): - """Notification mail lorsque une machine est automatiquement - ajoutée par le radius""" + """Function/method, send an email to notify the new interface + registered on user instance account. + + Parameters: + self (user instance): user to notify new registration + interface (interface instance): new interface registered + + Returns: + boolean: True if a confirmation of the mail is needed + """ template = loader.get_template("users/email_auto_newmachine") context = { "nom": self.get_full_name(), @@ -967,7 +1515,16 @@ class User( return def notif_disable(self, request=None): - """Envoi un mail de notification informant que l'adresse mail n'a pas été confirmée""" + """Function/method, send an email to notify that the account is disabled + in case of unconfirmed email address. + + Parameters: + self (user instance): user to notif disabled decision + request (django request): request to build email + + Returns: + email: Notification email + """ template = loader.get_template("users/email_disable_notif") context = { "name": self.get_full_name(), @@ -986,34 +1543,14 @@ class User( ) return - def set_password(self, password): - """ A utiliser de préférence, set le password en hash courrant et - dans la version ntlm""" - from re2o.login import hashNT - - super().set_password(password) - self.pwd_ntlm = hashNT(password) - return - - def confirm_mail(self): - """Marque l'email de l'utilisateur comme confirmé""" - self.email_state = self.EMAIL_STATE_VERIFIED - - @cached_property - def email_address(self): - if ( - OptionalUser.get_cached_value("local_email_accounts_enabled") - and self.local_email_enabled - ): - return self.emailaddress_set.all() - return EMailAddress.objects.none() - def get_next_domain_name(self): - """Look for an available name for a new interface for - this user by trying "pseudo0", "pseudo1", "pseudo2", ... + """Function/method, provide a unique name for a new interface. - Recherche un nom disponible, pour une machine. Doit-être - unique, concatène le nom, le pseudo et le numero de machine + Parameters: + self (user instance): user to get a new domain name + + Returns: + domain name (string): String of new domain name """ def simple_pseudo(): @@ -1281,6 +1818,7 @@ class User( :param user_request: The user who request :returns: a message and a boolean which is True if permission is granted. + """ can = user_request.is_superuser return ( @@ -1298,6 +1836,7 @@ class User( :param user_request: The user who ask for viewing the target. :return: A boolean telling if the acces is granted and an explanation text + """ if self.is_class_club and user_request.is_class_adherent: if ( @@ -1330,6 +1869,7 @@ class User( :param user_request: The user who wants to view the list. :return: True if the user can view the list and an explanation message. + """ can = user_request.has_perm("users.view_user") return ( @@ -1370,13 +1910,36 @@ class User( self.__original_email = self.email def clean_pseudo(self, *args, **kwargs): + """Method, clean the pseudo value. The pseudo must be unique, but also + it must not already be used an an email address, so a check is performed. + + Parameters: + self (user instance): user to clean pseudo value. + + Returns: + Django ValidationError: if the pseudo value can not be used. + + """ if EMailAddress.objects.filter(local_part=self.pseudo.lower()).exclude( user_id=self.id ): raise ValidationError(_("This username is already used.")) def clean_email(self, *args, **kwargs): - # Allow empty emails only if the user had an empty email before + """Method, clean the email value. + Validate that: + * An email value has been provided; email field can't be nullified. + (the user must be reachable by email) + * The provided email is not a local email to avoid loops + * Set the email as lower. + + Parameters: + self (user instance): user to clean email value. + + Returns: + Django ValidationError: if the email value can not be used. + + """ is_created = not self.pk if not self.email and (self.__original_email or is_created): raise forms.ValidationError( @@ -1393,19 +1956,20 @@ class User( ) def clean(self, *args, **kwargs): - """Check if this pseudo is already used by any mailalias. - Better than raising an error in post-save and catching it""" + """Method, general clean for User model. + Clean pseudo and clean email. + + Parameters: + self (user instance): user to clean. + + """ super(User, self).clean(*args, **kwargs) self.clean_pseudo(*args, **kwargs) self.clean_email(*args, **kwargs) - @property - def image_url(self): - if self.profile_image and hasattr(self.profile_image, 'url'): - return self.profile_image.url def save(self, *args, **kwargs): - if self.profile_image: + if self.profile_image: im = Image.open(self.profile_image) output = BytesIO() im = im.resize( (100,100) ) @@ -1421,8 +1985,14 @@ class User( class Adherent(User): - """ A class representing a member (it's a user with special - informations) """ + """Base re2o Adherent model, inherit from User. Add other attributes. + + Attributes: + name: name of the user + room: room of the user + gpg_fingerprint: The gpgfp of the user + + """ name = models.CharField(max_length=255) room = models.OneToOneField( @@ -1435,7 +2005,13 @@ class Adherent(User): verbose_name_plural = _("members") def format_gpgfp(self): - """Format gpg finger print as AAAA BBBB... from a string AAAABBBB....""" + """Method, format the gpgfp value, with blocks of 4 characters, + as AAAA BBBB instead of AAAABBBB. + + Parameters: + self (user instance): user to clean gpgfp value. + + """ self.gpg_fingerprint = " ".join( [ self.gpg_fingerprint[i : i + 4] @@ -1444,7 +2020,15 @@ class Adherent(User): ) def validate_gpgfp(self): - """Validate from raw entry if is it a valid gpg fp""" + """Method, clean the gpgfp value, validate if the raw entry is a valid gpg fp. + + Parameters: + self (user instance): user to clean gpgfp check. + + Returns: + Django ValidationError: if the gpgfp value is invalid. + + """ if self.gpg_fingerprint: gpg_fingerprint = self.gpg_fingerprint.replace(" ", "").upper() if not re.match("^[0-9A-F]{40}$", gpg_fingerprint): @@ -1459,6 +2043,7 @@ class Adherent(User): :param adherentid: The id of the adherent we are looking for. :return: An adherent. + """ return cls.objects.get(pk=adherentid) @@ -1469,6 +2054,7 @@ class Adherent(User): :param user_request: The user who wants to create a user object. :return: a message and a boolean which is True if the user can create a user or if the `options.all_can_create` is set. + """ if not user_request.is_authenticated: if not OptionalUser.get_cached_value( @@ -1478,7 +2064,7 @@ class Adherent(User): else: return True, None, None else: - if OptionalUser.get_cached_value("all_can_create_adherent"): + if OptionalUser.get_cached_value("all_can_create_adherent"): return True, None, None else: can = user_request.has_perm("users.add_user") @@ -1491,7 +2077,12 @@ class Adherent(User): ) def clean(self, *args, **kwargs): - """Format the GPG fingerprint""" + """Method, clean and validate the gpgfp value. + + Parameters: + self (user instance): user to perform clean. + + """ super(Adherent, self).clean(*args, **kwargs) if self.gpg_fingerprint: self.validate_gpgfp() @@ -1500,7 +2091,15 @@ class Adherent(User): class Club(User): """ A class representing a club (it is considered as a user - with special informations) """ + with special informations) + + Attributes: + administrators: administrators of the club + members: members of the club + room: room(s) of the club + mailing: Boolean, activate mailing list for this club. + + """ room = models.ForeignKey( "topologie.Room", on_delete=models.PROTECT, blank=True, null=True @@ -1579,9 +2178,11 @@ class Club(User): @receiver(post_save, sender=Club) @receiver(post_save, sender=User) def user_post_save(**kwargs): - """ Synchronisation post_save : envoie le mail de bienvenue si creation - Synchronise le pseudo, en créant un alias mail correspondant - Synchronise le ldap""" + """Django signal, post save operations on Adherent, Club and User. + Sync pseudo, sync ldap, create mailalias and send welcome email if needed + (new user) + + """ is_created = kwargs["created"] user = kwargs["instance"] EMailAddress.objects.get_or_create(local_part=user.pseudo.lower(), user=user) @@ -1598,6 +2199,10 @@ def user_post_save(**kwargs): @receiver(m2m_changed, sender=User.groups.through) def user_group_relation_changed(**kwargs): + """Django signal, used for User Groups change (related models). + Sync ldap, with calling group_refresh. + + """ action = kwargs["action"] if action in ("post_add", "post_remove", "post_clear"): user = kwargs["instance"] @@ -1610,14 +2215,29 @@ def user_group_relation_changed(**kwargs): @receiver(post_delete, sender=Club) @receiver(post_delete, sender=User) def user_post_delete(**kwargs): - """Post delete d'un user, on supprime son instance ldap""" + """Django signal, post delete operations on Adherent, Club and User. + Delete user in ldap. + + """ user = kwargs["instance"] user.ldap_del() regen("mailing") class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): - """ Classe des users daemons, règle leurs accès au ldap""" + """A class representing a serviceuser (it is considered as a user + with special informations). + The serviceuser is a special user used with special access to ldap tree. It is + its only usefullness, and service user can't connect to re2o. + Each service connected to ldap for auth (ex dokuwiki, owncloud, etc) should + have a different service user with special acl (readonly, auth) and password. + + Attributes: + pseudo: login of the serviceuser + access_group: acl for this serviceuser + comment: Comment for this serviceuser. + + """ readonly = "readonly" ACCESS = (("auth", "auth"), ("readonly", "readonly"), ("usermgmt", "usermgmt")) @@ -1640,15 +2260,34 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): verbose_name_plural = _("service users") def get_full_name(self): - """ Renvoie le nom complet du serviceUser formaté nom/prénom""" + """Shortcuts, return a pretty name for the serviceuser. + + Parameters: + self (ServiceUser instance): serviceuser to return infos. + + """ return _("Service user <{name}>").format(name=self.pseudo) def get_short_name(self): - """ Renvoie seulement le nom""" + """Shortcuts, return the shortname (pseudo) of the serviceuser. + + Parameters: + self (ServiceUser instance): serviceuser to return infos. + + """ return self.pseudo def ldap_sync(self): - """ Synchronisation du ServiceUser dans sa version ldap""" + """Method ldap_sync, sync the serviceuser in ldap with its attributes. + Each ServiceUser instance is copy into ldap, via a LdapServiceUser virtual object. + This method performs a copy of several attributes (pseudo, access). + + Update, or create if needed a mirror ldap entry related with the ServiceUserinstance. + + Parameters: + self (serviceuser instance): ServiceUser to sync in ldap. + + """ try: user_ldap = LdapServiceUser.objects.get(name=self.pseudo) except LdapServiceUser.DoesNotExist: @@ -1658,7 +2297,12 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): self.serviceuser_group_sync() def ldap_del(self): - """Suppression de l'instance ldap d'un service user""" + """Method, delete an ServiceUser in ldap. + + Parameters: + self (ServiceUser instance): serviceuser to delete in Ldap. + + """ try: user_ldap = LdapServiceUser.objects.get(name=self.pseudo) user_ldap.delete() @@ -1667,7 +2311,15 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): self.serviceuser_group_sync() def serviceuser_group_sync(self): - """Synchronise le groupe et les droits de groupe dans le ldap""" + """Method, update serviceuser group sync in ldap. + In LDAP, Acl depends on the ldapgroup (readonly, auth, or usermgt), + so the ldap group need to be synced with the accessgroup field on ServiceUser. + Called by ldap_sync and ldap_del. + + Parameters: + self (ServiceUser instance): serviceuser to update groups in LDAP. + + """ try: group = LdapServiceUserGroup.objects.get(name=self.access_group) except: @@ -1690,20 +2342,31 @@ class ServiceUser(RevMixin, AclMixin, AbstractBaseUser): @receiver(post_save, sender=ServiceUser) def service_user_post_save(**kwargs): - """ Synchronise un service user ldap après modification django""" + """Django signal, post save operations on ServiceUser. + Sync or create serviceuser in ldap. + + """ service_user = kwargs["instance"] service_user.ldap_sync() @receiver(post_delete, sender=ServiceUser) def service_user_post_delete(**kwargs): - """ Supprime un service user ldap après suppression django""" + """Django signal, post delete operations on ServiceUser. + Delete service user in ldap. + + """ service_user = kwargs["instance"] service_user.ldap_del() class School(RevMixin, AclMixin, models.Model): - """ Etablissement d'enseignement""" + """A class representing a school; which users are linked. + + Attributes: + name: name of the school + + """ name = models.CharField(max_length=255) @@ -1717,11 +2380,19 @@ class School(RevMixin, AclMixin, models.Model): class ListRight(RevMixin, AclMixin, Group): - """ Ensemble des droits existants. Chaque droit crée un groupe - ldap synchronisé, avec gid. - Permet de gérer facilement les accès serveurs et autres - La clef de recherche est le gid, pour cette raison là - il n'est plus modifiable après creation""" + """ A class representing a listright, inherit from basic django Group object. + Each listrights/groups gathers several users, and can have individuals django + rights, like can_view, can_edit, etc. + Moreover, a ListRight is also a standard unix group, usefull for creating linux + unix groups for servers access or re2o single rights, or both. + Gid is used as a primary key, and can't be changed. + + Attributes: + name: Inherited from Group, name of the ListRight + gid: Group id unix + critical: Boolean, if True the Group can't be changed without special acl + details: Details and description of the group + """ unix_name = models.CharField( max_length=255, @@ -1746,7 +2417,17 @@ class ListRight(RevMixin, AclMixin, Group): return self.name def ldap_sync(self): - """Sychronise les groups ldap avec le model listright coté django""" + """Method ldap_sync, sync the listright/group in ldap with its listright attributes. + Each ListRight/Group instance is copy into ldap, via a LdapUserGroup virtual objects. + This method performs a copy of several attributes (name, members, gid, etc). + The primary key is the gid, and should never change. + + Update, or create if needed a ldap entry related with the ListRight/Group instance. + + Parameters: + self (listright instance): ListRight/Group to sync in ldap. + + """ try: group_ldap = LdapUserGroup.objects.get(gid=self.gid) except LdapUserGroup.DoesNotExist: @@ -1756,7 +2437,12 @@ class ListRight(RevMixin, AclMixin, Group): group_ldap.save() def ldap_del(self): - """Supprime un groupe ldap""" + """Method, delete an ListRight/Group in ldap. + + Parameters: + self (listright/Group instance): group to delete in Ldap. + + """ try: group_ldap = LdapUserGroup.objects.get(gid=self.gid) group_ldap.delete() @@ -1766,21 +2452,32 @@ class ListRight(RevMixin, AclMixin, Group): @receiver(post_save, sender=ListRight) def listright_post_save(**kwargs): - """ Synchronise le droit ldap quand il est modifié""" + """Django signal, post save operations on ListRight/Group objects. + Sync or create group in ldap. + + """ right = kwargs["instance"] right.ldap_sync() @receiver(post_delete, sender=ListRight) def listright_post_delete(**kwargs): - """Suppression d'un groupe ldap après suppression coté django""" + """Django signal, post delete operations on ListRight/Group objects. + Delete group in ldap. + + """ right = kwargs["instance"] right.ldap_del() class ListShell(RevMixin, AclMixin, models.Model): - """Un shell possible. Pas de check si ce shell existe, les - admin sont des grands""" + """A class representing a shell; which users are linked. + A standard linux user shell. (zsh, bash, etc) + + Attributes: + shell: name of the shell + + """ shell = models.CharField(max_length=255, unique=True) @@ -1790,7 +2487,15 @@ class ListShell(RevMixin, AclMixin, models.Model): verbose_name_plural = _("shells") def get_pretty_name(self): - """Return the canonical name of the shell""" + """Method, returns a pretty name for a shell like "bash" or "zsh". + + Parameters: + self (listshell): Shell to return a pretty name. + + Returns: + pretty_name (string): Return a pretty name string for this shell. + + """ return self.shell.split("/")[-1] def __str__(self): @@ -1798,8 +2503,17 @@ class ListShell(RevMixin, AclMixin, models.Model): class Ban(RevMixin, AclMixin, models.Model): - """ Bannissement. Actuellement a un effet tout ou rien. - Gagnerait à être granulaire""" + """ A class representing a ban, which cuts internet access, + as a sanction. + + Attributes: + user: related user for this whitelist + raison: reason of this ban, can be null + date_start: Date of the start of the ban + date_end: Date of the end of the ban + state: Has no effect now, would specify this kind of ban + (hard, soft) + """ STATE_HARD = 0 STATE_SOFT = 1 @@ -1823,7 +2537,16 @@ class Ban(RevMixin, AclMixin, models.Model): verbose_name_plural = _("bans") def notif_ban(self, request=None): - """ Prend en argument un objet ban, envoie un mail de notification """ + """Function/method, send an email to notify that a ban has been + decided and internet access disabled. + + Parameters: + self (ban instance): ban to notif disabled decision + request (django request): request to build email + + Returns: + email: Notification email + """ template = loader.get_template("users/email_ban_notif") context = { "name": self.user.get_full_name(), @@ -1843,7 +2566,15 @@ class Ban(RevMixin, AclMixin, models.Model): return def is_active(self): - """Ce ban est-il actif?""" + """Method, return if the ban is active now or not. + + Parameters: + self (ban): Ban to test if is active. + + Returns: + is_active (boolean): Return True if the ban is active. + + """ return self.date_end > timezone.now() def can_view(self, user_request, *_args, **_kwargs): @@ -1869,7 +2600,10 @@ class Ban(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Ban) def ban_post_save(**kwargs): - """ Regeneration de tous les services après modification d'un ban""" + """Django signal, post save operations on Ban objects. + Sync user's access state in ldap, call email notification if needed. + + """ ban = kwargs["instance"] is_created = kwargs["created"] user = ban.user @@ -1886,7 +2620,10 @@ def ban_post_save(**kwargs): @receiver(post_delete, sender=Ban) def ban_post_delete(**kwargs): - """ Regen de tous les services après suppression d'un ban""" + """Django signal, post delete operations on Ban objects. + Sync user's access state in ldap. + + """ user = kwargs["instance"].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) regen("mailing") @@ -1895,9 +2632,16 @@ def ban_post_delete(**kwargs): class Whitelist(RevMixin, AclMixin, models.Model): - """Accès à titre gracieux. L'utilisateur ne paye pas; se voit - accorder un accès internet pour une durée défini. Moins - fort qu'un ban quel qu'il soit""" + """ A class representing a whitelist, which gives a free internet + access to a user for special reason. + Is overrided by a ban object. + + Attributes: + user: related user for this whitelist + raison: reason of this whitelist, can be null + date_start: Date of the start of the whitelist + date_end: Date of the end of the whitelist + """ user = models.ForeignKey("User", on_delete=models.PROTECT) raison = models.CharField(max_length=255) @@ -1910,7 +2654,15 @@ class Whitelist(RevMixin, AclMixin, models.Model): verbose_name_plural = _("whitelists (free of charge access)") def is_active(self): - """ Is this whitelisting active ? """ + """Method, returns if the whitelist is active now or not. + + Parameters: + self (whitelist): Whitelist to test if is active. + + Returns: + is_active (boolean): Return True if the whistelist is active. + + """ return self.date_end > timezone.now() def can_view(self, user_request, *_args, **_kwargs): @@ -1939,8 +2691,10 @@ class Whitelist(RevMixin, AclMixin, models.Model): @receiver(post_save, sender=Whitelist) def whitelist_post_save(**kwargs): - """Après modification d'une whitelist, on synchronise les services - et on lui permet d'avoir internet""" + """Django signal, post save operations on Whitelist objects. + Sync user's access state in ldap. + + """ whitelist = kwargs["instance"] user = whitelist.user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) @@ -1956,8 +2710,10 @@ def whitelist_post_save(**kwargs): @receiver(post_delete, sender=Whitelist) def whitelist_post_delete(**kwargs): - """Après suppression d'une whitelist, on supprime l'accès internet - en forçant la régénration""" + """Django signal, post delete operations on Whitelist objects. + Sync user's access state in ldap. + + """ user = kwargs["instance"].user user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) regen("mailing") @@ -1966,9 +2722,17 @@ def whitelist_post_delete(**kwargs): class Request(models.Model): - """ Objet request, générant une url unique de validation. - Utilisé par exemple pour la generation du mot de passe et - sa réinitialisation""" + """ A class representing for user's request of reset password by email, or + confirm a new email address, with a link. + + Attributes: + type: type of request (password, or confirm email address) + token: single-user token for this request + user: related user for this request + email: If needed, related email to send the request and the link + created_at: Date at the request was created + expires_at: The request will be invalid after the expires_at date + """ PASSWD = "PW" EMAIL = "EM" @@ -1990,138 +2754,13 @@ class Request(models.Model): super(Request, self).save() -class LdapUser(ldapdb.models.Model): - """ - Class for representing an LDAP user entry. - """ - - # LDAP meta-data - base_dn = LDAP["base_user_dn"] - object_classes = [ - "inetOrgPerson", - "top", - "posixAccount", - "sambaSamAccount", - "radiusprofile", - "shadowAccount", - ] - - # attributes - gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - uid = ldapdb.models.fields.CharField(db_column="uid", max_length=200) - uidNumber = ldapdb.models.fields.IntegerField(db_column="uidNumber", unique=True) - sn = ldapdb.models.fields.CharField(db_column="sn", max_length=200) - login_shell = ldapdb.models.fields.CharField( - db_column="loginShell", max_length=200, blank=True, null=True - ) - mail = ldapdb.models.fields.CharField(db_column="mail", max_length=200) - given_name = ldapdb.models.fields.CharField(db_column="givenName", max_length=200) - home_directory = ldapdb.models.fields.CharField( - db_column="homeDirectory", max_length=200 - ) - display_name = ldapdb.models.fields.CharField( - db_column="displayName", max_length=200, blank=True, null=True - ) - dialupAccess = ldapdb.models.fields.CharField(db_column="dialupAccess") - sambaSID = ldapdb.models.fields.IntegerField(db_column="sambaSID", unique=True) - user_password = ldapdb.models.fields.CharField( - db_column="userPassword", max_length=200, blank=True, null=True - ) - sambat_nt_password = ldapdb.models.fields.CharField( - db_column="sambaNTPassword", max_length=200, blank=True, null=True - ) - macs = ldapdb.models.fields.ListField( - db_column="radiusCallingStationId", max_length=200, blank=True, null=True - ) - shadowexpire = ldapdb.models.fields.CharField( - db_column="shadowExpire", blank=True, null=True - ) - - def __str__(self): - return self.name - - def __unicode__(self): - return self.name - - def save(self, *args, **kwargs): - self.sn = self.name - self.uid = self.name - self.sambaSID = self.uidNumber - super(LdapUser, self).save(*args, **kwargs) - - -class LdapUserGroup(ldapdb.models.Model): - """ - Class for representing an LDAP group entry. - - Un groupe ldap - """ - - # LDAP meta-data - base_dn = LDAP["base_usergroup_dn"] - object_classes = ["posixGroup"] - - # attributes - gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") - members = ldapdb.models.fields.ListField(db_column="memberUid", blank=True) - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - - def __str__(self): - return self.name - - -class LdapServiceUser(ldapdb.models.Model): - """ - Class for representing an LDAP userservice entry. - - Un user de service coté ldap - """ - - # LDAP meta-data - base_dn = LDAP["base_userservice_dn"] - object_classes = ["applicationProcess", "simpleSecurityObject"] - - # attributes - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - user_password = ldapdb.models.fields.CharField( - db_column="userPassword", max_length=200, blank=True, null=True - ) - - def __str__(self): - return self.name - - -class LdapServiceUserGroup(ldapdb.models.Model): - """ - Class for representing an LDAP userservice entry. - - Un group user de service coté ldap. Dans userservicegroupdn - (voir dans settings_local.py) - """ - - # LDAP meta-data - base_dn = LDAP["base_userservicegroup_dn"] - object_classes = ["groupOfNames"] - - # attributes - name = ldapdb.models.fields.CharField( - db_column="cn", max_length=200, primary_key=True - ) - members = ldapdb.models.fields.ListField(db_column="member", blank=True) - - def __str__(self): - return self.name - - class EMailAddress(RevMixin, AclMixin, models.Model): - """Defines a local email account for a user + """ A class representing an EMailAddress, for local emailaccounts + support. Each emailaddress belongs to a user. + + Attributes: + user: parent user address for this email + local_part: local extension of the email """ user = models.ForeignKey( @@ -2145,6 +2784,16 @@ class EMailAddress(RevMixin, AclMixin, models.Model): @cached_property def complete_email_address(self): + """Shortcuts, returns a complete mailaddress from localpart and emaildomain + specified in preferences. + + Parameters: + self (emailaddress): emailaddress. + + Returns: + emailaddress (string): Complete valid emailaddress + + """ return str(self.local_part) + OptionalUser.get_cached_value( "local_email_domain" ) @@ -2280,6 +2929,17 @@ class EMailAddress(RevMixin, AclMixin, models.Model): ) def clean(self, *args, **kwargs): + """Method, general clean for EMailAddres model. + Clean email local_part field, checking if it is available by calling + the smtp.. + + Parameters: + self (emailaddress): emailaddress local_part to clean. + + Returns: + Django ValidationError, if the localpart does not comply with the policy. + + """ self.local_part = self.local_part.lower() if "@" in self.local_part or "+" in self.local_part: raise ValidationError(_("The local part must not contain @ or +.")) @@ -2287,3 +2947,172 @@ class EMailAddress(RevMixin, AclMixin, models.Model): if result: raise ValidationError(reason) super(EMailAddress, self).clean(*args, **kwargs) + + +class LdapUser(ldapdb.models.Model): + """A class representing a LdapUser in LDAP, its LDAP conterpart. + Synced from re2o django User model, (User django models), + with a copy of its attributes/fields into LDAP, so this class is a mirror + of the classic django User model. + + The basedn userdn is specified in settings. + + Attributes: + name: The name of this User + uid: The uid (login) for the unix user + uidNumber: Linux uid number + gid: The default gid number for this user + sn: The user "str" pseudo + login_shell: Linux shell for the user + mail: Email address contact for this user + display_name: Pretty display name for this user + dialupAccess: Boolean, True for valid membership + sambaSID: Identical id as uidNumber + user_password: SSHA hashed password of user + samba_nt_password: NTLM hashed password of user + macs: Multivalued mac address + shadowexpire: Set it to 0 to block access for this user and disabled + account + """ + + # LDAP meta-data + base_dn = LDAP["base_user_dn"] + object_classes = [ + "inetOrgPerson", + "top", + "posixAccount", + "sambaSamAccount", + "radiusprofile", + "shadowAccount", + ] + + # attributes + gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + uid = ldapdb.models.fields.CharField(db_column="uid", max_length=200) + uidNumber = ldapdb.models.fields.IntegerField(db_column="uidNumber", unique=True) + sn = ldapdb.models.fields.CharField(db_column="sn", max_length=200) + login_shell = ldapdb.models.fields.CharField( + db_column="loginShell", max_length=200, blank=True, null=True + ) + mail = ldapdb.models.fields.CharField(db_column="mail", max_length=200) + given_name = ldapdb.models.fields.CharField(db_column="givenName", max_length=200) + home_directory = ldapdb.models.fields.CharField( + db_column="homeDirectory", max_length=200 + ) + display_name = ldapdb.models.fields.CharField( + db_column="displayName", max_length=200, blank=True, null=True + ) + dialupAccess = ldapdb.models.fields.CharField(db_column="dialupAccess") + sambaSID = ldapdb.models.fields.IntegerField(db_column="sambaSID", unique=True) + user_password = ldapdb.models.fields.CharField( + db_column="userPassword", max_length=200, blank=True, null=True + ) + sambat_nt_password = ldapdb.models.fields.CharField( + db_column="sambaNTPassword", max_length=200, blank=True, null=True + ) + macs = ldapdb.models.fields.ListField( + db_column="radiusCallingStationId", max_length=200, blank=True, null=True + ) + shadowexpire = ldapdb.models.fields.CharField( + db_column="shadowExpire", blank=True, null=True + ) + + def __str__(self): + return self.name + + def __unicode__(self): + return self.name + + def save(self, *args, **kwargs): + self.sn = self.name + self.uid = self.name + self.sambaSID = self.uidNumber + super(LdapUser, self).save(*args, **kwargs) + + +class LdapUserGroup(ldapdb.models.Model): + """A class representing a LdapUserGroup in LDAP, its LDAP conterpart. + Synced from UserGroup, (ListRight/Group django models), + with a copy of its attributes/fields into LDAP, so this class is a mirror + of the classic django ListRight model. + + The basedn usergroupdn is specified in settings. + + Attributes: + name: The name of this LdapUserGroup + gid: The gid number for this unix group + members: Users dn members of this LdapUserGroup + """ + + # LDAP meta-data + base_dn = LDAP["base_usergroup_dn"] + object_classes = ["posixGroup"] + + # attributes + gid = ldapdb.models.fields.IntegerField(db_column="gidNumber") + members = ldapdb.models.fields.ListField(db_column="memberUid", blank=True) + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + + def __str__(self): + return self.name + + +class LdapServiceUser(ldapdb.models.Model): + """A class representing a ServiceUser in LDAP, its LDAP conterpart. + Synced from ServiceUser, with a copy of its attributes/fields into LDAP, + so this class is a mirror of the classic django ServiceUser model. + + The basedn userservicedn is specified in settings. + + Attributes: + name: The name of this ServiceUser + user_password: The SSHA hashed password of this ServiceUser + """ + + # LDAP meta-data + base_dn = LDAP["base_userservice_dn"] + object_classes = ["applicationProcess", "simpleSecurityObject"] + + # attributes + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + user_password = ldapdb.models.fields.CharField( + db_column="userPassword", max_length=200, blank=True, null=True + ) + + def __str__(self): + return self.name + + +class LdapServiceUserGroup(ldapdb.models.Model): + """A class representing a ServiceUserGroup in LDAP, its LDAP conterpart. + Synced from ServiceUserGroup, with a copy of its attributes/fields into LDAP, + so this class is a mirror of the classic django ServiceUserGroup model. + + The basedn userservicegroupdn is specified in settings. + + Attributes: + name: The name of this ServiceUserGroup + members: ServiceUsers dn members of this ServiceUserGroup + """ + + # LDAP meta-data + base_dn = LDAP["base_userservicegroup_dn"] + object_classes = ["groupOfNames"] + + # attributes + name = ldapdb.models.fields.CharField( + db_column="cn", max_length=200, primary_key=True + ) + members = ldapdb.models.fields.ListField(db_column="member", blank=True) + + def __str__(self): + return self.name + + diff --git a/users/urls.py b/users/urls.py index 9ecb3967..19a39adf 100644 --- a/users/urls.py +++ b/users/urls.py @@ -23,7 +23,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. """ -Definition des urls, pointant vers les views +The defined URLs for the Users app """ from __future__ import unicode_literals diff --git a/users/views.py b/users/views.py index 57141a78..54f67b77 100644 --- a/users/views.py +++ b/users/views.py @@ -27,13 +27,30 @@ # Lara Kermarec, Gabriel Détraz, Lemesle Augustin # Gplv2 """ -Module des views. +Django users views module. -On définit les vues pour l'ajout, l'edition des users : infos personnelles, -mot de passe, etc +Here are defined all functions of views, for the users re2o application. This views +allow both edition, creation, deletion and diplay of users objects. +Here are view that allow the addition/deletion/edition of: + * Users (Club/Adherent) and derived settings like EmailSettings of users + * School + * Bans + * Whitelist + * Shell + * ServiceUser + +Also add extra views for : + * Ask for reset password by email + * Ask for new email for email confirmation + * Register room and interface on user account with switch web redirection. + +All the view must be as simple as possible, with returning the correct form to user during +get, and during post, performing change in database with simple ".save()" function. + +The aim is to put all "intelligent" functions in both forms and models functions. In fact, this +will allow to user other frontend (like REST api) to perform editions, creations, etc on database, +without code duplication. -Permet aussi l'ajout, edition et suppression des droits, des bannissements, -des whitelist, des services users et des écoles """ from __future__ import unicode_literals @@ -117,8 +134,17 @@ import os @can_create(Adherent) def new_user(request): - """ Vue de création d'un nouvel utilisateur, - envoie un mail pour le mot de passe""" + """View for new Adherent/User form creation. + Then, send an email to the new user, and also if needed to + set its password. + + Parameters: + request (django request): Standard django request. + + Returns: + Django User form. + + """ user = AdherentCreationForm(request.POST or None, request.FILES or None, user=request.user) user.request = request @@ -167,8 +193,17 @@ def new_user(request): @login_required @can_create(Club) def new_club(request): - """ Vue de création d'un nouveau club, - envoie un mail pour le mot de passe""" + """View for new Club/User form creation. + Then, send an email to the new user, and also if needed to + set its password. + + Parameters: + request (django request): Standard django request. + + Returns: + Django User form. + + """ club = ClubForm(request.POST or None, request.FILES or None, user=request.user) club.request = request @@ -192,8 +227,16 @@ def new_club(request): @login_required @can_edit(Club) def edit_club_admin_members(request, club_instance, **_kwargs): - """Vue d'edition de la liste des users administrateurs et - membres d'un club""" + """View for editing clubs and administrators. + + Parameters: + request (django request): Standard django request. + club_instance: Club instance to edit + + Returns: + Django User form. + + """ club = ClubAdminandMembersForm(request.POST or None, request.FILES or None, instance=club_instance) if club.is_valid(): if club.changed_data: @@ -212,9 +255,17 @@ def edit_club_admin_members(request, club_instance, **_kwargs): @login_required @can_edit(User) def edit_info(request, user, userid): - """ Edite un utilisateur à partir de son id, - si l'id est différent de request.user, vérifie la - possession du droit cableur """ + """View for editing base user informations. + Perform an acl check on user instance. + + Parameters: + request (django request): Standard django request. + user: User instance to edit + + Returns: + Django User form. + + """ if user.is_class_adherent: user_form = AdherentEditForm( request.POST or None, request.FILES or None, instance=user.adherent, user=request.user @@ -246,7 +297,18 @@ def edit_info(request, user, userid): @login_required @can_edit(User, "state") def state(request, user, userid): - """ Change the state (active/unactive/archived) of a user""" + """View for editing state of user. + Perform an acl check on user instance, and check if editing user + has state edition permission. + + Parameters: + request (django request): Standard django request. + user: User instance to edit + + Returns: + Django User form. + + """ state_form = StateForm(request.POST or None, instance=user) if state_form.is_valid(): if state_form.changed_data: @@ -265,7 +327,18 @@ def state(request, user, userid): @login_required @can_edit(User, "groups") def groups(request, user, userid): - """ View to edit the groups of a user """ + """View for editing groups of user. + Perform an acl check on user instance, and check if editing user + has groups edition permission. + + Parameters: + request (django request): Standard django request. + user: User instance to edit + + Returns: + Django User form. + + """ group_form = GroupForm(request.POST or None, instance=user, user=request.user) if group_form.is_valid(): if group_form.changed_data: @@ -280,9 +353,20 @@ def groups(request, user, userid): @login_required @can_edit(User, "password") def password(request, user, userid): - """ Reinitialisation d'un mot de passe à partir de l'userid, - pour self par défaut, pour tous sans droit si droit cableur, - pour tous si droit bureau """ + """View for editing password of user. + Perform an acl check on user instance, and check if editing user + has password edition permission. + If User instance is in critical groups, the edition requires extra + permission. + + Parameters: + request (django request): Standard django request. + user: User instance to edit password + + Returns: + Django User form. + + """ u_form = PassForm(request.POST or None, instance=user, user=request.user) if u_form.is_valid(): if u_form.changed_data: @@ -299,7 +383,20 @@ def password(request, user, userid): @login_required @can_edit(User, "groups") def del_group(request, user, listrightid, **_kwargs): - """ View used to delete a group """ + """View for editing groups of user. + Perform an acl check on user instance, and check if editing user + has groups edition permission. + If User instance is in critical groups, the edition requires extra + permission. + + Parameters: + request (django request): Standard django request. + user: User instance to edit groups + + Returns: + Django User form. + + """ user.groups.remove(ListRight.objects.get(id=listrightid)) user.save() messages.success(request, _("%s was removed from the group.") % user) @@ -309,7 +406,18 @@ def del_group(request, user, listrightid, **_kwargs): @login_required @can_edit(User, "is_superuser") def del_superuser(request, user, **_kwargs): - """Remove the superuser right of an user.""" + """View for editing superuser attribute of user. + Perform an acl check on user instance, and check if editing user + has edition of superuser flag on target user. + + Parameters: + request (django request): Standard django request. + user: User instance to edit superuser flag. + + Returns: + Django User form. + + """ user.is_superuser = False user.save() messages.success(request, _("%s is no longer superuser.") % user) @@ -319,7 +427,18 @@ def del_superuser(request, user, **_kwargs): @login_required @can_create(ServiceUser) def new_serviceuser(request): - """ Vue de création d'un nouvel utilisateur service""" + """View for creation of new serviceuser, for external services on + ldap tree for auth purpose (dokuwiki, owncloud, etc). + Perform an acl check on editing user, and check if editing user + has permission of create new serviceuser. + + Parameters: + request (django request): Standard django request. + + Returns: + Django ServiceUser form. + + """ user = ServiceUserForm(request.POST or None) if user.is_valid(): user.save() @@ -333,7 +452,19 @@ def new_serviceuser(request): @login_required @can_edit(ServiceUser) def edit_serviceuser(request, serviceuser, **_kwargs): - """ Edit a ServiceUser """ + """View for edition of serviceuser, for external services on + ldap tree for auth purpose (dokuwiki, owncloud, etc). + Perform an acl check on editing user, and check if editing user + has permission of edit target serviceuser. + + Parameters: + request (django request): Standard django request. + serviceuser: ServiceUser instance to edit attributes. + + Returns: + Django ServiceUser form. + + """ serviceuser = EditServiceUserForm(request.POST or None, instance=serviceuser) if serviceuser.is_valid(): if serviceuser.changed_data: @@ -348,7 +479,19 @@ def edit_serviceuser(request, serviceuser, **_kwargs): @login_required @can_delete(ServiceUser) def del_serviceuser(request, serviceuser, **_kwargs): - """Suppression d'un ou plusieurs serviceusers""" + """View for removing serviceuser, for external services on + ldap tree for auth purpose (dokuwiki, owncloud, etc). + Perform an acl check on editing user, and check if editing user + has permission of deleting target serviceuser. + + Parameters: + request (django request): Standard django request. + serviceuser: ServiceUser instance to delete. + + Returns: + Django ServiceUser form. + + """ if request.method == "POST": serviceuser.delete() messages.success(request, _("The service user was deleted.")) @@ -364,9 +507,19 @@ def del_serviceuser(request, serviceuser, **_kwargs): @can_create(Ban) @can_edit(User) def add_ban(request, user, userid): - """ Ajouter un banissement, nécessite au moins le droit bofh - (a fortiori bureau) - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" + """View for adding a ban object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of adding a ban on target user, add_ban. + Syntaxe: DD/MM/AAAA, the ban takes an immediate effect. + + Parameters: + request (django request): Standard django request. + user: User instance to add a ban. + + Returns: + Django Ban form. + + """ ban_instance = Ban(user=user) ban = BanForm(request.POST or None, instance=ban_instance) ban.request = request @@ -383,9 +536,19 @@ def add_ban(request, user, userid): @login_required @can_edit(Ban) def edit_ban(request, ban_instance, **_kwargs): - """ Editer un bannissement, nécessite au moins le droit bofh - (a fortiori bureau) - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement""" + """View for editing a ban object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of editing a ban on target user, edit_ban. + Syntaxe: DD/MM/AAAA, the ban takes an immediate effect. + + Parameters: + request (django request): Standard django request. + ban: Ban instance to edit. + + Returns: + Django Ban form. + + """ ban = BanForm(request.POST or None, instance=ban_instance) ban.request = request @@ -400,7 +563,18 @@ def edit_ban(request, ban_instance, **_kwargs): @login_required @can_delete(Ban) def del_ban(request, ban, **_kwargs): - """ Supprime un banissement""" + """View for removing a ban object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of deleting a ban on target user, del_ban. + + Parameters: + request (django request): Standard django request. + ban: Ban instance to delete. + + Returns: + Django Ban form. + + """ if request.method == "POST": ban.delete() messages.success(request, _("The ban was deleted.")) @@ -412,10 +586,19 @@ def del_ban(request, ban, **_kwargs): @can_create(Whitelist) @can_edit(User) def add_whitelist(request, user, userid): - """ Accorder un accès gracieux, temporaire ou permanent. - Need droit cableur - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, - raison obligatoire""" + """View for adding a whitelist object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of adding a wheitelist on target user, add_whitelist. + Syntaxe: DD/MM/AAAA, the whitelist takes an immediate effect. + + Parameters: + request (django request): Standard django request. + user: User instance to add a whitelist. + + Returns: + Django Whitelist form. + + """ whitelist_instance = Whitelist(user=user) whitelist = WhitelistForm(request.POST or None, instance=whitelist_instance) if whitelist.is_valid(): @@ -434,10 +617,19 @@ def add_whitelist(request, user, userid): @login_required @can_edit(Whitelist) def edit_whitelist(request, whitelist_instance, **_kwargs): - """ Editer un accès gracieux, temporaire ou permanent. - Need droit cableur - Syntaxe : JJ/MM/AAAA , heure optionnelle, prend effet immédiatement, - raison obligatoire""" + """View for editing a whitelist object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of editing a whitelist on target user, edit_whitelist. + Syntaxe: DD/MM/AAAA, the whitelist takes an immediate effect. + + Parameters: + request (django request): Standard django request. + whitelist: whitelist instance to edit. + + Returns: + Django Whitelist form. + + """ whitelist = WhitelistForm(request.POST or None, instance=whitelist_instance) if whitelist.is_valid(): if whitelist.changed_data: @@ -452,7 +644,18 @@ def edit_whitelist(request, whitelist_instance, **_kwargs): @login_required @can_delete(Whitelist) def del_whitelist(request, whitelist, **_kwargs): - """ Supprime un acces gracieux""" + """View for removing a whitelist object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of deleting a whitelist on target user, del_whitelist. + + Parameters: + request (django request): Standard django request. + whitelist: Whitelist instance to delete. + + Returns: + Django Whitelist form. + + """ if request.method == "POST": whitelist.delete() messages.success(request, _("The whitelist was deleted.")) @@ -468,7 +671,18 @@ def del_whitelist(request, whitelist, **_kwargs): @can_create(EMailAddress) @can_edit(User) def add_emailaddress(request, user, userid): - """ Create a new local email account""" + """View for adding an emailaddress object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of adding an emailaddress on target user. + + Parameters: + request (django request): Standard django request. + user: User instance to add an emailaddress. + + Returns: + Django EmailAddress form. + + """ emailaddress_instance = EMailAddress(user=user) emailaddress = EMailAddressForm( request.POST or None, instance=emailaddress_instance @@ -487,7 +701,18 @@ def add_emailaddress(request, user, userid): @login_required @can_edit(EMailAddress) def edit_emailaddress(request, emailaddress_instance, **_kwargs): - """ Edit a local email account""" + """View for edit an emailaddress object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of editing an emailaddress on target user. + + Parameters: + request (django request): Standard django request. + emailaddress: Emailaddress to edit. + + Returns: + Django EmailAddress form. + + """ emailaddress = EMailAddressForm( request.POST or None, instance=emailaddress_instance ) @@ -510,7 +735,18 @@ def edit_emailaddress(request, emailaddress_instance, **_kwargs): @login_required @can_delete(EMailAddress) def del_emailaddress(request, emailaddress, **_kwargs): - """Delete a local email account""" + """View for deleting an emailaddress object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of deleting an emailaddress on target user. + + Parameters: + request (django request): Standard django request. + emailaddress: Emailaddress to delete. + + Returns: + Django EmailAddress form. + + """ if request.method == "POST": emailaddress.delete() messages.success(request, _("The local email account was deleted.")) @@ -527,7 +763,18 @@ def del_emailaddress(request, emailaddress, **_kwargs): @login_required @can_edit(User) def edit_email_settings(request, user_instance, **_kwargs): - """Edit the email settings of a user""" + """View for editing User's emailaddress settings for user instance. + Perform an acl check on editing user, and check if editing user + has permission of editing email settings on target user. + + Parameters: + request (django request): Standard django request. + user: User instance to edit email settings. + + Returns: + Django User form. + + """ email_settings = EmailSettingsForm( request.POST or None, instance=user_instance, user=request.user ) @@ -559,8 +806,17 @@ def edit_email_settings(request, user_instance, **_kwargs): @login_required @can_create(School) def add_school(request): - """ Ajouter un établissement d'enseignement à la base de donnée, - need cableur""" + """View for adding a new school object. + Perform an acl check on editing user, and check if editing user + has permission of adding a new school, add_school. + + Parameters: + request (django request): Standard django request. + + Returns: + Django School form. + + """ school = SchoolForm(request.POST or None) if school.is_valid(): school.save() @@ -574,8 +830,18 @@ def add_school(request): @login_required @can_edit(School) def edit_school(request, school_instance, **_kwargs): - """ Editer un établissement d'enseignement à partir du schoolid dans - la base de donnée, need cableur""" + """View for editing a school instance object. + Perform an acl check on editing user, and check if editing user + has permission of editing a school, edit_school. + + Parameters: + request (django request): Standard django request. + school_instance: school instance to edit. + + Returns: + Django School form. + + """ school = SchoolForm(request.POST or None, instance=school_instance) if school.is_valid(): if school.changed_data: @@ -590,10 +856,20 @@ def edit_school(request, school_instance, **_kwargs): @login_required @can_delete_set(School) def del_school(request, instances): - """ Supprimer un établissement d'enseignement à la base de donnée, - need cableur - Objet protégé, possible seulement si aucun user n'est affecté à - l'établissement """ + """View for deleting a school instance object. + Perform an acl check on editing user, and check if editing user + has permission of deleting a school, del_school. + A school can be deleted only if it is not assigned to a user (mode + protect). + + Parameters: + request (django request): Standard django request. + school_instance: school instance to delete. + + Returns: + Django School form. + + """ school = DelSchoolForm(request.POST or None, instances=instances) if school.is_valid(): school_dels = school.cleaned_data["schools"] @@ -619,7 +895,17 @@ def del_school(request, instances): @login_required @can_create(ListShell) def add_shell(request): - """ Ajouter un shell à la base de donnée""" + """View for adding a new linux shell object. + Perform an acl check on editing user, and check if editing user + has permission of adding a new shell, add_school. + + Parameters: + request (django request): Standard django request. + + Returns: + Django Shell form. + + """ shell = ShellForm(request.POST or None) if shell.is_valid(): shell.save() @@ -633,7 +919,18 @@ def add_shell(request): @login_required @can_edit(ListShell) def edit_shell(request, shell_instance, **_kwargs): - """ Editer un shell à partir du listshellid""" + """View for editing a shell instance object. + Perform an acl check on editing user, and check if editing user + has permission of editing a shell, edit_shell. + + Parameters: + request (django request): Standard django request. + shell_instance: shell instance to edit. + + Returns: + Django Shell form. + + """ shell = ShellForm(request.POST or None, instance=shell_instance) if shell.is_valid(): if shell.changed_data: @@ -648,7 +945,20 @@ def edit_shell(request, shell_instance, **_kwargs): @login_required @can_delete(ListShell) def del_shell(request, shell, **_kwargs): - """Destruction d'un shell""" + """View for deleting a shell instance object. + Perform an acl check on editing user, and check if editing user + has permission of deleting a shell, del_shell. + A shell can be deleted only if it is not assigned to a user (mode + protect). + + Parameters: + request (django request): Standard django request. + shell_instance: shell instance to delete. + + Returns: + Django Shell form. + + """ if request.method == "POST": shell.delete() messages.success(request, _("The shell was deleted.")) @@ -661,8 +971,18 @@ def del_shell(request, shell, **_kwargs): @login_required @can_create(ListRight) def add_listright(request): - """ Ajouter un droit/groupe, nécessite droit bureau. - Obligation de fournir un gid pour la synchro ldap, unique """ + """View for adding a new group of rights and users (listright linked to groups) + object for user instance. + Perform an acl check on editing user, and check if editing user + has permission of adding a new listright. + + Parameters: + request (django request): Standard django request. + + Returns: + Django ListRight form. + + """ listright = NewListRightForm(request.POST or None) if listright.is_valid(): listright.save() @@ -678,8 +998,18 @@ def add_listright(request): @login_required @can_edit(ListRight) def edit_listright(request, listright_instance, **_kwargs): - """ Editer un groupe/droit, necessite droit bureau, - à partir du listright id """ + """View for editing a listright instance object. + Perform an acl check on editing user, and check if editing user + has permission of editing a listright, edit_listright. + + Parameters: + request (django request): Standard django request. + listright_instance: listright instance to edit. + + Returns: + Django ListRight form. + + """ listright_form = ListRightForm(request.POST or None, instance=listright_instance) if listright_form.is_valid(): if listright_form.changed_data: @@ -701,8 +1031,20 @@ def edit_listright(request, listright_instance, **_kwargs): @login_required @can_delete_set(ListRight) def del_listright(request, instances): - """ Supprimer un ou plusieurs groupe, possible si il est vide, need droit - bureau """ + """View for deleting a listright instance object. + Perform an acl check on editing user, and check if editing user + has permission of deleting a listright, del_listright. + A listright/group can be deleted only if it is empty (mode + protect). + + Parameters: + request (django request): Standard django request. + listright_instance: listright instance to delete. + + Returns: + Django ListRight form. + + """ listright = DelListRightForm(request.POST or None, instances=instances) if listright.is_valid(): listright_dels = listright.cleaned_data["listrights"] @@ -729,7 +1071,17 @@ def del_listright(request, instances): @can_view_all(User) @can_change(User, "state") def mass_archive(request): - """ Permet l'archivage massif""" + """View for performing a mass archive operation. + Check if editing User has the acl for globaly changing "State" + flag on users, and can edit all the users. + + Parameters: + request (django request): Standard django request. + + Returns: + Django User form. + + """ pagination_number = GeneralOption.get_cached_value("pagination_number") to_archive_form = MassArchiveForm(request.POST or None) to_archive_list = [] @@ -764,7 +1116,16 @@ def mass_archive(request): @login_required @can_view_all(Adherent) def index(request): - """ Affiche l'ensemble des adherents, need droit cableur """ + """View for displaying the paginated list of all users/adherents in re2o. + Need the global acl for viewing all users, can_view_all. + + Parameters: + request (django request): Standard django request. + + Returns: + Django Adherent Form. + + """ pagination_number = GeneralOption.get_cached_value("pagination_number") users_list = Adherent.objects.select_related("room") users_list = SortTable.sort( @@ -780,7 +1141,16 @@ def index(request): @login_required @can_view_all(Club) def index_clubs(request): - """ Affiche l'ensemble des clubs, need droit cableur """ + """View for displaying the paginated list of all users/clubs in re2o. + Need the global acl for viewing all users, can_view_all. + + Parameters: + request (django request): Standard django request. + + Returns: + Django Adherent Form. + + """ pagination_number = GeneralOption.get_cached_value("pagination_number") clubs_list = Club.objects.select_related("room") clubs_list = SortTable.sort( @@ -796,7 +1166,16 @@ def index_clubs(request): @login_required @can_view_all(Ban) def index_ban(request): - """ Affiche l'ensemble des ban, need droit cableur """ + """View for displaying the paginated list of all bans in re2o. + Need the global acl for viewing all bans, can_view_all. + + Parameters: + request (django request): Standard django request. + + Returns: + Django Ban Form. + + """ pagination_number = GeneralOption.get_cached_value("pagination_number") ban_list = Ban.objects.select_related("user") ban_list = SortTable.sort( @@ -812,7 +1191,16 @@ def index_ban(request): @login_required @can_view_all(Whitelist) def index_white(request): - """ Affiche l'ensemble des whitelist, need droit cableur """ + """View for displaying the paginated list of all whitelists in re2o. + Need the global acl for viewing all whitelists, can_view_all. + + Parameters: + request (django request): Standard django request. + + Returns: + Django Whitelist Form. + + """ pagination_number = GeneralOption.get_cached_value("pagination_number") white_list = Whitelist.objects.select_related("user") white_list = SortTable.sort( @@ -828,7 +1216,16 @@ def index_white(request): @login_required @can_view_all(School) def index_school(request): - """ Affiche l'ensemble des établissement""" + """View for displaying the paginated list of all schools in re2o. + Need the global acl for viewing all schools, can_view_all. + + Parameters: + request (django request): Standard django request. + + Returns: + Django School Form. + + """ school_list = School.objects.order_by("name") pagination_number = GeneralOption.get_cached_value("pagination_number") school_list = SortTable.sort( @@ -844,7 +1241,16 @@ def index_school(request): @login_required @can_view_all(ListShell) def index_shell(request): - """ Affiche l'ensemble des shells""" + """View for displaying the paginated list of all shells in re2o. + Need the global acl for viewing all shells, can_view_all. + + Parameters: + request (django request): Standard django request. + + Returns: + Django Shell Form. + + """ shell_list = ListShell.objects.order_by("shell") return render(request, "users/index_shell.html", {"shell_list": shell_list}) @@ -852,7 +1258,17 @@ def index_shell(request): @login_required @can_view_all(ListRight) def index_listright(request): - """ Affiche l'ensemble des droits""" + """View for displaying the listrights/groups list in re2o. + The listrights are sorted by members users, and individual + acl for a complete display. + + Parameters: + request (django request): Standard django request. + + Returns: + Django ListRight Form. + + """ rights = {} for right in ( ListRight.objects.order_by("name") @@ -875,7 +1291,17 @@ def index_listright(request): @login_required @can_view_all(ServiceUser) def index_serviceusers(request): - """ Affiche les users de services (pour les accès ldap)""" + """View for displaying the paginated list of all serviceusers in re2o + See ServiceUser model for more informations on service users. + Need the global acl for viewing all serviceusers, can_view_all. + + Parameters: + request (django request): Standard django request. + + Returns: + Django ServiceUser Form. + + """ serviceusers_list = ServiceUser.objects.order_by("pseudo") return render( request, @@ -886,14 +1312,42 @@ def index_serviceusers(request): @login_required def mon_profil(request): - """ Lien vers profil, renvoie request.id à la fonction """ + """Shortcuts view to profil view, with correct arguments. + Returns the view profil with users argument, users is set to + default request.user. + + Parameters: + request (django request): Standard django request. + + Returns: + Django User Profil Form. + + """ return redirect(reverse("users:profil", kwargs={"userid": str(request.user.id)})) @login_required @can_view(User) def profil(request, users, **_kwargs): - """ Affiche un profil, self or cableur, prend un userid en argument """ + """Profil view. Display informations on users, the single user. + Informations displayed are: + * Adherent or Club User instance informations + * Interface/Machine belonging to User instance + * Invoice belonging to User instance + * Ban instances belonging to User + * Whitelists instances belonging to User + * Email Settings of User instance + * Tickets belonging to User instance. + Requires the acl can_view on user instance. + + Parameters: + request (django request): Standard django request. + users: User instance to display profil + + Returns: + Django User Profil Form. + + """ machines = ( Machine.objects.filter(user=users) .select_related("user") @@ -969,7 +1423,17 @@ def profil(request, users, **_kwargs): def reset_password(request): - """ Reintialisation du mot de passe si mdp oublié """ + """Reset password form, linked to form forgotten password. + If an user is found, send an email to him with a link + to reset its password. + + Parameters: + request (django request): Standard django request. + + Returns: + Django ResetPassword Form. + + """ userform = ResetPasswordForm(request.POST or None) if userform.is_valid(): try: @@ -994,8 +1458,17 @@ def reset_password(request): def process(request, token): - """Process, lien pour la reinitialisation du mot de passe - et la confirmation de l'email""" + """Process view, in case of both reset password, or confirm email in case + of new email set. + This view calls process_passwd or process_email. + + Parameters: + request (django request): Standard django request. + + Returns: + Correct Django process Form. + + """ valid_reqs = Request.objects.filter(expires_at__gt=timezone.now()) req = get_object_or_404(valid_reqs, token=token) @@ -1009,8 +1482,16 @@ def process(request, token): def process_passwd(request, req): - """Process le changeemnt de mot de passe, renvoie le formulaire - demandant le nouveau password""" + """Process view, in case of reset password by email. Returns + a form to change and reset the password. + + Parameters: + request (django request): Standard django request. + + Returns: + Correct Django process password Form. + + """ user = req.user u_form = PassForm(request.POST or None, instance=user, user=request.user) if u_form.is_valid(): @@ -1031,8 +1512,17 @@ def process_passwd(request, req): def process_email(request, req): - """Process la confirmation de mail, renvoie le formulaire - de validation""" + """Process view, in case of confirm a new email. Returns + a form to notify the success of the email confirmation to + request.User. + + Parameters: + request (django request): Standard django request. + + Returns: + Correct Django process email Form. + + """ user = req.user if request.method == "POST": with transaction.atomic(), reversion.create_revision(): @@ -1055,7 +1545,16 @@ def process_email(request, req): @login_required @can_edit(User) def resend_confirmation_email(request, logged_user, userid): - """ Renvoi du mail de confirmation """ + """View to resend confirm email, for adding a new email. + Check if User has the correct acl. + + Parameters: + request (django request): Standard django request. + + Returns: + Correct Django resend email Form. + + """ try: user = User.objects.get( id=userid, @@ -1074,6 +1573,20 @@ def resend_confirmation_email(request, logged_user, userid): @login_required def initial_register(request): + """View to register both a new room, and a new interface/machine for a user. + This view is used with switchs function of redirect web after AAA authentication + failed. Then, the users log-in, and the new mac-address and switch port, in order to + get the room, are included in HTTP Headers by the switch redirection functionnality. + This allow to add the new interface with the correct mac-address, and confirm if needed, + the new room of request.user. + + Parameters: + request (django request): Standard django request. + + Returns: + Initial room and interface/machine register Form. + + """ switch_ip = request.GET.get("switch_ip", None) switch_port = request.GET.get("switch_port", None) client_mac = request.GET.get("client_mac", None)