models.py 4.24 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
from django.conf import settings
18
19

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

24
25
26
27
# 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.
David Haynes's avatar
David Haynes committed
28
SIMILAR_CHARS = set(["b", "G", "6", "g", "q", "l", "1", "I", "S", "5", "O", "0"])
29
ALPHANUMERICS = set(string.ascii_letters + string.digits)
David Haynes's avatar
David Haynes committed
30
31
32
LINK_CHARS = "".join(ALPHANUMERICS - SIMILAR_CHARS)

HASHIDS = Hashids(salt="srct.gmu.edu", alphabet=(LINK_CHARS))
33

Jean Michel Rouly's avatar
Jean Michel Rouly committed
34

35
class RegisteredUser(models.Model):
36
    """
37
38
    Wrapper model for the built in User model which stores data pertaining to
    the registration / approval / blocked status of a django user.
39
    """
David Haynes's avatar
David Haynes committed
40

41
    user = models.OneToOneField(
David Haynes's avatar
David Haynes committed
42
        User, on_delete="cascade", verbose_name="Django User Object"
43
    )
Jean Michel Rouly's avatar
Jean Michel Rouly committed
44

David Haynes's avatar
David Haynes committed
45
    full_name = models.CharField("Full Name", max_length=100, default="")
Jean Michel Rouly's avatar
Jean Michel Rouly committed
46

David Haynes's avatar
David Haynes committed
47
    organization = models.CharField("Organization", max_length=100, default="")
48

David Haynes's avatar
David Haynes committed
49
    description = models.TextField("Signup Description", blank=True, default="")
50

David Haynes's avatar
David Haynes committed
51
    registered = models.BooleanField("Registration Status", default=False)
52

David Haynes's avatar
David Haynes committed
53
    approved = models.BooleanField("Approval Status", default=False)
54

David Haynes's avatar
David Haynes committed
55
    blocked = models.BooleanField("Blocked Status", default=False)
David Haynes's avatar
David Haynes committed
56

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

David Haynes's avatar
David Haynes committed
60

61
@receiver(post_save, sender=User)
62
def handle_reguser_creation(sender, instance, created, **kwargs):
David Haynes's avatar
David Haynes committed
63
    """
64
    When a post_save is called on a User object (and it is newly created), this
David Haynes's avatar
David Haynes committed
65
    is called to create an associated RegisteredUser.
David Haynes's avatar
David Haynes committed
66
    """
67
    if created:
David Haynes's avatar
David Haynes committed
68
        RegisteredUser.objects.create(user=instance, full_name=instance.get_full_name())
69

David Haynes's avatar
David Haynes committed
70

71
72
73
@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
    if created:
David Haynes's avatar
David Haynes committed
74
75
        Token.objects.create(user=instance)

76

77
class URL(models.Model):
David Haynes's avatar
David Haynes committed
78
    """
79
80
    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
81
    """
David Haynes's avatar
David Haynes committed
82

83
    owner = models.ForeignKey(
David Haynes's avatar
David Haynes committed
84
        RegisteredUser, on_delete="cascade", verbose_name="RegisteredUser Owner"
85
86
    )

David Haynes's avatar
David Haynes committed
87
    date_created = models.DateTimeField("Go Link Creation Date", default=timezone.now)
88

David Haynes's avatar
David Haynes committed
89
    date_expires = models.DateTimeField("Go Link Expiry Date", blank=True, null=True)
90
91

    destination = models.URLField(
David Haynes's avatar
David Haynes committed
92
        "Go Link Destination URL", max_length=1000, default="https://go.gmu.edu"
93
94
    )

David Haynes's avatar
David Haynes committed
95
    # Note: min_length cannot exist on a model so it is enforced in forms.py
David Haynes's avatar
David Haynes committed
96
    short = models.CharField(
97
        "Go Shortcode",
98
99
        max_length=20,
        unique=True,
David Haynes's avatar
David Haynes committed
100
        validators=[unique_short_validator, regex_short_validator],
101
102
103
104
105
106
    )

    # 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="")
107

108
    def __str__(self):
109
        return f"<Owner: {self.owner.user} - Destination URL: {self.destination}>"
110
111

    class Meta:
David Haynes's avatar
David Haynes committed
112
        ordering = ["short"]
113
114
115

    @staticmethod
    def generate_valid_short():
David Haynes's avatar
David Haynes committed
116
        """
117
118
        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
119
        """
120
        if cache.get("hashids_counter") is None:
121
            cache.set("hashids_counter", URL.objects.count())
122
123
124
125
126
127
128
129

        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