models.py 5.65 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
# Python stdlib Imports
import string
David Haynes's avatar
David Haynes committed
9
import re
David Haynes's avatar
David Haynes committed
10

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

# Other Imports
21
from hashids import Hashids
22

23
24
"""
Generate the salt and initialize Hashids
25

26
27
28
29
30
31
32
33
Note: the Hashids library already implements several restrictions on character
placement, including repeating or incrementing numbers, or placing curse word
characters adjacent to one another.
"""
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

David Haynes's avatar
David Haynes committed
39

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

51
    full_name = models.CharField(
52
        "verbose name",
53
        max_length=100,
54
55
        default="",
        help_text=""
56
    )
Jean Michel Rouly's avatar
Jean Michel Rouly committed
57

58
    organization = models.CharField(
59
        "verbose name",
60
        max_length=100,
61
62
        default="",
        help_text=""
63
64
    )

65
66
67
68
69
70
    description = models.TextField(
        "verbose name",
        blank=True,
        default="",
        help_text=""
    )
71

72
73
74
75
76
    registered = models.BooleanField(
        "verbose name",
        default=False,
        help_text=""
    )
77

78
79
80
81
82
    approved = models.BooleanField(
        "verbose name",
        default=False,
        help_text=""
    )
83

84
85
86
87
88
    blocked = models.BooleanField(
        "verbose name",
        default=False,
        help_text=""
    )
David Haynes's avatar
David Haynes committed
89

90
    def __str__(self):
91
        return "<Registered User: {0} - Approval Status: {1}>".format(
92
93
            self.user, self.approved
        )
94

David Haynes's avatar
David Haynes committed
95

96
@receiver(post_save, sender=User)
97
def handle_reguser_creation(sender, instance, created, **kwargs):
David Haynes's avatar
David Haynes committed
98
    """
99
    When a post_save is called on a User object (and it is newly created), this
David Haynes's avatar
David Haynes committed
100
    is called to create an associated RegisteredUser.
David Haynes's avatar
David Haynes committed
101
    """
102
103
104
    if created:
        RegisteredUser.objects.create(user=instance)

David Haynes's avatar
David Haynes committed
105

106
class URL(models.Model):
David Haynes's avatar
David Haynes committed
107
    """
108
109
    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
110
    """
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
    # DAY = '1 Day'
    # WEEK = '1 Week'
    # MONTH = '1 Month'
    # CUSTOM = 'Custom Date'
    # NEVER = 'Never'

    # EXPIRATION_CHOICES = (
    #     (DAY, DAY),
    #     (WEEK, WEEK),
    #     (MONTH, MONTH),
    #     (NEVER, NEVER),
    #     (CUSTOM, CUSTOM),
    # ) TODO

    owner = models.ForeignKey(
        RegisteredUser,
        on_delete="cascade",
        verbose_name="verbose name"
    )

    date_created = models.DateTimeField(
        "verbose name",
        default=timezone.now,
        help_text=""
    )

    date_expires = models.DateTimeField(
        "verbose name",
        blank=True,
        null=True,
        # choices=EXPIRATION_CHOICES, TODO
        # default=NEVER, TODO
        help_text=""
    )

    destination = models.URLField(
        max_length=1000,
        default="https://go.gmu.edu",
David Haynes's avatar
David Haynes committed
149
        help_text="The URL to be redirected to when visiting the shortlink."
150
151
    )

David Haynes's avatar
David Haynes committed
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
    def unique_short_validator(value):
        """
        Check to make sure the short url has not been used
        """
        try:
            # if we're able to get a URL with the same short url
            URL.objects.get(short__iexact=value)
            raise ValidationError(f"Short url already exists.")
        except URL.DoesNotExist as ex:
            print(ex)
            return

    def regex_short_validator(value):
        """
        Run the short through our regex validation before insertion into the
        database.
        """
        # http://stackoverflow.com/a/13752628/6762004
        re_emoji = re.compile('^(([\U00010000-\U0010ffff][\U0000200D]?)+)$')
        re_str = re.compile('^([-\w]+)$')
        if not re_emoji.match(value) and not re_str.match(value):
            raise ValidationError(f"Short url fails regex check.")

    # Note: min_length cannot exist on a model so it is enforced in forms.py
David Haynes's avatar
David Haynes committed
176
    short = models.CharField(
177
178
        max_length=20,
        unique=True,
David Haynes's avatar
David Haynes committed
179
180
        validators=[unique_short_validator, regex_short_validator],
        help_text="The shortcode that acts as the unique go link."
181
182
183
184
185
186
    )

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

188
    def __str__(self):
David Haynes's avatar
David Haynes committed
189
        return f"<Owner: {self.owner.user} - destination URL: {self.destination}>"
190
191
192
193
194
195

    class Meta:
        ordering = ['short']

    @staticmethod
    def generate_valid_short():
David Haynes's avatar
David Haynes committed
196
        """
197
198
        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
199
        """
200
        if cache.get("hashids_counter") is None:
201
            cache.set("hashids_counter", URL.objects.count())
202
203
204
205
206
207
208
209

        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