models.py 4.81 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
    # 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=""
    )

David Haynes's avatar
David Haynes committed
150 151 152 153 154 155 156 157
    # TODO Validator for Slug + Emoji
    """
    # http://stackoverflow.com/a/13752628/6762004
    RE_EMOJI = re.compile('[\U00010000-\U0010ffff]', flags=re.UNICODE)
    slug_unicode_re = _lazy_re_compile(r'^[-\w]+\Z')
    slug_re = _lazy_re_compile(r'^[-a-zA-Z0-9_]+\Z')
    """
    short = models.CharField(
158 159 160 161 162 163 164 165 166
        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="")
167

168
    def __str__(self):
David Haynes's avatar
David Haynes committed
169
        return '<Owner: %s - destination URL: %s>' % (
David Haynes's avatar
David Haynes committed
170
            self.owner.user, self.destination
171
        )
172 173 174 175 176 177

    class Meta:
        ordering = ['short']

    @staticmethod
    def generate_valid_short():
David Haynes's avatar
David Haynes committed
178
        """
179 180
        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
181
        """
182
        if cache.get("hashids_counter") is None:
183
            cache.set("hashids_counter", URL.objects.count())
184 185 186 187 188 189 190 191

        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