Django2+后台更新邮箱时如何向email_candidate发送验证邮件
项目背景
- 项目基于Django 2+开发,为医患管理系统,当前已正常运行的业务逻辑如下:
- 医生为患者注册账号后,患者将收到附带验证链接的确认邮件,点击链接完成验证后才可正常使用平台
- 患者在个人中心设置页修改邮箱时,系统不会直接更新
User模型的email字段,仅先更新email_candidate(候选邮箱)字段,验证邮件将发送至该候选邮箱地址;待患者点击邮件内验证链接后,系统才会将email字段正式替换为候选邮箱值,该设计可避免用户输错新邮箱导致账号无法登录,此部分患者端逻辑运行正常。
待解决问题
在Patient Support Admin(患者支持管理后台)中,客服人员可协助患者更新邮箱,但后台配置的resend_email_verification(重发邮箱验证)动作仅会向用户当前已绑定的正式email地址发送验证邮件,不会选取email_candidate字段存储的候选邮箱地址发送,需要复用患者端已实现的update_candidate_email序列化方法,实现后台向候选邮箱发送验证邮件的功能。
相关代码
models.py - User模型
class User(AbstractUser): objects = UserManager() is_physician = models.BooleanField(default=False) is_patient = models.BooleanField(default=False) title = models.CharField(choices=TITLE_CHOICES, max_length=10, blank=True, null=True) first_name = models.CharField(max_length=254, blank=True, null=True, ) last_name = models.CharField(max_length=254, blank=True, null=True, ) birthdate = models.DateField(null=True, blank=True) home_phone = PhoneNumberField(blank=True, null=True) mobile_phone = PhoneNumberField(blank=True, null=True) gender = models.CharField(max_length=1, choices=GENDER_CHOICES, blank=True, null=True) language = models.CharField(max_length=4, choices=settings.LANGUAGES, default='en', blank=True, null=True) email = models.EmailField(unique=True, db_index=True, error_messages={ 'unique': "Email already exists." } ) email_candidate = models.EmailField( max_length=254, blank=True, null=True, default=None) username = models.CharField( max_length=30, unique=True, blank=True, null=True, validators=[ validators.RegexValidator( r'^\w+$', 'Enter a valid username. This value may contain only ' 'letters, numbers and _ character.', 'invalid' ), ], error_messages={ 'unique': "Username already exists." } ) USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] ordering = ('created',) class Meta(object): verbose_name = 'User' verbose_name_plural = 'Users' # abstract = False permissions = u_perm.user_permissions @property def name(self): return '{} {}'.format(self.last_name, self.first_name, )
models.py - Patient模型(与User为OneToOne关联)
class Patient(models.Model): user = models.OneToOneField( settings.AUTH_USER_MODEL, on_delete=models.CASCADE) physician = models.ForeignKey(Physician, related_name='patients', null=True, blank=True, on_delete=models.SET_NULL) address = models.TextField(blank=True, null=True) city = models.CharField(max_length=85, null=True, blank=True) country = CountryField(null=True, blank=True) postal_code = models.CharField(max_length=32, null=True, blank=True) reminder_hour_default = models.TimeField( 'Send Reminder Time', default=settings.REMINDER_HOUR_DEFAULT) is_eligible = models.BooleanField(default=True) def __str__(self): return self.user.email @receiver(post_save, sender=Patient) @receiver(post_save, sender=User) def update_status(sender, **kwargs): def update_patient(instance): # from pending/disabled to active current_status = instance.user.status instance.save() instance.user.save() return def update_user(instance): pass instance = kwargs.get('instance') method_name = get_inner_method(sender) if instance and method_name: method_name(instance)
models.py - PatientSupport代理模型
class PatientSupport(Patient): class Meta: proxy = True # Add verbose name verbose_name = 'Patient Support' verbose_name_plural = 'Patients Support'
serializers.py - UserSerializer(包含患者端已正常运行的update_candidate_email方法)
class UserSerializer(serializers.ModelSerializer): permission_list = serializers.SerializerMethodField() permissions = serializers.SerializerMethodField() user_messages = serializers.SerializerMethodField() class Meta: model = User fields = ( 'pk', 'title', 'first_name', 'last_name', 'birthdate', 'email', 'email_candidate', 'home_phone', 'mobile_phone', 'gender', 'language', 'username', 'password', 'is_patient', ) extra_kwargs = { 'email': {'required': False, 'validators': [], #'read_only': True, }, 'email_candidate': {'required': False, 'validators': [], #'read_only': True, }, 'home_phone': {'required': False, 'validators': [], }, 'mobile_phone': {'required': False, 'validators': [], }, 'is_patient': {'required': False}, 'username': {'read_only': True, }, } def update_candidate_email(self, instance, validated_data, email_candidate): # if email candidate is None (and changed) -> search for tokens and delete token sent if instance.email_candidate and email_candidate == None: instance.email_candidate = email_candidate instance.save() ResetPasswordToken.objects.filter(user=instance).delete() # if email candidate is same to current email -> error - email in use ! if instance.email_candidate == instance.email: raise serializers.ValidationError( {'email': "email candidate cannot be same to current mail"} ) # if email candidate is not None and already not in use-> save it (email candidate) and send new email replace_email_mail if email_candidate and email_candidate != instance.email_candidate: if User.objects.filter(email=email_candidate): raise serializers.ValidationError( {'email': "email in use"} ) ResetPasswordToken.objects.filter(user=instance).delete() instance.email_candidate = email_candidate instance.save() site_domain = Site.objects.filter(name__icontains='admin').first() # create pass token send mail token = ResetPasswordToken.objects.create( user=instance,) tiny_link = shortener.create(instance, 'https://{}/api/users/verifyMail/?lang={}&token={}®ion={}&patient={}'.format( site_domain.domain, instance.language if instance.language else "en", token.key, settings.REGION_PREFIX, instance.is_patient ) ) from messaging.notify import Notify activate(instance.language) msg_name = 'EMAIL_VERIFICATION' if not instance.is_patient: msg_name = 'EMAIL_VERIFICATION_STAFF' notify = Notify() notify.send_notification( msg_name=msg_name, users_pk=[instance.pk, ], context={ 'tiny_link': 'https://{}/s/{}/?lang={}®ion={}'.format( site_domain.domain, tiny_link, instance.language if instance.language else "en", settings.REGION_PREFIX), 'email_candidate': email_candidate }) return
admin.py - PatientSupportAdmin(需实现向候选邮箱发验证邮件的功能)
class PatientSupportAdmin(admin.ModelAdmin): list_display = ['pk', 'patient', 'patient_email', 'email_candidate' ] list_display_links = ('pk', 'patient') def get_queryset(self, request): qs = super(PatientSupportAdmin, self).get_queryset(request) return qs.filter(support_consent=True) fieldsets = ( ('User', { 'fields': ( 'patient', 'get_gender', 'birthdate', 'home_phone', 'mobile_phone', 'language', 'email', 'username', 'date_modified', 'date_joined', 'status', 'user', )}), ('Patient', {'fields': ('physician', 'country', 'id_number', )}), def patient(self, obj): return obj.user.get_full_name() def patient_email(self, obj): return obj.user.email def email_candidate(self, obj): return obj.user.email_candidate def birthdate(self, obj): return obj.user.birthdate or '' def home_phone(self, obj): return obj.user.home_phone def mobile_phone(self, obj): return obj.user.mobile_phone def email(self, obj): return obj.user.email def username(self, obj): return obj.user.username or '' actions = [resend_email_verification, ]
admin.py - 现有resend_email_verification方法(仅向正式绑定邮箱发验证邮件)
def resend_email_verifcation(modeladmin, request, queryset): for user in queryset: if 'users/patient' in request.path: user = user.user if user.is_patient and not user.patient.is_eligible: continue site_domain = Site.objects.filter(name__icontains='admin').first() token = ResetPasswordToken.objects.create( user=user, ) reset_password_token_created.send( sender=modeladmin.__class__, reset_password_token=token)
内容的提问来源于stack exchange,提问作者Mike
相关产品推荐
相关产品推荐

