Почему я не могу сбросить пароль?

Такой вопрос пришел сегодня в техподдержку. Пользователь заходит на страницу восстановления пароля, вводит свой email, нажимает кнопку «Восстановить». Система радостно сообщает, что email отправлен. Пользователь заходит в почтовый ящик, пользователь не видит письма, пользователь недоволен.Далее следует стандартное:  «Проверьте правильность ввода email’а, убедитесь, что письмо не попало в спам». Проверили-убедились, не помогло. Захожу на почтовый сервер — письмо даже не было отправлено.Отрываюсь от всех дел и бросаюсь в тестирование. Захожу на страницу восстановления, ввожу свой email — все в порядке, письмо со ссылкой на восстановление пароля приходит. Ввожу email пользователя — тишина. Письмо не отправляется. В логе — ничего (от слова «совсем»).

Далее следует с полчаса бесполезных метаний, немного недоумения и много нецензурной лексики. Успокаиваемся, делаем глубокий вдох и лезем в исходный код Django.

За сброс пароля отвечает password_reset:

Скрытый текст @csrf_protect def password_reset (request, is_admin_site=False, template_name='registration/password_reset_form.html', email_template_name='registration/password_reset_email.html', subject_template_name='registration/password_reset_subject.txt', password_reset_form=PasswordResetForm, token_generator=default_token_generator, post_reset_redirect=None, from_email=None, current_app=None, extra_context=None): if post_reset_redirect is None: post_reset_redirect = reverse ('password_reset_done') else: post_reset_redirect = resolve_url (post_reset_redirect) if request.method == «POST»: form = password_reset_form (request.POST) if form.is_valid (): opts = { 'use_https': request.is_secure (), 'token_generator': token_generator, 'from_email': from_email, 'email_template_name': email_template_name, 'subject_template_name': subject_template_name, 'request': request, } if is_admin_site: opts = dict (opts, domain_override=request.get_host ()) form.save (**opts) return HttpResponseRedirect (post_reset_redirect) else: form = password_reset_form () context = { 'form': form, } if extra_context is not None: context.update (extra_context) return TemplateResponse (request, template_name, context, current_app=current_app) Раз редирект на post_reset_redirect происходит, значит, form.save () выполняется. Смотрим, что у него под капотом: Скрытый текст def save (self, domain_override=None, subject_template_name='registration/password_reset_subject.txt', email_template_name='registration/password_reset_email.html', use_https=False, token_generator=default_token_generator, from_email=None, request=None): »« Generates a one-use only link for resetting password and sends to the user. »« from django.core.mail import send_mail UserModel = get_user_model () email = self.cleaned_data[«email»] active_users = UserModel._default_manager.filter ( email__iexact=email, is_active=True) for user in active_users: # Make sure that no email is sent to a user that actually has # a password marked as unusable if not user.has_usable_password (): continue if not domain_override: current_site = get_current_site (request) site_name = current_site.name domain = current_site.domain else: site_name = domain = domain_override c = { 'email': user.email, 'domain': domain, 'site_name': site_name, 'uid': urlsafe_base64_encode (force_bytes (user.pk)), 'user': user, 'token': token_generator.make_token (user), 'protocol': 'https' if use_https else 'http', } subject = loader.render_to_string (subject_template_name, c) # Email subject *must not* contain newlines subject = ''.join (subject.splitlines ()) email = loader.render_to_string (email_template_name, c) send_mail (subject, email, from_email, [user.email]) Тут, конечно, до меня доходит. Пользователь сначала регистрировался через ВКонтакте. Потом поставил email. И отвязал ВКонтакте. И вот в процессе регистрации через ВК ему, т.е. пользователю, назначали set_unusable_password () (ибо пароля-то у него не было).Весь этот поток эмоций был вызван вот этими строками:

# Make sure that no email is sent to a user that actually has # a password marked as unusable if not user.has_usable_password (): continue Зачем? Почему? Кто виноват? Почему нельзя сбросить пароль, если изначально он не был задан? А главное, почему система об этом никак не сообщает, а, хихикая, перенаправляет на post_reset_redirect?! Как там говорится, «явное лучше неявного»? В общем, имейте в виду. И не наступайте на эти грабли.

© Habrahabr.ru