models.py 4.63 KB
Newer Older
1 2
"""
go/models.py
3

David Haynes's avatar
David Haynes committed
4 5 6
The core of Go: define the business logic through classes that represent
tables containing structured data in the database.
"""
David Haynes's avatar
David Haynes committed
7 8 9
# Python stdlib Imports
import string

10
# Django Imports
Jean Michel Rouly's avatar
Jean Michel Rouly committed
11
from django.contrib.auth.models import User
12
from django.core.cache import cache
13
from django.db import models
14 15
from django.db.models.signals import post_save
from django.dispatch import receiver
16
from django.utils import timezone
17 18 19
from django.conf import settings
from django.db.models.signals import post_save
from django.dispatch import receiver
20 21

# Other Imports
22
from hashids import Hashids
23
from .validators import regex_short_validator, unique_short_validator
24
from rest_framework.authtoken.models import Token
25

26 27 28 29
# Generate the salt and initialize Hashids
# Note: the Hashids library already implements several restrictions oncharacter
# placement, including repeating or incrementing numbers, or placing curse word
# characters adjacent to one another.
30 31 32 33
SIMILAR_CHARS = set(['b', 'G', '6', 'g', 'q', 'l',
                     '1', 'I', 'S', '5', 'O', '0'])
ALPHANUMERICS = set(string.ascii_letters + string.digits)
LINK_CHARS = ''.join(ALPHANUMERICS - SIMILAR_CHARS)
34

35
HASHIDS = Hashids(
36
    salt="srct.gmu.edu", alphabet=(LINK_CHARS)
37
)
Jean Michel Rouly's avatar
Jean Michel Rouly committed
38

39
class RegisteredUser(models.Model):
40
    """
41 42
    Wrapper model for the built in User model which stores data pertaining to
    the registration / approval / blocked status of a django user.
43
    """
44 45 46 47 48
    user = models.OneToOneField(
        User,
        on_delete="cascade",
        verbose_name="Django User Object"
    )
Jean Michel Rouly's avatar
Jean Michel Rouly committed
49

50
    full_name = models.CharField(
51
        "Full Name",
52
        max_length=100,
53
        default="",
54
    )
Jean Michel Rouly's avatar
Jean Michel Rouly committed
55

56
    organization = models.CharField(
57
        "Organization",
58
        max_length=100,
59
        default="",
60 61
    )

62
    description = models.TextField(
63
        "Signup Description",
64 65 66
        blank=True,
        default="",
    )
67

68
    registered = models.BooleanField(
69
        "Registration Status",
70 71
        default=False,
    )
72

73
    approved = models.BooleanField(
74
        "Approval Status",
75 76
        default=False,
    )
77

78
    blocked = models.BooleanField(
79
        "Blocked Status",
80 81
        default=False,
    )
David Haynes's avatar
David Haynes committed
82

83
    def __str__(self):
84
        return f"<RegisteredUser: {self.user} - Approval Status: {self.approved}>"
David Haynes's avatar
David Haynes committed
85

86
@receiver(post_save, sender=User)
87
def handle_reguser_creation(sender, instance, created, **kwargs):
David Haynes's avatar
David Haynes committed
88
    """
89
    When a post_save is called on a User object (and it is newly created), this
David Haynes's avatar
David Haynes committed
90
    is called to create an associated RegisteredUser.
David Haynes's avatar
David Haynes committed
91
    """
92
    if created:
David Haynes's avatar
David Haynes committed
93
        RegisteredUser.objects.create(user=instance, full_name=instance.get_full_name())
94

95 96 97 98 99 100
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
        token = Token.objects.create(user=instance)
        print(token.key)

101
class URL(models.Model):
David Haynes's avatar
David Haynes committed
102
    """
103 104
    The representation of a stored URL redirection rule. Each URL has
    attributes that are used for analytic purposes.
David Haynes's avatar
David Haynes committed
105
    """
106 107 108
    owner = models.ForeignKey(
        RegisteredUser,
        on_delete="cascade",
109
        verbose_name="RegisteredUser Owner"
110 111 112
    )

    date_created = models.DateTimeField(
113
        "Go Link Creation Date",
114 115 116 117
        default=timezone.now,
    )

    date_expires = models.DateTimeField(
118
        "Go Link Expiry Date",
119 120 121 122 123
        blank=True,
        null=True,
    )

    destination = models.URLField(
124
        "Go Link Destination URL",
125 126 127 128
        max_length=1000,
        default="https://go.gmu.edu",
    )

David Haynes's avatar
David Haynes committed
129
    # Note: min_length cannot exist on a model so it is enforced in forms.py
David Haynes's avatar
David Haynes committed
130
    short = models.CharField(
131
        "Go Shortcode",
132 133
        max_length=20,
        unique=True,
David Haynes's avatar
David Haynes committed
134
        validators=[unique_short_validator, regex_short_validator],
135 136 137 138 139 140
    )

    # TODO Abstract analytics into their own model
    clicks = models.IntegerField(default=0, help_text="")
    qrclicks = models.IntegerField(default=0, help_text="")
    socialclicks = models.IntegerField(default=0, help_text="")
141

142
    def __str__(self):
143
        return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>"
144 145 146 147 148 149

    class Meta:
        ordering = ['short']

    @staticmethod
    def generate_valid_short():
David Haynes's avatar
David Haynes committed
150
        """
151 152
        Generate a short to be used as a default go link if the user does not
        provide a custom one.
David Haynes's avatar
David Haynes committed
153
        """
154
        if cache.get("hashids_counter") is None:
155
            cache.set("hashids_counter", URL.objects.count())
156 157 158 159 160 161 162 163

        short = HASHIDS.encrypt(cache.get("hashids_counter"))

        # Continually generate new shorts until there are no conflicts
        while URL.objects.filter(short__iexact=short).count() > 0:
            short = HASHIDS.encrypt(cache.get("hashids_counter"))

        return short