We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
S'inscrire avec Facebook crée un pseudo sans les lettres accentuées
Si je me m'inscrit à partir de mon compte Facebook dont le nom est « Clémentine Sanpépins », mon pseudo sur Zeste de Savoir sera « ClmentineSanppins ».

C'est le cas en local sur `upstream/dev` à la création de ce ticket. Je ne sais pas si c'est le cas en bêta et en production.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `zds/settings/abstract_base/requirements.py`
Content:
```
1 from .config import config
2 
3 # best quality, 100 is the same but documentation says
4 # ' values up to 100 are allowed, but this is not recommended'
5 # so let's use 95
6 THUMBNAIL_QUALITY = 95
7 # Let's use the default value BUT if we want to let png in lossless format, we have tu use (png,) instead of None
8 THUMBNAIL_PRESERVE_EXTENSIONS = ("svg",)
9 
10 
11 social_auth_config = config.get("social_auth", {})
12 
13 SOCIAL_AUTH_RAISE_EXCEPTIONS = False
14 
15 SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"]
16 
17 SOCIAL_AUTH_PIPELINE = (
18     "social_core.pipeline.social_auth.social_details",
19     "social_core.pipeline.social_auth.social_uid",
20     "social_core.pipeline.social_auth.auth_allowed",
21     "social_core.pipeline.social_auth.social_user",
22     "social_core.pipeline.user.get_username",
23     "social_core.pipeline.social_auth.associate_by_email",
24     "social_core.pipeline.user.create_user",
25     "zds.member.models.save_profile",
26     "social_core.pipeline.social_auth.associate_user",
27     "social_core.pipeline.social_auth.load_extra_data",
28     "social_core.pipeline.user.user_details",
29 )
30 
31 # Before adding new providers such as Facebook and Google,
32 # you need to make sure they validate the user's email address on sign up!
33 # If they don't, a malicious person could take control of someone else account!
34 SOCIAL_AUTH_FACEBOOK_KEY = social_auth_config.get("facebook_key", "")
35 SOCIAL_AUTH_FACEBOOK_SECRET = social_auth_config.get("facebook_secret", "")
36 SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = social_auth_config.get(
37     "google_oauth2_key",
38     "696570367703-r6hc7mdd27t1sktdkivpnc5b25i0uip2.apps.googleusercontent.com",
39 )
40 SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = social_auth_config.get(
41     "google_oauth2_secret",
42     "mApWNh3stCsYHwsGuWdbZWP8",
43 )
44 
45 SOCIAL_AUTH_SANITIZE_REDIRECTS = social_auth_config.get(
46     "sanitize_redirects",
47     False,
48 )
49 
50 
51 recaptcha_config = config.get("recaptcha", {})
52 
53 USE_CAPTCHA = recaptcha_config.get("use_captcha", False)
54 RECAPTCHA_PUBLIC_KEY = recaptcha_config.get("public_key", "dummy")
55 RECAPTCHA_PRIVATE_KEY = recaptcha_config.get("private_key", "dummy")
56 
57 
58 OAUTH2_PROVIDER = {"OAUTH2_BACKEND_CLASS": "oauth2_provider.oauth2_backends.JSONOAuthLibCore"}
59
```

Path: `zds/utils/misc.py`
Content:
```
1 import hashlib
2 import re
3 
4 from django.contrib.auth import get_user_model
5 from django.http import HttpRequest
6 
7 THUMB_MAX_WIDTH = 80
8 THUMB_MAX_HEIGHT = 80
9 
10 MEDIUM_MAX_WIDTH = 200
11 MEDIUM_MAX_HEIGHT = 200
12 
13 
14 def compute_hash(filenames):
15     """returns a md5 hexdigest of group of files to check if they have change"""
16     md5_hash = hashlib.md5()
17     for filename in filenames:
18         if filename:
19             file_handle = open(filename, "rb")
20             must_continue = True
21             while must_continue:
22                 read_bytes = file_handle.read(8096)
23                 if not read_bytes:
24                     must_continue = False
25                 else:
26                     md5_hash.update(read_bytes)
27     return md5_hash.hexdigest()
28 
29 
30 def content_has_changed(filenames, md5):
31     return md5 != compute_hash(filenames)
32 
33 
34 def has_changed(instance, field, manager="objects"):
35     """Returns true if a field has changed in a model May be used in a
36     model.save() method."""
37     if not instance.pk:
38         return True
39     manager = getattr(instance.__class__, manager)
40     old = getattr(manager.get(pk=instance.pk), field)
41     return not getattr(instance, field) == old
42 
43 
44 def convert_camel_to_underscore(camel_case):
45     """
46     Converts a name in camel case to underscore.
47     """
48     s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", camel_case)
49     return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
50 
51 
52 def contains_utf8mb4(s):
53     """
54     This string contains at least one character of more than 3 bytes
55     """
56     if not isinstance(s, str):
57         s = str(s, "utf-8")
58     re_pattern = re.compile("[^\u0000-\uD7FF\uE000-\uFFFF]", re.UNICODE)
59     return s != re_pattern.sub("\uFFFD", s)
60 
61 
62 def check_essential_accounts():
63     """
64     Verify that essential accounts are present in the database.
65     Raise an exception if it is not the case.
66     """
67 
68     from django.conf import settings
69 
70     User = get_user_model()
71     essential_accounts = ("bot_account", "anonymous_account", "external_account")
72 
73     for account in essential_accounts:
74         username = settings.ZDS_APP["member"][account]
75         try:
76             User.objects.get(username=username)
77         except User.DoesNotExist:
78             raise Exception(
79                 f"User {username!r} does not exist. You must create it to run the server. "
80                 f"On a development instance, load the fixtures to solve this issue."
81             )
82 
83 
84 def is_ajax(request: HttpRequest):
85     """
86     Check whether the request was sent asynchronously.
87 
88     The function returns True for :
89 
90     * requests sent using jQuery.ajax() since it sets the header `X-Requested-With`
91       to `XMLHttpRequest` by default ;
92     * requests sent using the tools provided by `ajax.js`, which reproduce the behavior
93       described above to ease the progressive removal of jQuery from the codebase.
94 
95     The function returns False for requests without the appropriate header.
96     These requests will not be recognized as AJAX.
97 
98     The function replaces `request.is_ajax()`, which is removed starting from Django 4.0.
99     """
100     return request.META.get("HTTP_X_REQUESTED_WITH") == "XMLHttpRequest"
101
```

Path: `zds/member/validators.py`
Content:
```
1 from django.contrib.auth.models import User
2 from django.core.exceptions import ValidationError
3 from django.core.validators import EmailValidator, ProhibitNullCharactersValidator
4 from django.utils.encoding import force_str
5 from django.utils.translation import gettext_lazy as _
6 
7 from zds.utils.misc import contains_utf8mb4
8 from zds.member.models import BannedEmailProvider, Profile
9 
10 
11 def validate_not_empty(value):
12     """
13     Fields cannot be empty or only contain spaces.
14 
15     :param value: value to validate (str or None)
16     :return:
17     """
18     if value is None or not value.strip():
19         raise ValidationError(_("Le champs ne peut être vide"))
20 
21 
22 class ZdSEmailValidator(EmailValidator):
23     """
24     Based on https://docs.djangoproject.com/en/1.8/_modules/django/core/validators/#EmailValidator
25     Changed :
26       - check if provider is not if blacklisted
27       - check if email is not used by another user
28       - remove whitelist check
29       - add custom errors and translate them into French
30     """
31 
32     message = _("Utilisez une adresse de courriel valide.")
33 
34     def __call__(self, value, check_username_available=True):
35         value = force_str(value)
36 
37         if not value or "@" not in value:
38             raise ValidationError(self.message, code=self.code)
39 
40         user_part, domain_part = value.rsplit("@", 1)
41 
42         if not self.user_regex.match(user_part) or contains_utf8mb4(user_part):
43             raise ValidationError(self.message, code=self.code)
44 
45         # check if provider is blacklisted
46         blacklist = BannedEmailProvider.objects.values_list("provider", flat=True)
47         for provider in blacklist:
48             if f"@{provider}" in value.lower():
49                 raise ValidationError(_("Ce fournisseur ne peut pas être utilisé."), code=self.code)
50 
51         # check if email is used by another user
52         user_count = User.objects.filter(email=value).count()
53         if check_username_available and user_count > 0:
54             raise ValidationError(_("Cette adresse courriel est déjà utilisée"), code=self.code)
55         # check if email exists in database
56         elif not check_username_available and user_count == 0:
57             raise ValidationError(_("Cette adresse courriel n'existe pas"), code=self.code)
58 
59         if domain_part and not self.validate_domain_part(domain_part):
60             # Try for possible IDN domain-part
61             try:
62                 domain_part = domain_part.encode("idna").decode("ascii")
63                 if self.validate_domain_part(domain_part):
64                     return
65             except UnicodeError:
66                 pass
67             raise ValidationError(self.message, code=self.code)
68 
69 
70 validate_zds_email = ZdSEmailValidator()
71 
72 
73 def validate_zds_username(value, check_username_available=True):
74     """
75     Check if username is used by another user
76 
77     :param value: value to validate (str or None)
78     :return:
79     """
80 
81     # If the character \x00 is in the username, the homoglyphs library called
82     # in Profile.find_username_skeleton() will raise a ValueError (the bug has
83     # been reported: https://github.com/yamatt/homoglyphs/issues/6). To prevent
84     # this, we call this validator which will raise a ValidationError if \x00 is
85     # in the username.
86     ProhibitNullCharactersValidator()(value)
87 
88     msg = None
89     user_count = User.objects.filter(username=value).count()
90     skeleton_user_count = Profile.objects.filter(username_skeleton=Profile.find_username_skeleton(value)).count()
91     if "," in value:
92         msg = _("Le nom d'utilisateur ne peut contenir de virgules")
93     elif "/" in value:
94         msg = _("Le nom d'utilisateur ne peut contenir de barres obliques")
95     elif contains_utf8mb4(value):
96         msg = _("Le nom d'utilisateur ne peut pas contenir des caractères utf8mb4")
97     elif check_username_available and user_count > 0:
98         msg = _("Ce nom d'utilisateur est déjà utilisé")
99     elif check_username_available and skeleton_user_count > 0:
100         msg = _("Un nom d'utilisateur visuellement proche du votre existe déjà")
101     elif not check_username_available and user_count == 0:
102         msg = _("Ce nom d'utilisateur n'existe pas")
103     if msg is not None:
104         raise ValidationError(msg)
105 
106 
107 def validate_raw_zds_username(data):
108     """
109     Check if raw username hasn't space on left or right
110     """
111     msg = None
112     username = data.get("username", None)
113     if username is None:
114         msg = _("Le nom d'utilisateur n'est pas fourni")
115     elif username != username.strip():
116         msg = _("Le nom d'utilisateur ne peut commencer ou finir par des espaces")
117 
118     if msg is not None:
119         raise ValidationError(msg)
120 
121 
122 def validate_zds_password(value):
123     """
124 
125     :param value:
126     :return:
127     """
128     if contains_utf8mb4(value):
129         raise ValidationError(_("Le mot de passe ne peut pas contenir des caractères utf8mb4"))
130 
131 
132 def validate_passwords(
133     cleaned_data, password_label="password", password_confirm_label="password_confirm", username=None
134 ):
135     """
136     Chek if cleaned_data['password'] == cleaned_data['password_confirm'] and password is not username.
137     :param cleaned_data:
138     :param password_label:
139     :param password_confirm_label:
140     :return:
141     """
142 
143     password = cleaned_data.get(password_label)
144     password_confirm = cleaned_data.get(password_confirm_label)
145     msg = None
146 
147     if username is None:
148         username = cleaned_data.get("username")
149 
150     if not password_confirm == password:
151         msg = _("Les mots de passe sont différents")
152 
153         if password_label in cleaned_data:
154             del cleaned_data[password_label]
155 
156         if password_confirm_label in cleaned_data:
157             del cleaned_data[password_confirm_label]
158 
159     if username is not None:
160         # Check that password != username
161         if password == username:
162             msg = _("Le mot de passe doit être différent du pseudo")
163             if password_label in cleaned_data:
164                 del cleaned_data[password_label]
165             if password_confirm_label in cleaned_data:
166                 del cleaned_data[password_confirm_label]
167 
168     if msg is not None:
169         raise ValidationError(msg)
170 
171     return cleaned_data
172
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/zds/member/validators.py b/zds/member/validators.py
--- a/zds/member/validators.py
+++ b/zds/member/validators.py
@@ -4,7 +4,7 @@
 from django.utils.encoding import force_str
 from django.utils.translation import gettext_lazy as _
 
-from zds.utils.misc import contains_utf8mb4
+from zds.utils.misc import contains_utf8mb4, remove_utf8mb4
 from zds.member.models import BannedEmailProvider, Profile
 
 
@@ -70,6 +70,15 @@
 validate_zds_email = ZdSEmailValidator()
 
 
+def clean_username_social_auth(username):
+    """
+    Clean username of accounts created using social auth.
+    """
+    # These three conditions are the same as the first three in the "validate_zds_username" function below.
+    # If you modify one of them here, make sure you do the same there!
+    return remove_utf8mb4(username).replace(",", "").replace("/", "")
+
+
 def validate_zds_username(value, check_username_available=True):
     """
     Check if username is used by another user
@@ -88,6 +97,9 @@
     msg = None
     user_count = User.objects.filter(username=value).count()
     skeleton_user_count = Profile.objects.filter(username_skeleton=Profile.find_username_skeleton(value)).count()
+
+    # These first three conditions are the same as those in the "clean_username_social_auth" function above.
+    # If you modify one of them here, make sure you do the same there!
     if "," in value:
         msg = _("Le nom d'utilisateur ne peut contenir de virgules")
     elif "/" in value:
diff --git a/zds/settings/abstract_base/requirements.py b/zds/settings/abstract_base/requirements.py
--- a/zds/settings/abstract_base/requirements.py
+++ b/zds/settings/abstract_base/requirements.py
@@ -10,9 +10,12 @@
 
 social_auth_config = config.get("social_auth", {})
 
+SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = "zds.member.validators.clean_username_social_auth"
+
 SOCIAL_AUTH_RAISE_EXCEPTIONS = False
 
 SOCIAL_AUTH_FACEBOOK_SCOPE = ["email"]
+SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {"fields": "name,email"}
 
 SOCIAL_AUTH_PIPELINE = (
     "social_core.pipeline.social_auth.social_details",
diff --git a/zds/utils/misc.py b/zds/utils/misc.py
--- a/zds/utils/misc.py
+++ b/zds/utils/misc.py
@@ -49,14 +49,21 @@
     return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
 
 
-def contains_utf8mb4(s):
+def remove_utf8mb4(s):
     """
-    This string contains at least one character of more than 3 bytes
+    Remove characters of more than 3 bytes.
     """
     if not isinstance(s, str):
         s = str(s, "utf-8")
     re_pattern = re.compile("[^\u0000-\uD7FF\uE000-\uFFFF]", re.UNICODE)
-    return s != re_pattern.sub("\uFFFD", s)
+    return re_pattern.sub("", s)
+
+
+def contains_utf8mb4(s):
+    """
+    Check if this string contains at least one character of more than 3 bytes.
+    """
+    return s != remove_utf8mb4(s)
 
 
 def check_essential_accounts():

{"golden_diff": "diff --git a/zds/member/validators.py b/zds/member/validators.py\n--- a/zds/member/validators.py\n+++ b/zds/member/validators.py\n@@ -4,7 +4,7 @@\n from django.utils.encoding import force_str\n from django.utils.translation import gettext_lazy as _\n \n-from zds.utils.misc import contains_utf8mb4\n+from zds.utils.misc import contains_utf8mb4, remove_utf8mb4\n from zds.member.models import BannedEmailProvider, Profile\n \n \n@@ -70,6 +70,15 @@\n validate_zds_email = ZdSEmailValidator()\n \n \n+def clean_username_social_auth(username):\n+    \"\"\"\n+    Clean username of accounts created using social auth.\n+    \"\"\"\n+    # These three conditions are the same as the first three in the \"validate_zds_username\" function below.\n+    # If you modify one of them here, make sure you do the same there!\n+    return remove_utf8mb4(username).replace(\",\", \"\").replace(\"/\", \"\")\n+\n+\n def validate_zds_username(value, check_username_available=True):\n     \"\"\"\n     Check if username is used by another user\n@@ -88,6 +97,9 @@\n     msg = None\n     user_count = User.objects.filter(username=value).count()\n     skeleton_user_count = Profile.objects.filter(username_skeleton=Profile.find_username_skeleton(value)).count()\n+\n+    # These first three conditions are the same as those in the \"clean_username_social_auth\" function above.\n+    # If you modify one of them here, make sure you do the same there!\n     if \",\" in value:\n         msg = _(\"Le nom d'utilisateur ne peut contenir de virgules\")\n     elif \"/\" in value:\ndiff --git a/zds/settings/abstract_base/requirements.py b/zds/settings/abstract_base/requirements.py\n--- a/zds/settings/abstract_base/requirements.py\n+++ b/zds/settings/abstract_base/requirements.py\n@@ -10,9 +10,12 @@\n \n social_auth_config = config.get(\"social_auth\", {})\n \n+SOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = \"zds.member.validators.clean_username_social_auth\"\n+\n SOCIAL_AUTH_RAISE_EXCEPTIONS = False\n \n SOCIAL_AUTH_FACEBOOK_SCOPE = [\"email\"]\n+SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {\"fields\": \"name,email\"}\n \n SOCIAL_AUTH_PIPELINE = (\n     \"social_core.pipeline.social_auth.social_details\",\ndiff --git a/zds/utils/misc.py b/zds/utils/misc.py\n--- a/zds/utils/misc.py\n+++ b/zds/utils/misc.py\n@@ -49,14 +49,21 @@\n     return re.sub(\"([a-z0-9])([A-Z])\", r\"\\1_\\2\", s1).lower()\n \n \n-def contains_utf8mb4(s):\n+def remove_utf8mb4(s):\n     \"\"\"\n-    This string contains at least one character of more than 3 bytes\n+    Remove characters of more than 3 bytes.\n     \"\"\"\n     if not isinstance(s, str):\n         s = str(s, \"utf-8\")\n     re_pattern = re.compile(\"[^\\u0000-\\uD7FF\\uE000-\\uFFFF]\", re.UNICODE)\n-    return s != re_pattern.sub(\"\\uFFFD\", s)\n+    return re_pattern.sub(\"\", s)\n+\n+\n+def contains_utf8mb4(s):\n+    \"\"\"\n+    Check if this string contains at least one character of more than 3 bytes.\n+    \"\"\"\n+    return s != remove_utf8mb4(s)\n \n \n def check_essential_accounts():\n", "issue": "S'inscrire avec Facebook cr\u00e9e un pseudo sans les lettres accentu\u00e9es\nSi je me m'inscrit \u00e0 partir de mon compte Facebook dont le nom est \u00ab Cl\u00e9mentine Sanp\u00e9pins \u00bb, mon pseudo sur Zeste de Savoir sera \u00ab ClmentineSanppins \u00bb.\r\n\r\nC'est le cas en local sur `upstream/dev` \u00e0 la cr\u00e9ation de ce ticket. Je ne sais pas si c'est le cas en b\u00eata et en production.\n", "before_files": [{"content": "from .config import config\n\n# best quality, 100 is the same but documentation says\n# ' values up to 100 are allowed, but this is not recommended'\n# so let's use 95\nTHUMBNAIL_QUALITY = 95\n# Let's use the default value BUT if we want to let png in lossless format, we have tu use (png,) instead of None\nTHUMBNAIL_PRESERVE_EXTENSIONS = (\"svg\",)\n\n\nsocial_auth_config = config.get(\"social_auth\", {})\n\nSOCIAL_AUTH_RAISE_EXCEPTIONS = False\n\nSOCIAL_AUTH_FACEBOOK_SCOPE = [\"email\"]\n\nSOCIAL_AUTH_PIPELINE = (\n    \"social_core.pipeline.social_auth.social_details\",\n    \"social_core.pipeline.social_auth.social_uid\",\n    \"social_core.pipeline.social_auth.auth_allowed\",\n    \"social_core.pipeline.social_auth.social_user\",\n    \"social_core.pipeline.user.get_username\",\n    \"social_core.pipeline.social_auth.associate_by_email\",\n    \"social_core.pipeline.user.create_user\",\n    \"zds.member.models.save_profile\",\n    \"social_core.pipeline.social_auth.associate_user\",\n    \"social_core.pipeline.social_auth.load_extra_data\",\n    \"social_core.pipeline.user.user_details\",\n)\n\n# Before adding new providers such as Facebook and Google,\n# you need to make sure they validate the user's email address on sign up!\n# If they don't, a malicious person could take control of someone else account!\nSOCIAL_AUTH_FACEBOOK_KEY = social_auth_config.get(\"facebook_key\", \"\")\nSOCIAL_AUTH_FACEBOOK_SECRET = social_auth_config.get(\"facebook_secret\", \"\")\nSOCIAL_AUTH_GOOGLE_OAUTH2_KEY = social_auth_config.get(\n    \"google_oauth2_key\",\n    \"696570367703-r6hc7mdd27t1sktdkivpnc5b25i0uip2.apps.googleusercontent.com\",\n)\nSOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = social_auth_config.get(\n    \"google_oauth2_secret\",\n    \"mApWNh3stCsYHwsGuWdbZWP8\",\n)\n\nSOCIAL_AUTH_SANITIZE_REDIRECTS = social_auth_config.get(\n    \"sanitize_redirects\",\n    False,\n)\n\n\nrecaptcha_config = config.get(\"recaptcha\", {})\n\nUSE_CAPTCHA = recaptcha_config.get(\"use_captcha\", False)\nRECAPTCHA_PUBLIC_KEY = recaptcha_config.get(\"public_key\", \"dummy\")\nRECAPTCHA_PRIVATE_KEY = recaptcha_config.get(\"private_key\", \"dummy\")\n\n\nOAUTH2_PROVIDER = {\"OAUTH2_BACKEND_CLASS\": \"oauth2_provider.oauth2_backends.JSONOAuthLibCore\"}\n", "path": "zds/settings/abstract_base/requirements.py"}, {"content": "import hashlib\nimport re\n\nfrom django.contrib.auth import get_user_model\nfrom django.http import HttpRequest\n\nTHUMB_MAX_WIDTH = 80\nTHUMB_MAX_HEIGHT = 80\n\nMEDIUM_MAX_WIDTH = 200\nMEDIUM_MAX_HEIGHT = 200\n\n\ndef compute_hash(filenames):\n    \"\"\"returns a md5 hexdigest of group of files to check if they have change\"\"\"\n    md5_hash = hashlib.md5()\n    for filename in filenames:\n        if filename:\n            file_handle = open(filename, \"rb\")\n            must_continue = True\n            while must_continue:\n                read_bytes = file_handle.read(8096)\n                if not read_bytes:\n                    must_continue = False\n                else:\n                    md5_hash.update(read_bytes)\n    return md5_hash.hexdigest()\n\n\ndef content_has_changed(filenames, md5):\n    return md5 != compute_hash(filenames)\n\n\ndef has_changed(instance, field, manager=\"objects\"):\n    \"\"\"Returns true if a field has changed in a model May be used in a\n    model.save() method.\"\"\"\n    if not instance.pk:\n        return True\n    manager = getattr(instance.__class__, manager)\n    old = getattr(manager.get(pk=instance.pk), field)\n    return not getattr(instance, field) == old\n\n\ndef convert_camel_to_underscore(camel_case):\n    \"\"\"\n    Converts a name in camel case to underscore.\n    \"\"\"\n    s1 = re.sub(\"(.)([A-Z][a-z]+)\", r\"\\1_\\2\", camel_case)\n    return re.sub(\"([a-z0-9])([A-Z])\", r\"\\1_\\2\", s1).lower()\n\n\ndef contains_utf8mb4(s):\n    \"\"\"\n    This string contains at least one character of more than 3 bytes\n    \"\"\"\n    if not isinstance(s, str):\n        s = str(s, \"utf-8\")\n    re_pattern = re.compile(\"[^\\u0000-\\uD7FF\\uE000-\\uFFFF]\", re.UNICODE)\n    return s != re_pattern.sub(\"\\uFFFD\", s)\n\n\ndef check_essential_accounts():\n    \"\"\"\n    Verify that essential accounts are present in the database.\n    Raise an exception if it is not the case.\n    \"\"\"\n\n    from django.conf import settings\n\n    User = get_user_model()\n    essential_accounts = (\"bot_account\", \"anonymous_account\", \"external_account\")\n\n    for account in essential_accounts:\n        username = settings.ZDS_APP[\"member\"][account]\n        try:\n            User.objects.get(username=username)\n        except User.DoesNotExist:\n            raise Exception(\n                f\"User {username!r} does not exist. You must create it to run the server. \"\n                f\"On a development instance, load the fixtures to solve this issue.\"\n            )\n\n\ndef is_ajax(request: HttpRequest):\n    \"\"\"\n    Check whether the request was sent asynchronously.\n\n    The function returns True for :\n\n    * requests sent using jQuery.ajax() since it sets the header `X-Requested-With`\n      to `XMLHttpRequest` by default ;\n    * requests sent using the tools provided by `ajax.js`, which reproduce the behavior\n      described above to ease the progressive removal of jQuery from the codebase.\n\n    The function returns False for requests without the appropriate header.\n    These requests will not be recognized as AJAX.\n\n    The function replaces `request.is_ajax()`, which is removed starting from Django 4.0.\n    \"\"\"\n    return request.META.get(\"HTTP_X_REQUESTED_WITH\") == \"XMLHttpRequest\"\n", "path": "zds/utils/misc.py"}, {"content": "from django.contrib.auth.models import User\nfrom django.core.exceptions import ValidationError\nfrom django.core.validators import EmailValidator, ProhibitNullCharactersValidator\nfrom django.utils.encoding import force_str\nfrom django.utils.translation import gettext_lazy as _\n\nfrom zds.utils.misc import contains_utf8mb4\nfrom zds.member.models import BannedEmailProvider, Profile\n\n\ndef validate_not_empty(value):\n    \"\"\"\n    Fields cannot be empty or only contain spaces.\n\n    :param value: value to validate (str or None)\n    :return:\n    \"\"\"\n    if value is None or not value.strip():\n        raise ValidationError(_(\"Le champs ne peut \u00eatre vide\"))\n\n\nclass ZdSEmailValidator(EmailValidator):\n    \"\"\"\n    Based on https://docs.djangoproject.com/en/1.8/_modules/django/core/validators/#EmailValidator\n    Changed :\n      - check if provider is not if blacklisted\n      - check if email is not used by another user\n      - remove whitelist check\n      - add custom errors and translate them into French\n    \"\"\"\n\n    message = _(\"Utilisez une adresse de courriel valide.\")\n\n    def __call__(self, value, check_username_available=True):\n        value = force_str(value)\n\n        if not value or \"@\" not in value:\n            raise ValidationError(self.message, code=self.code)\n\n        user_part, domain_part = value.rsplit(\"@\", 1)\n\n        if not self.user_regex.match(user_part) or contains_utf8mb4(user_part):\n            raise ValidationError(self.message, code=self.code)\n\n        # check if provider is blacklisted\n        blacklist = BannedEmailProvider.objects.values_list(\"provider\", flat=True)\n        for provider in blacklist:\n            if f\"@{provider}\" in value.lower():\n                raise ValidationError(_(\"Ce fournisseur ne peut pas \u00eatre utilis\u00e9.\"), code=self.code)\n\n        # check if email is used by another user\n        user_count = User.objects.filter(email=value).count()\n        if check_username_available and user_count > 0:\n            raise ValidationError(_(\"Cette adresse courriel est d\u00e9j\u00e0 utilis\u00e9e\"), code=self.code)\n        # check if email exists in database\n        elif not check_username_available and user_count == 0:\n            raise ValidationError(_(\"Cette adresse courriel n'existe pas\"), code=self.code)\n\n        if domain_part and not self.validate_domain_part(domain_part):\n            # Try for possible IDN domain-part\n            try:\n                domain_part = domain_part.encode(\"idna\").decode(\"ascii\")\n                if self.validate_domain_part(domain_part):\n                    return\n            except UnicodeError:\n                pass\n            raise ValidationError(self.message, code=self.code)\n\n\nvalidate_zds_email = ZdSEmailValidator()\n\n\ndef validate_zds_username(value, check_username_available=True):\n    \"\"\"\n    Check if username is used by another user\n\n    :param value: value to validate (str or None)\n    :return:\n    \"\"\"\n\n    # If the character \\x00 is in the username, the homoglyphs library called\n    # in Profile.find_username_skeleton() will raise a ValueError (the bug has\n    # been reported: https://github.com/yamatt/homoglyphs/issues/6). To prevent\n    # this, we call this validator which will raise a ValidationError if \\x00 is\n    # in the username.\n    ProhibitNullCharactersValidator()(value)\n\n    msg = None\n    user_count = User.objects.filter(username=value).count()\n    skeleton_user_count = Profile.objects.filter(username_skeleton=Profile.find_username_skeleton(value)).count()\n    if \",\" in value:\n        msg = _(\"Le nom d'utilisateur ne peut contenir de virgules\")\n    elif \"/\" in value:\n        msg = _(\"Le nom d'utilisateur ne peut contenir de barres obliques\")\n    elif contains_utf8mb4(value):\n        msg = _(\"Le nom d'utilisateur ne peut pas contenir des caract\u00e8res utf8mb4\")\n    elif check_username_available and user_count > 0:\n        msg = _(\"Ce nom d'utilisateur est d\u00e9j\u00e0 utilis\u00e9\")\n    elif check_username_available and skeleton_user_count > 0:\n        msg = _(\"Un nom d'utilisateur visuellement proche du votre existe d\u00e9j\u00e0\")\n    elif not check_username_available and user_count == 0:\n        msg = _(\"Ce nom d'utilisateur n'existe pas\")\n    if msg is not None:\n        raise ValidationError(msg)\n\n\ndef validate_raw_zds_username(data):\n    \"\"\"\n    Check if raw username hasn't space on left or right\n    \"\"\"\n    msg = None\n    username = data.get(\"username\", None)\n    if username is None:\n        msg = _(\"Le nom d'utilisateur n'est pas fourni\")\n    elif username != username.strip():\n        msg = _(\"Le nom d'utilisateur ne peut commencer ou finir par des espaces\")\n\n    if msg is not None:\n        raise ValidationError(msg)\n\n\ndef validate_zds_password(value):\n    \"\"\"\n\n    :param value:\n    :return:\n    \"\"\"\n    if contains_utf8mb4(value):\n        raise ValidationError(_(\"Le mot de passe ne peut pas contenir des caract\u00e8res utf8mb4\"))\n\n\ndef validate_passwords(\n    cleaned_data, password_label=\"password\", password_confirm_label=\"password_confirm\", username=None\n):\n    \"\"\"\n    Chek if cleaned_data['password'] == cleaned_data['password_confirm'] and password is not username.\n    :param cleaned_data:\n    :param password_label:\n    :param password_confirm_label:\n    :return:\n    \"\"\"\n\n    password = cleaned_data.get(password_label)\n    password_confirm = cleaned_data.get(password_confirm_label)\n    msg = None\n\n    if username is None:\n        username = cleaned_data.get(\"username\")\n\n    if not password_confirm == password:\n        msg = _(\"Les mots de passe sont diff\u00e9rents\")\n\n        if password_label in cleaned_data:\n            del cleaned_data[password_label]\n\n        if password_confirm_label in cleaned_data:\n            del cleaned_data[password_confirm_label]\n\n    if username is not None:\n        # Check that password != username\n        if password == username:\n            msg = _(\"Le mot de passe doit \u00eatre diff\u00e9rent du pseudo\")\n            if password_label in cleaned_data:\n                del cleaned_data[password_label]\n            if password_confirm_label in cleaned_data:\n                del cleaned_data[password_confirm_label]\n\n    if msg is not None:\n        raise ValidationError(msg)\n\n    return cleaned_data\n", "path": "zds/member/validators.py"}], "after_files": [{"content": "from .config import config\n\n# best quality, 100 is the same but documentation says\n# ' values up to 100 are allowed, but this is not recommended'\n# so let's use 95\nTHUMBNAIL_QUALITY = 95\n# Let's use the default value BUT if we want to let png in lossless format, we have tu use (png,) instead of None\nTHUMBNAIL_PRESERVE_EXTENSIONS = (\"svg\",)\n\n\nsocial_auth_config = config.get(\"social_auth\", {})\n\nSOCIAL_AUTH_CLEAN_USERNAME_FUNCTION = \"zds.member.validators.clean_username_social_auth\"\n\nSOCIAL_AUTH_RAISE_EXCEPTIONS = False\n\nSOCIAL_AUTH_FACEBOOK_SCOPE = [\"email\"]\nSOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {\"fields\": \"name,email\"}\n\nSOCIAL_AUTH_PIPELINE = (\n    \"social_core.pipeline.social_auth.social_details\",\n    \"social_core.pipeline.social_auth.social_uid\",\n    \"social_core.pipeline.social_auth.auth_allowed\",\n    \"social_core.pipeline.social_auth.social_user\",\n    \"social_core.pipeline.user.get_username\",\n    \"social_core.pipeline.social_auth.associate_by_email\",\n    \"social_core.pipeline.user.create_user\",\n    \"zds.member.models.save_profile\",\n    \"social_core.pipeline.social_auth.associate_user\",\n    \"social_core.pipeline.social_auth.load_extra_data\",\n    \"social_core.pipeline.user.user_details\",\n)\n\n# Before adding new providers such as Facebook and Google,\n# you need to make sure they validate the user's email address on sign up!\n# If they don't, a malicious person could take control of someone else account!\nSOCIAL_AUTH_FACEBOOK_KEY = social_auth_config.get(\"facebook_key\", \"\")\nSOCIAL_AUTH_FACEBOOK_SECRET = social_auth_config.get(\"facebook_secret\", \"\")\nSOCIAL_AUTH_GOOGLE_OAUTH2_KEY = social_auth_config.get(\n    \"google_oauth2_key\",\n    \"696570367703-r6hc7mdd27t1sktdkivpnc5b25i0uip2.apps.googleusercontent.com\",\n)\nSOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = social_auth_config.get(\n    \"google_oauth2_secret\",\n    \"mApWNh3stCsYHwsGuWdbZWP8\",\n)\n\nSOCIAL_AUTH_SANITIZE_REDIRECTS = social_auth_config.get(\n    \"sanitize_redirects\",\n    False,\n)\n\n\nrecaptcha_config = config.get(\"recaptcha\", {})\n\nUSE_CAPTCHA = recaptcha_config.get(\"use_captcha\", False)\nRECAPTCHA_PUBLIC_KEY = recaptcha_config.get(\"public_key\", \"dummy\")\nRECAPTCHA_PRIVATE_KEY = recaptcha_config.get(\"private_key\", \"dummy\")\n\n\nOAUTH2_PROVIDER = {\"OAUTH2_BACKEND_CLASS\": \"oauth2_provider.oauth2_backends.JSONOAuthLibCore\"}\n", "path": "zds/settings/abstract_base/requirements.py"}, {"content": "import hashlib\nimport re\n\nfrom django.contrib.auth import get_user_model\nfrom django.http import HttpRequest\n\nTHUMB_MAX_WIDTH = 80\nTHUMB_MAX_HEIGHT = 80\n\nMEDIUM_MAX_WIDTH = 200\nMEDIUM_MAX_HEIGHT = 200\n\n\ndef compute_hash(filenames):\n    \"\"\"returns a md5 hexdigest of group of files to check if they have change\"\"\"\n    md5_hash = hashlib.md5()\n    for filename in filenames:\n        if filename:\n            file_handle = open(filename, \"rb\")\n            must_continue = True\n            while must_continue:\n                read_bytes = file_handle.read(8096)\n                if not read_bytes:\n                    must_continue = False\n                else:\n                    md5_hash.update(read_bytes)\n    return md5_hash.hexdigest()\n\n\ndef content_has_changed(filenames, md5):\n    return md5 != compute_hash(filenames)\n\n\ndef has_changed(instance, field, manager=\"objects\"):\n    \"\"\"Returns true if a field has changed in a model May be used in a\n    model.save() method.\"\"\"\n    if not instance.pk:\n        return True\n    manager = getattr(instance.__class__, manager)\n    old = getattr(manager.get(pk=instance.pk), field)\n    return not getattr(instance, field) == old\n\n\ndef convert_camel_to_underscore(camel_case):\n    \"\"\"\n    Converts a name in camel case to underscore.\n    \"\"\"\n    s1 = re.sub(\"(.)([A-Z][a-z]+)\", r\"\\1_\\2\", camel_case)\n    return re.sub(\"([a-z0-9])([A-Z])\", r\"\\1_\\2\", s1).lower()\n\n\ndef remove_utf8mb4(s):\n    \"\"\"\n    Remove characters of more than 3 bytes.\n    \"\"\"\n    if not isinstance(s, str):\n        s = str(s, \"utf-8\")\n    re_pattern = re.compile(\"[^\\u0000-\\uD7FF\\uE000-\\uFFFF]\", re.UNICODE)\n    return re_pattern.sub(\"\", s)\n\n\ndef contains_utf8mb4(s):\n    \"\"\"\n    Check if this string contains at least one character of more than 3 bytes.\n    \"\"\"\n    return s != remove_utf8mb4(s)\n\n\ndef check_essential_accounts():\n    \"\"\"\n    Verify that essential accounts are present in the database.\n    Raise an exception if it is not the case.\n    \"\"\"\n\n    from django.conf import settings\n\n    User = get_user_model()\n    essential_accounts = (\"bot_account\", \"anonymous_account\", \"external_account\")\n\n    for account in essential_accounts:\n        username = settings.ZDS_APP[\"member\"][account]\n        try:\n            User.objects.get(username=username)\n        except User.DoesNotExist:\n            raise Exception(\n                f\"User {username!r} does not exist. You must create it to run the server. \"\n                f\"On a development instance, load the fixtures to solve this issue.\"\n            )\n\n\ndef is_ajax(request: HttpRequest):\n    \"\"\"\n    Check whether the request was sent asynchronously.\n\n    The function returns True for :\n\n    * requests sent using jQuery.ajax() since it sets the header `X-Requested-With`\n      to `XMLHttpRequest` by default ;\n    * requests sent using the tools provided by `ajax.js`, which reproduce the behavior\n      described above to ease the progressive removal of jQuery from the codebase.\n\n    The function returns False for requests without the appropriate header.\n    These requests will not be recognized as AJAX.\n\n    The function replaces `request.is_ajax()`, which is removed starting from Django 4.0.\n    \"\"\"\n    return request.META.get(\"HTTP_X_REQUESTED_WITH\") == \"XMLHttpRequest\"\n", "path": "zds/utils/misc.py"}, {"content": "from django.contrib.auth.models import User\nfrom django.core.exceptions import ValidationError\nfrom django.core.validators import EmailValidator, ProhibitNullCharactersValidator\nfrom django.utils.encoding import force_str\nfrom django.utils.translation import gettext_lazy as _\n\nfrom zds.utils.misc import contains_utf8mb4, remove_utf8mb4\nfrom zds.member.models import BannedEmailProvider, Profile\n\n\ndef validate_not_empty(value):\n    \"\"\"\n    Fields cannot be empty or only contain spaces.\n\n    :param value: value to validate (str or None)\n    :return:\n    \"\"\"\n    if value is None or not value.strip():\n        raise ValidationError(_(\"Le champs ne peut \u00eatre vide\"))\n\n\nclass ZdSEmailValidator(EmailValidator):\n    \"\"\"\n    Based on https://docs.djangoproject.com/en/1.8/_modules/django/core/validators/#EmailValidator\n    Changed :\n      - check if provider is not if blacklisted\n      - check if email is not used by another user\n      - remove whitelist check\n      - add custom errors and translate them into French\n    \"\"\"\n\n    message = _(\"Utilisez une adresse de courriel valide.\")\n\n    def __call__(self, value, check_username_available=True):\n        value = force_str(value)\n\n        if not value or \"@\" not in value:\n            raise ValidationError(self.message, code=self.code)\n\n        user_part, domain_part = value.rsplit(\"@\", 1)\n\n        if not self.user_regex.match(user_part) or contains_utf8mb4(user_part):\n            raise ValidationError(self.message, code=self.code)\n\n        # check if provider is blacklisted\n        blacklist = BannedEmailProvider.objects.values_list(\"provider\", flat=True)\n        for provider in blacklist:\n            if f\"@{provider}\" in value.lower():\n                raise ValidationError(_(\"Ce fournisseur ne peut pas \u00eatre utilis\u00e9.\"), code=self.code)\n\n        # check if email is used by another user\n        user_count = User.objects.filter(email=value).count()\n        if check_username_available and user_count > 0:\n            raise ValidationError(_(\"Cette adresse courriel est d\u00e9j\u00e0 utilis\u00e9e\"), code=self.code)\n        # check if email exists in database\n        elif not check_username_available and user_count == 0:\n            raise ValidationError(_(\"Cette adresse courriel n'existe pas\"), code=self.code)\n\n        if domain_part and not self.validate_domain_part(domain_part):\n            # Try for possible IDN domain-part\n            try:\n                domain_part = domain_part.encode(\"idna\").decode(\"ascii\")\n                if self.validate_domain_part(domain_part):\n                    return\n            except UnicodeError:\n                pass\n            raise ValidationError(self.message, code=self.code)\n\n\nvalidate_zds_email = ZdSEmailValidator()\n\n\ndef clean_username_social_auth(username):\n    \"\"\"\n    Clean username of accounts created using social auth.\n    \"\"\"\n    # These three conditions are the same as the first three in the \"validate_zds_username\" function below.\n    # If you modify one of them here, make sure you do the same there!\n    return remove_utf8mb4(username).replace(\",\", \"\").replace(\"/\", \"\")\n\n\ndef validate_zds_username(value, check_username_available=True):\n    \"\"\"\n    Check if username is used by another user\n\n    :param value: value to validate (str or None)\n    :return:\n    \"\"\"\n\n    # If the character \\x00 is in the username, the homoglyphs library called\n    # in Profile.find_username_skeleton() will raise a ValueError (the bug has\n    # been reported: https://github.com/yamatt/homoglyphs/issues/6). To prevent\n    # this, we call this validator which will raise a ValidationError if \\x00 is\n    # in the username.\n    ProhibitNullCharactersValidator()(value)\n\n    msg = None\n    user_count = User.objects.filter(username=value).count()\n    skeleton_user_count = Profile.objects.filter(username_skeleton=Profile.find_username_skeleton(value)).count()\n\n    # These first three conditions are the same as those in the \"clean_username_social_auth\" function above.\n    # If you modify one of them here, make sure you do the same there!\n    if \",\" in value:\n        msg = _(\"Le nom d'utilisateur ne peut contenir de virgules\")\n    elif \"/\" in value:\n        msg = _(\"Le nom d'utilisateur ne peut contenir de barres obliques\")\n    elif contains_utf8mb4(value):\n        msg = _(\"Le nom d'utilisateur ne peut pas contenir des caract\u00e8res utf8mb4\")\n    elif check_username_available and user_count > 0:\n        msg = _(\"Ce nom d'utilisateur est d\u00e9j\u00e0 utilis\u00e9\")\n    elif check_username_available and skeleton_user_count > 0:\n        msg = _(\"Un nom d'utilisateur visuellement proche du votre existe d\u00e9j\u00e0\")\n    elif not check_username_available and user_count == 0:\n        msg = _(\"Ce nom d'utilisateur n'existe pas\")\n    if msg is not None:\n        raise ValidationError(msg)\n\n\ndef validate_raw_zds_username(data):\n    \"\"\"\n    Check if raw username hasn't space on left or right\n    \"\"\"\n    msg = None\n    username = data.get(\"username\", None)\n    if username is None:\n        msg = _(\"Le nom d'utilisateur n'est pas fourni\")\n    elif username != username.strip():\n        msg = _(\"Le nom d'utilisateur ne peut commencer ou finir par des espaces\")\n\n    if msg is not None:\n        raise ValidationError(msg)\n\n\ndef validate_zds_password(value):\n    \"\"\"\n\n    :param value:\n    :return:\n    \"\"\"\n    if contains_utf8mb4(value):\n        raise ValidationError(_(\"Le mot de passe ne peut pas contenir des caract\u00e8res utf8mb4\"))\n\n\ndef validate_passwords(\n    cleaned_data, password_label=\"password\", password_confirm_label=\"password_confirm\", username=None\n):\n    \"\"\"\n    Chek if cleaned_data['password'] == cleaned_data['password_confirm'] and password is not username.\n    :param cleaned_data:\n    :param password_label:\n    :param password_confirm_label:\n    :return:\n    \"\"\"\n\n    password = cleaned_data.get(password_label)\n    password_confirm = cleaned_data.get(password_confirm_label)\n    msg = None\n\n    if username is None:\n        username = cleaned_data.get(\"username\")\n\n    if not password_confirm == password:\n        msg = _(\"Les mots de passe sont diff\u00e9rents\")\n\n        if password_label in cleaned_data:\n            del cleaned_data[password_label]\n\n        if password_confirm_label in cleaned_data:\n            del cleaned_data[password_confirm_label]\n\n    if username is not None:\n        # Check that password != username\n        if password == username:\n            msg = _(\"Le mot de passe doit \u00eatre diff\u00e9rent du pseudo\")\n            if password_label in cleaned_data:\n                del cleaned_data[password_label]\n            if password_confirm_label in cleaned_data:\n                del cleaned_data[password_confirm_label]\n\n    if msg is not None:\n        raise ValidationError(msg)\n\n    return cleaned_data\n", "path": "zds/member/validators.py"}]}