mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-27 07:02:26 +00:00
First step of docstring translation
This commit is contained in:
parent
87022a7b04
commit
02678b7ccd
9 changed files with 300 additions and 75 deletions
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 _
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
|
|
166
re2o/utils.py
166
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:
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in a new issue