GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#76)
by
unknown
01:07
created

RegistrationProfile._send_email()   B

Complexity

Conditions 4

Size

Total Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
c 2
b 0
f 0
dl 0
loc 28
rs 8.5806
1
# -*- coding: utf-8 -*-
2
from __future__ import unicode_literals
3
"""
4
Models of django-inspectional-registration
5
6
This is a modification of django-registration_ ``models.py``
7
The original code is written by James Bennett
8
9
.. _django-registration: https://bitbucket.org/ubernostrum/django-registration
10
11
12
Original License::
13
14
    Copyright (c) 2007-2011, James Bennett
15
    All rights reserved.
16
17
    Redistribution and use in source and binary forms, with or without
18
    modification, are permitted provided that the following conditions are
19
    met:
20
21
        * Redistributions of source code must retain the above copyright
22
        notice, this list of conditions and the following disclaimer.
23
        * Redistributions in binary form must reproduce the above
24
        copyright notice, this list of conditions and the following
25
        disclaimer in the documentation and/or other materials provided
26
        with the distribution.
27
        * Neither the name of the author nor the names of other
28
        contributors may be used to endorse or promote products derived
29
        from this software without specific prior written permission.
30
31
    THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
32
    "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
33
    LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
34
    A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
35
    OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36
    SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
37
    LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
38
    DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
39
    THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
40
    (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
41
    OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42
43
"""
44
__author__ = 'Alisue <[email protected]>'
45
__all__ = (
46
    'ActivationForm', 'RegistrationForm', 
47
    'RegistrationFormNoFreeEmail',
48
    'RegistrationFormTermsOfService',
49
    'RegistrationFormUniqueEmail',
50
)
51
import re
52
import sys
53
import datetime
54
55
from django.db import models
56
from django.template.loader import render_to_string
57
from django.core.exceptions import ObjectDoesNotExist
58
from django.utils.text import ugettext_lazy as _
59
from django.utils.encoding import python_2_unicode_compatible
60
61
from registration.conf import settings
62
from registration.compat import get_user_model
63
from registration.compat import user_model_label
64
from registration.compat import datetime_now
65
from registration.utils import generate_activation_key
66
from registration.utils import generate_random_password
67
from registration.utils import send_mail
68
from registration.supplements import get_supplement_class
69
from registration.compat import transaction_atomic
70
71
from logging import getLogger
72
logger = getLogger(__name__)
73
74
SHA1_RE = re.compile(r'^[a-f0-9]{40}$')
75
76
77
class RegistrationManager(models.Manager):
78
    """Custom manager for the ``RegistrationProfile`` model.
79
80
    The methods defined here provide shortcuts for account registration,
81
    registration acceptance, registration rejection and account activation
82
    (including generation and emailing of activation keys), and for cleaning out
83
    expired/rejected inactive accounts.
84
85
    """
86
    @transaction_atomic
87
    def register(self, username, email, site, send_email=True):
88
        """register new user with ``username`` and ``email``
89
90
        Create a new, inactive ``User``, generate a ``RegistrationProfile``
91
        and email notification to the ``User``, returning the new ``User``.
92
93
        By default, a registration email will be sent to the new user. To
94
        disable this, pass ``send_email=False``. A registration email will be
95
        generated by ``registration/registration_email.txt`` and
96
        ``registration/registration_email_subject.txt``.
97
98
        The user created by this method has no usable password and it will
99
        be set after activation.
100
101
        This method is transactional. Thus if some exception has occur in this
102
        method, the newly created user will be rollbacked.
103
104
        """
105
        User = get_user_model()
106
        new_user = User.objects.create_user(username, email, 'password')
107
        new_user.set_unusable_password()
108
        new_user.is_active = False
109
        new_user.save()
110
111
        profile = self.create(user=new_user)
112
113
        if send_email:
114
            profile.send_registration_email(site)
115
116
        return new_user
117
118
    @transaction_atomic
119
    def accept_registration(self, profile, site,
120
                            send_email=True, message=None, force=False):
121
        """accept account registration of ``profile``
122
123
        Accept account registration and email activation url to the ``User``,
124
        returning accepted ``User``. 
125
126
        By default, an acceptance email will be sent to the new user. To
127
        disable this, pass ``send_email=False``. An acceptance email will be
128
        generated by ``registration/acceptance_email.txt`` and
129
        ``registration/acceptance_email_subject.txt``.
130
131
        This method **DOES** works even after ``reject_registration`` has called
132
        (this mean the account registration has rejected previously) because 
133
        rejecting user by mistake may occur in this real world :-p If the account 
134
        registration has already accepted, returning will be ``None``
135
136
        The ``date_joined`` attribute of ``User`` updated to now in this
137
        method and ``activation_key`` of ``RegistrationProfile`` will
138
        be generated.
139
140
        """
141
        # rejected -> accepted is allowed
142
        if force or profile.status in ('untreated', 'rejected'):
143
            if force:
144
                # removing activation_key will force to create a new one
145
                profile.activation_key = None
146
            profile.status = 'accepted'
147
            profile.save()
148
149
            if send_email:
150
                profile.send_acceptance_email(site, message=message)
151
152
            return profile.user
153
        return None
154
155
    @transaction_atomic
156
    def reject_registration(self, profile, site, send_email=True, message=None):
157
        """reject account registration of ``profile``
158
159
        Reject account registration and email rejection to the ``User``,
160
        returning accepted ``User``. 
161
162
        By default, an rejection email will be sent to the new user. To
163
        disable this, pass ``send_email=False``. An rejection email will be
164
        generated by ``registration/rejection_email.txt`` and
165
        ``registration/rejection_email_subject.txt``.
166
167
        This method **DOES NOT** works after ``accept_registration`` has called
168
        (this mean the account registration has accepted previously).
169
        If the account registration has already accepted/rejected, returning 
170
        will be ``None``
171
172
        """
173
        # accepted -> rejected is not allowed
174
        if profile.status == 'untreated':
175
            profile.status = 'rejected'
176
            profile.save()
177
178
            if send_email:
179
                profile.send_rejection_email(site, message=message)
180
181
            return profile.user
182
        return None
183
184
    @transaction_atomic
185
    def activate_user(self, activation_key, site, password=None,
186
                      send_email=True, message=None, no_profile_delete=False):
187
        """activate account with ``activation_key`` and ``password``
188
189
        Activate account and email notification to the ``User``, returning 
190
        activated ``User``, ``password`` and ``is_generated``. 
191
192
        By default, an activation email will be sent to the new user. To
193
        disable this, pass ``send_email=False``. An activation email will be
194
        generated by ``registration/activation_email.txt`` and
195
        ``registration/activation_email_subject.txt``.
196
197
        This method **DOES NOT** works if the account registration has not been
198
        accepted. You must accept the account registration before activate the
199
        account. Returning will be ``None`` if the account registration has not
200
        accepted or activation key has expired.
201
202
        if passed ``password`` is ``None`` then random password will be generated
203
        and set to the ``User``. If the password is generated, ``is_generated``
204
        will be ``True``
205
206
        Use returning value like::
207
208
            activated = RegistrationProfile.objects.activate_user(activation_key)
209
210
            if activated:
211
                # Activation has success
212
                user, password, is_generated = activated
213
                # user -- a ``User`` instance of account
214
                # password -- a raw password of ``User``
215
                # is_generated -- ``True`` if the password is generated
216
217
        When activation has success, the ``RegistrationProfile`` of the ``User``
218
        will be deleted from database because the profile is no longer required.
219
220
        """
221
        try:
222
            profile = self.get(_status='accepted', activation_key=activation_key)
223
        except self.model.DoesNotExist:
224
            return None
225
        if not profile.activation_key_expired():
226
            is_generated = password is None
227
            password = password or generate_random_password(
228
                    length=settings.REGISTRATION_DEFAULT_PASSWORD_LENGTH)
229
            user = profile.user
230
            user.set_password(password)
231
            user.is_active = True
232
            user.save()
233
234
            if send_email:
235
                profile.send_activation_email(site, password,
236
                                              is_generated, message=message)
237
238
            if not no_profile_delete:
239
                # the profile is no longer required
240
                profile.delete()
241
            return user, password, is_generated
242
        return None
243
244
    @transaction_atomic
245
    def delete_expired_users(self):
246
        """delete expired users from database
247
248
        Remove expired instance of ``RegistrationProfile`` and their associated
249
        ``User``.
250
251
        Accounts to be deleted are identified by searching for instance of
252
        ``RegistrationProfile`` with expired activation keys, and then checking
253
        to see if their associated ``User`` instance have the field ``is_active``
254
        set to ``False`` (it is for compatibility of django-registration); any
255
        ``User`` who is both inactive and has an expired activation key will be
256
        deleted.
257
258
        It is recommended that this method be executed regularly as part of your
259
        routine site maintenance; this application provides a custom management
260
        command which will call this method, accessible as 
261
        ``manage.py cleanupexpiredregistration`` (for just expired users) or
262
        ``manage.py cleanupregistration`` (for expired or rejected users).
263
264
        Reqularly clearing out accounts which have never been activated servers
265
        two useful purposes:
266
267
        1.  It alleviates the ocasional need to reset a ``RegistrationProfile``
268
            and/or re-send an activation email when a user does not receive or
269
            does not act upon the initial activation email; since the account
270
            will be deleted, the user will be able to simply re-register and
271
            receive a new activation key (if accepted).
272
273
        2.  It prevents the possibility of a malicious user registering one or
274
            more accounts and never activating them (thus denying the use of
275
            those username to anyone else); since those accounts will be deleted,
276
            the username will become available for use again.
277
278
        If you have a troublesome ``User`` and wish to disable their account while
279
        keeping it in the database, simply delete the associated 
280
        ``RegistrationProfile``; an inactive ``User`` which does not have an 
281
        associated ``RegistrationProfile`` will be deleted.
282
283
        """
284
        for profile in self.all():
285
            if profile.activation_key_expired():
286
                try:
287
                    user = profile.user
288
                    if not user.is_active:
289
                        user.delete()
290
                        profile.delete()    # just in case
291
                except ObjectDoesNotExist:
292
                    profile.delete()
293
294
    @transaction_atomic
295
    def delete_rejected_users(self):
296
        """delete rejected users from database
297
298
        Remove rejected instance of ``RegistrationProfile`` and their associated
299
        ``User``.
300
301
        Accounts to be deleted are identified by searching for instance of
302
        ``RegistrationProfile`` with rejected status, and then checking
303
        to see if their associated ``User`` instance have the field ``is_active``
304
        set to ``False`` (it is for compatibility of django-registration); any
305
        ``User`` who is both inactive and its registration has been rejected will
306
        be deleted.
307
308
        It is recommended that this method be executed regularly as part of your
309
        routine site maintenance; this application provides a custom management
310
        command which will call this method, accessible as 
311
        ``manage.py cleanuprejectedregistration`` (for just rejected users) or
312
        ``manage.py cleanupregistration`` (for expired or rejected users).
313
314
        Reqularly clearing out accounts which have never been activated servers
315
        two useful purposes:
316
317
        1.  It alleviates the ocasional need to reset a ``RegistrationProfile``
318
            and/or re-send an activation email when a user does not receive or
319
            does not act upon the initial activation email; since the account
320
            will be deleted, the user will be able to simply re-register and
321
            receive a new activation key (if accepted).
322
323
        2.  It prevents the possibility of a malicious user registering one or
324
            more accounts and never activating them (thus denying the use of
325
            those username to anyone else); since those accounts will be deleted,
326
            the username will become available for use again.
327
328
        If you have a troublesome ``User`` and wish to disable their account while
329
        keeping it in the database, simply delete the associated 
330
        ``RegistrationProfile``; an inactive ``User`` which does not have an 
331
        associated ``RegistrationProfile`` will be deleted.
332
333
        """
334
        for profile in self.all():
335
            if profile.status == 'rejected':
336
                try:
337
                    user = profile.user
338
                    if not user.is_active:
339
                        user.delete()
340
                        profile.delete() # just in case
341
                except ObjectDoesNotExist:
342
                    profile.delete()
343
344
345
@python_2_unicode_compatible
346
class RegistrationProfile(models.Model):
347
    """Registration profile model class
348
349
    A simple profile which stores an activation key and inspection status for use
350
    during user account registration/inspection.
351
352
    Generally, you will not want to interact directly with instances of this model;
353
    the provided manager includes method for creating, accepting, rejecting and
354
    activating, as well as for cleaning out accounts which have never been activated
355
    or its registration has been rejected.
356
357
    While it is possible to use this model as the value of the ``AUTH_PROFILE_MODEL``
358
    setting, it's not recommended that you do so. This model's sole purpose is to
359
    store data temporarily during account registration, inspection and activation.
360
361
    """
362
    STATUS_LIST = (
363
        ('untreated', _('Unprocessed')),
364
        ('accepted', _('Registration accepted')),
365
        ('rejected', _('Registration rejected')),
366
    )
367
    user = models.OneToOneField(user_model_label, verbose_name=_('user'), 
368
                                related_name='registration_profile',
369
                                editable=False)
370
    _status = models.CharField(_('status'), max_length=10, db_column='status',
371
                              choices=STATUS_LIST, default='untreated',
372
                              editable=False)
373
    activation_key = models.CharField(_('activation key'), max_length=40,
374
                                      null=True, default=None, editable=False)
375
376
    objects = RegistrationManager()
377
378
    class Meta:
379
        verbose_name = _('registration profile')
380
        verbose_name_plural = _('registration profiles')
381
        permissions = (
382
                ('accept_registration', 'Can accept registration'),
383
                ('reject_registration', 'Can reject registration'),
384
                ('activate_user', 'Can activate user in admin site'),
385
            )
386
387
    def _get_supplement_class(self):
388
        """get supplement class of this registration"""
389
        return get_supplement_class()
390
    supplement_class = property(_get_supplement_class)
391
392
    def _get_supplement(self):
393
        """get supplement information of this registration"""
394
        supplement_class = self.supplement_class
395
        if supplement_class:
396
            app_label = supplement_class._meta.app_label
397
            class_name = supplement_class.__name__.lower()
398
            field_name = '_%s_%s_supplement' % (app_label, class_name)
399
            return getattr(self, field_name, None)
400
        else:
401
            return None
402
    supplement = property(_get_supplement)
403
404
    def _get_status(self):
405
        """get inspection status of this profile
406
407
        this will return 'expired' for profile which is accepted but
408
        activation key has expired
409
410
        """
411
        if self.activation_key_expired():
412
            return 'expired'
413
        return self._status
414
    def _set_status(self, value):
415
        """set inspection status of this profile
416
417
        Setting status to ``'accepted'`` will generate activation key
418
        and update ``date_joined`` attribute to now of associated ``User``
419
420
        Setting status not to ``'accepted'`` will remove activation key
421
        of this profile.
422
423
        """
424
        self._status = value
425
        # Automatically generate activation key for accepted profile
426
        if value == 'accepted' and not self.activation_key:
427
            username = self.user.username
428
            self.activation_key = generate_activation_key(username)
429
            # update user's date_joined
430
            self.user.date_joined = datetime_now()
431
            self.user.save()
432
        elif value != 'accepted' and self.activation_key:
433
            self.activation_key = None
434
    status = property(_get_status, _set_status)
435
436
    def get_status_display(self):
437
        """get human readable status"""
438
        sl = list(self.STATUS_LIST)
439
        sl.append(('expired', _('Activation key has expired')))
440
        sl = dict(sl)
441
        return sl.get(self.status)
442
    get_status_display.short_description = _("status")
443
444
    def __str__(self):
445
        return "Registration information for %s" % self.user
446
447
    def activation_key_expired(self):
448
        
449
        """get whether the activation key of this profile has expired
450
451
        Determine whether this ``RegistrationProfiel``'s activation key has
452
        expired, returning a boolean -- ``True`` if the key has expired.
453
454
        Key expiration is determined by a two-step process:
455
456
        1.  If the inspection status is not ``'accepted'``, the key is set to
457
            ``None``. In this case, this method returns ``False`` because these
458
            profiles are not treated yet or rejected by inspector.
459
460
        2.  Otherwise, the date the user signed up (which automatically updated
461
            in registration acceptance) is incremented by the number of days 
462
            specified in the setting ``ACCOUNT_ACTIVATION_DAYS`` (which should
463
            be the number of days after acceptance during which a user is allowed
464
            to activate their account); if the result is less than or equal to
465
            the current date, the key has expired and this method return ``True``.
466
467
        """
468
        if self._status != 'accepted':
469
            return False
470
        expiration_date = datetime.timedelta(
471
                days=settings.ACCOUNT_ACTIVATION_DAYS)
472
        expired = self.user.date_joined + expiration_date <= datetime_now()
473
        return expired
474
    activation_key_expired.boolean = True
475
    activation_key_expired.short_description = _(
476
        'Activation Key Expired?'
477
    )
478
479
    def _send_email(self, site, action, extra_context=None):
480
        context = {
481
                'user': self.user,
482
                'site': site,
483
            }
484
        if action != 'activation':
485
            # the profile was deleted in 'activation' action
486
            context['profile'] = self
487
488
        if extra_context:
489
            context.update(extra_context)
490
491
        subject = render_to_string(
492
                'registration/%s_email_subject.txt' % action, context)
493
        subject = ''.join(subject.splitlines())
494
        message = render_to_string(
495
                'registration/%s_email.txt' % action, context)
496
497
        try:
498
            mail_from = settings.REGISTRATION_FROM_EMAIL
499
        except AttributeError as e:
500
            logger.warning('%s:%s:settings.REGISTRATION_FROM_EMAIL not'
501
                           ' found: %s' % (
502
                               __file__, sys._getframe().f_code.co_name, e)
503
                           )
504
            mail_from = settings.DEFAULT_FROM_EMAIL
505
            
506
        send_mail(subject, message, mail_from, [self.user.email])
507
508
    def send_registration_email(self, site):
509
        """send registration email to the user associated with this profile
510
511
        Send a registration email to the ``User`` associated with this
512
        ``RegistrationProfile``.
513
514
        The registration email will make use of two templates:
515
516
        ``registration/registration_email_subject.txt``
517
            This template will be used for the subject line of the email. Because
518
            it is used as the subject line of an email, this template's output
519
            **must** be only a single line of text; output longer than one line
520
            will be forcibly joined into only a single line.
521
522
        ``registration/registration_email.txt``
523
            This template will be used for the body of the email
524
525
        These templates will each receive the following context variables:
526
527
        ``site``
528
            An object representing the site on which the user registered;this is
529
            an instance of ``django.contrib.sites.models.Site`` or
530
            ``django.contrib.sites.models.RequestSite``
531
532
        ``user``
533
            A ``User`` instance of the registration.
534
535
        ``profile``
536
            A ``RegistrationProfile`` instance of the registration
537
538
        """
539
        self._send_email(site, 'registration')
540
541
    def send_acceptance_email(self, site, message=None):
542
        """send acceptance email to the user associated with this profile
543
544
        Send an acceptance email to the ``User`` associated with this
545
        ``RegistrationProfile``.
546
547
        The acceptance email will make use of two templates:
548
549
        ``registration/acceptance_email_subject.txt``
550
            This template will be used for the subject line of the email. Because
551
            it is used as the subject line of an email, this template's output
552
            **must** be only a single line of text; output longer than one line
553
            will be forcibly joined into only a single line.
554
555
        ``registration/acceptance_email.txt``
556
            This template will be used for the body of the email
557
558
        These templates will each receive the following context variables:
559
560
        ``site``
561
            An object representing the site on which the user registered;this is
562
            an instance of ``django.contrib.sites.models.Site`` or
563
            ``django.contrib.sites.models.RequestSite``
564
565
        ``user``
566
            A ``User`` instance of the registration.
567
568
        ``profile``
569
            A ``RegistrationProfile`` instance of the registration
570
571
        ``activation_key``
572
            The activation key for tne new account. Use following code to get
573
            activation url in the email body::
574
575
                http://{{ site.domain }}
576
                {% url 'registration_activate' activation_key=activation_key %}
577
578
        ``expiration_days``
579
            The number of days remaining during which the account may be activated.
580
581
        ``message``
582
            A message from inspector. In default template, it is not shown.
583
584
        """
585
        extra_context = {
586
                'activation_key': self.activation_key,
587
                'expiration_days': settings.ACCOUNT_ACTIVATION_DAYS,
588
                'message': message,
589
            }
590
        self._send_email(site, 'acceptance', extra_context)
591
592
    def send_rejection_email(self, site, message=None):
593
        """send rejection email to the user associated with this profile
594
595
        Send a rejection email to the ``User`` associated with this
596
        ``RegistrationProfile``.
597
598
        The rejection email will make use of two templates:
599
600
        ``registration/rejection_email_subject.txt``
601
            This template will be used for the subject line of the email. Because
602
            it is used as the subject line of an email, this template's output
603
            **must** be only a single line of text; output longer than one line
604
            will be forcibly joined into only a single line.
605
606
        ``registration/rejection_email.txt``
607
            This template will be used for the body of the email
608
609
        These templates will each receive the following context variables:
610
611
        ``site``
612
            An object representing the site on which the user registered;this is
613
            an instance of ``django.contrib.sites.models.Site`` or
614
            ``django.contrib.sites.models.RequestSite``
615
616
        ``user``
617
            A ``User`` instance of the registration.
618
619
        ``profile``
620
            A ``RegistrationProfile`` instance of the registration
621
622
        ``message``
623
            A message from inspector. In default template, it is used for explain
624
            why the account registration has been rejected.
625
626
        """
627
        extra_context = {
628
                'message': message,
629
            }
630
        self._send_email(site, 'rejection', extra_context)
631
632
    def send_activation_email(self, site, password=None, is_generated=False,
633
                              message=None):
634
        """send activation email to the user associated with this profile
635
636
        Send a activation email to the ``User`` associated with this
637
        ``RegistrationProfile``.
638
639
        The activation email will make use of two templates:
640
641
        ``registration/activation_email_subject.txt``
642
            This template will be used for the subject line of the email. Because
643
            it is used as the subject line of an email, this template's output
644
            **must** be only a single line of text; output longer than one line
645
            will be forcibly joined into only a single line.
646
647
        ``registration/activation_email.txt``
648
            This template will be used for the body of the email
649
650
        These templates will each receive the following context variables:
651
652
        ``site``
653
            An object representing the site on which the user registered;this is
654
            an instance of ``django.contrib.sites.models.Site`` or
655
            ``django.contrib.sites.models.RequestSite``
656
657
        ``user``
658
            A ``User`` instance of the registration.
659
660
        ``password``
661
            A raw password of ``User``. Use this to tell user to them password
662
            when the password is generated
663
664
        ``is_generated``
665
            A boolean -- ``True`` if the password is generated. Don't forget to
666
            tell user to them password when the password is generated
667
668
        ``message``
669
            A message from inspector. In default template, it is not shown.
670
671
        """
672
        extra_context = {
673
                'password': password,
674
                'is_generated': is_generated,
675
                'message': message,
676
            }
677
        self._send_email(site, 'activation', extra_context)
678