models.py 4.53 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

# Other Imports
19
from hashids import Hashids
20

21
22
"""
Generate the salt and initialize Hashids
23

24
25
26
27
28
29
30
31
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)
32

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

David Haynes's avatar
David Haynes committed
37

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

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

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

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

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

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

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

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

David Haynes's avatar
David Haynes committed
93

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

David Haynes's avatar
David Haynes committed
103

104
class URL(models.Model):
David Haynes's avatar
David Haynes committed
105
    """
106
107
    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
108
    """
109
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
149
150
151
152
153
154
155
156
157
158
159
    # 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",
        help_text=""
    )

    short = models.SlugField(
        max_length=20,
        unique=True,
        help_text=""
    )

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

161
    def __str__(self):
162
163
164
        return '<Owner: %s - Target URL: %s>' % (
            self.owner.user, self.target
        )
165
166
167
168
169
170

    class Meta:
        ordering = ['short']

    @staticmethod
    def generate_valid_short():
David Haynes's avatar
David Haynes committed
171
        """
172
173
        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
174
        """
175
        if cache.get("hashids_counter") is None:
176
            cache.set("hashids_counter", URL.objects.count())
177
178
179
180
181
182
183
184

        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