models.py 14.1 KB
Newer Older
David Haynes's avatar
David Haynes committed
1 2 3 4 5 6 7 8
"""
api/models.py

Define the objects that will be stored in the database and later served through
the API.

https://docs.djangoproject.com/en/1.11/topics/db/models/
"""
9 10 11 12 13 14 15 16
# Future Imports
from __future__ import (absolute_import, division, print_function,
                        unicode_literals)

# Python stdlib Imports
import datetime

# Django Imports
17
from django.db import models
David Haynes's avatar
David Haynes committed
18
from django.contrib.gis.db.models import PointField
19
from django.contrib.auth.models import User
20
from django.core.validators import RegexValidator
David Haynes's avatar
David Haynes committed
21
from django.utils import timezone
David Haynes's avatar
David Haynes committed
22 23

# Other Imports
Ben Waters's avatar
Ben Waters committed
24
from model_utils.models import TimeStampedModel
25
from autoslug import AutoSlugField
26
from taggit.managers import TaggableManager
Tyler Hallada's avatar
Tyler Hallada committed
27

Ben Waters's avatar
Ben Waters committed
28
class Category(TimeStampedModel):
29
    """
David Haynes's avatar
David Haynes committed
30 31 32 33 34 35 36
    Represents the "category" that a Facility falls under. A Category is a
    grouping of Facilities that serve a common/similar purpose.

    ex.
    - Dining
    - Gyms
    - Study areas (Libraries, The Ridge, JC, etc)
37
    """
David Haynes's avatar
David Haynes committed
38
    # The name of the category
39 40 41 42 43
    name = models.CharField(max_length=100)

    class Meta:
        verbose_name = "category"
        verbose_name_plural = "categories"
44
        # Sort by name in admin view.
45 46
        ordering = ['name']

47
    def __str__(self):
David Haynes's avatar
David Haynes committed
48 49 50 51
        """
        String representation of a Category object.
        """
        return self.name
52

David Haynes's avatar
David Haynes committed
53 54
class Location(TimeStampedModel):
    """
55
    Represents a specific location that a Facility can be found.
David Haynes's avatar
David Haynes committed
56
    """
57 58 59 60 61 62 63
    CAMPUS_LOCATIONS = (
        # (set in model, human readable version)
        ("prince william", "Prince William County Science and Technology"),
        ("korea", "Mason Korea"),
        ("fairfax", "Fairfax"),
        ("arlington", "Arlington")
    )
64 65 66 67
    # The building that the facility is located in (on campus).
    building = models.CharField(max_length=100)
    # The physical address of the facility.
    address = models.CharField(max_length=100)
68 69
    campus_region = models.CharField(choices=CAMPUS_LOCATIONS,
                                     max_length=100, default="fairfax")
70
    # Boolean for whether or not the location is "on campus" or not.
David Haynes's avatar
David Haynes committed
71
    on_campus = models.BooleanField(default=True)
David Haynes's avatar
David Haynes committed
72 73
    # A GeoJson coordinate pair that represents the physical location
    coordinate_location = PointField()
David Haynes's avatar
David Haynes committed
74

75 76 77 78 79 80 81 82 83 84 85 86
    class Meta:
        verbose_name = "location"
        verbose_name_plural = "locations"

    def __str__(self):
        """
        String representation of a Location object.
        """
        return 'Found in %s at %s | On Campus: %s' % (self.building,
                                                      self.address,
                                                      self.on_campus)

Ben Waters's avatar
Ben Waters committed
87
class Facility(TimeStampedModel):
David Haynes's avatar
David Haynes committed
88
    """
David Haynes's avatar
David Haynes committed
89 90 91
    Represents a specific facility location. A Facility is some type of
    establishment that has a schedule of open hours and a location that serves
    a specific purpose that can be categorized.
David Haynes's avatar
David Haynes committed
92 93
    """
    # The name of the Facility
David Haynes's avatar
David Haynes committed
94
    facility_name = models.CharField(max_length=100)
David Haynes's avatar
David Haynes committed
95
    # Instead of id
96
    slug = AutoSlugField(populate_from='facility_name', unique=True)
97

David Haynes's avatar
David Haynes committed
98
    # The category that this facility can be grouped with
99
    facility_category = models.ForeignKey('Category',
David Haynes's avatar
David Haynes committed
100
                                          related_name="categories")
David Haynes's avatar
David Haynes committed
101
    # The location object that relates to this facility
102 103
    facility_location = models.ForeignKey('Location',
                                          related_name="facilities")
104

David Haynes's avatar
David Haynes committed
105
    # The User(s) that claim ownership over this facility
106
    owners = models.ManyToManyField(User)
David Haynes's avatar
David Haynes committed
107 108

    # The schedule that is defaulted to if no special schedule is in effect
109
    main_schedule = models.ForeignKey('Schedule',
David Haynes's avatar
David Haynes committed
110 111
                                      related_name='facility_main')
    # A schedule that has a specific start and end date
112
    special_schedules = models.ManyToManyField('Schedule',
David Haynes's avatar
David Haynes committed
113 114
                                               related_name='facility_special',
                                               blank=True,
David Haynes's avatar
David Haynes committed
115 116
                                               help_text="This schedule will come into effect only for its specified duration.")

117 118 119
    # URL, if it exists, to the Tapingo page that is associated with this
    # facility
    tapingo_url = models.URLField(blank=True, validators=[RegexValidator(regex='^https:\/\/www.tapingo.com\/',
David Haynes's avatar
David Haynes committed
120
                                                                         message='The link is not a valid tapingo link. Example: https://www.tapingo.com/order/restaurant/starbucks-gmu-johnson/',
121
                                                                         code='invalid_tapingo_url')])
122 123 124
    # A comma seperate list of words that neatly an aptly describe the product
    # that this facility produces. (ex. for Taco Bell: mexican, taco, cheap)
    facility_product_tags = TaggableManager()
David Haynes's avatar
David Haynes committed
125

David Haynes's avatar
David Haynes committed
126
    def is_open(self):
127
        """
128
        Return true if this facility is currently open.
129

David Haynes's avatar
David Haynes committed
130 131
        First checks any valid special schedules and then checks the main,
        default, schedule.
132
        """
David Haynes's avatar
David Haynes committed
133
        # Get the current date
134
        today = datetime.datetime.today().date()
David Haynes's avatar
David Haynes committed
135
        # Check special schedules first, loop through all of them
136 137 138
        for schedule in self.special_schedules.all():
            # Special schedules must have valid_start and valid_end set
            if schedule.valid_start and schedule.valid_end:
David Haynes's avatar
David Haynes committed
139
                # If a special schedule in in effect
140
                if schedule.valid_start <= today <= schedule.valid_end:
David Haynes's avatar
David Haynes committed
141 142 143 144
                    # Check if the facility is open or not based on that 
                    # special schedule
                    if schedule.is_open_now():
                        # Open
145
                        return True
146
                    else:
David Haynes's avatar
David Haynes committed
147
                        # Closed
148
                        return False
David Haynes's avatar
David Haynes committed
149 150 151 152
        # If no special schedule is in effect then check if the facility is
        # open using the main_schedule
        if self.main_schedule.is_open_now():
            # Open
153
            return True
David Haynes's avatar
David Haynes committed
154 155 156 157
        else:
            # Closed
            return False

158 159 160 161 162 163 164
    def clean_special_schedules(self):
        """
        Loop through every special_schedule and remove entries that have
        expired.
        """
        for special_schedule in self.special_schedules.all():
            # If it ends before today
165
            if special_schedule.valid_end < datetime.date.today() and special_schedule.schedule_for_removal:
166 167
                self.special_schedules.remove(special_schedule)

David Haynes's avatar
David Haynes committed
168 169 170 171
    class Meta:
        verbose_name = "facility"
        verbose_name_plural = "facilities"
        # Sort by name in admin view
David Haynes's avatar
David Haynes committed
172
        ordering = ['facility_name']
173

174
    def __str__(self):
David Haynes's avatar
David Haynes committed
175 176 177
        """
        String representation of a Facility object.
        """
David Haynes's avatar
David Haynes committed
178
        return self.facility_name
179

Ben Waters's avatar
Ben Waters committed
180
class Schedule(TimeStampedModel):
181
    """
David Haynes's avatar
David Haynes committed
182 183
    A period of time between two dates that represents the beginning and end of
    a "schedule" or rather, a collection of open times for a facility.
184
    """
David Haynes's avatar
David Haynes committed
185
    # The name of the schedule
Tyler Hallada's avatar
Tyler Hallada committed
186
    name = models.CharField(max_length=100)
David Haynes's avatar
David Haynes committed
187 188

    # The start date of the schedule
189
    # (inclusive)
190 191
    valid_start = models.DateTimeField('Start Date', null=True, blank=True,
                                       help_text="Date & time that this schedule goes into effect")
David Haynes's avatar
David Haynes committed
192 193
    # The end date of the schedule
    # (inclusive)
194 195 196
    valid_end = models.DateTimeField('End Date', null=True, blank=True,
                                     help_text="Last date & time that this schedule is in effect")

David Haynes's avatar
David Haynes committed
197 198 199
    # Boolean for if this schedule is 24 hours
    twenty_four_hours = models.BooleanField('24 hour schedule?', blank=True,
                                            default=False, help_text="Toggle to True if the Facility is open 24 hours. You do not need to specify any Open Times, it will always be displayed as open.")
200

201 202 203 204
    # Boolean for if this schedule should never be removed.
    schedule_for_removal = models.BooleanField('Schedule for removal', blank=False,
                                               default=True, help_text="Toggle to False if the schedule should never be removed in the backend. By default, all schedules are automatically deleted after they have expired.")

David Haynes's avatar
David Haynes committed
205
    def is_open_now(self):
206 207 208
        """
        Return true if this schedule is open right now.
        """
David Haynes's avatar
David Haynes committed
209 210 211 212 213 214 215 216 217 218 219 220 221 222
        # If the schedule is a 24 hour one, then it's open.
        if self.twenty_four_hours:
            return True
        # Otherwise let's check if it's open.
        else:
            # Loop through all the open times that correspond to this schedule
            for open_time in OpenTime.objects.filter(schedule=self):
                # If the current time we are looking at is open, then the schedule 
                # will say that the facility is open
                if open_time.is_open_now():
                    # Open
                    return True
            # Closed (all open times are not open)
            return False
223

David Haynes's avatar
David Haynes committed
224 225 226 227
    class Meta:
        # Sort by name in admin view
        ordering = ['name']

228
    def __str__(self):
David Haynes's avatar
David Haynes committed
229 230 231
        """
        String representation of a Schedule object.
        """
232 233 234
        return self.name


Ben Waters's avatar
Ben Waters committed
235
class OpenTime(TimeStampedModel):
236
    """
David Haynes's avatar
David Haynes committed
237 238 239
    Represents a time period when a Facility is open.

    Monday = 0, Sunday = 6.
240
    """
David Haynes's avatar
David Haynes committed
241
    # Define integer constants to represent days of the week
242 243 244 245 246 247 248 249
    MONDAY = 0
    TUESDAY = 1
    WEDNESDAY = 2
    THURSDAY = 3
    FRIDAY = 4
    SATURDAY = 5
    SUNDAY = 6

David Haynes's avatar
David Haynes committed
250
    # Tuple that ties a day of the week with an integer representation
251 252 253 254 255 256 257 258 259 260
    DAY_CHOICES = (
        (MONDAY, 'Monday'),
        (TUESDAY, 'Tuesday'),
        (WEDNESDAY, 'Wednesday'),
        (THURSDAY, 'Thursday'),
        (FRIDAY, 'Friday'),
        (SATURDAY, 'Saturday'),
        (SUNDAY, 'Sunday'),
    )

David Haynes's avatar
David Haynes committed
261
    # The schedule that this period of open time is a part of
262
    schedule = models.ForeignKey('Schedule', related_name='open_times')
David Haynes's avatar
David Haynes committed
263 264

    # The day that the open time begins on
265
    start_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
266
    # The day that the open time ends on
267
    end_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
268 269 270 271

    # The time of day that the open time begins at
    start_time = models.TimeField()
    # The time of day that the open time ends
272 273
    end_time = models.TimeField()

David Haynes's avatar
David Haynes committed
274
    def is_open_now(self):
275
        """
David Haynes's avatar
David Haynes committed
276
        Return true if the current time is this OpenTime's range.
277
        """
David Haynes's avatar
David Haynes committed
278
        # Get the current datetime
279
        today = datetime.datetime.today()
David Haynes's avatar
David Haynes committed
280
        # Check that the start occurs before the end
281
        if self.start_day <= self.end_day:
David Haynes's avatar
David Haynes committed
282
            # If today is the start_day
283
            if self.start_day == today.weekday():
David Haynes's avatar
David Haynes committed
284
                # If the start_time has not occurred
285
                if self.start_time > today.time():
David Haynes's avatar
David Haynes committed
286
                    # Closed
287
                    return False
David Haynes's avatar
David Haynes committed
288
            # If the start_day has not occurred
289
            elif self.start_day > today.weekday():
David Haynes's avatar
David Haynes committed
290
                # Closed
291
                return False
David Haynes's avatar
David Haynes committed
292
            # If the end_day is today
293
            if self.end_day == today.weekday():
David Haynes's avatar
David Haynes committed
294
                # If the end_time has already occurred
295
                if self.end_time < today.time():
David Haynes's avatar
David Haynes committed
296
                    # Closed
297
                    return False
David Haynes's avatar
David Haynes committed
298
            # If the end_day has already occurred
299
            elif self.end_day < today.weekday():
David Haynes's avatar
David Haynes committed
300
                # Closed
301
                return False
David Haynes's avatar
David Haynes committed
302
        # The end_day > start_day
303
        else:
David Haynes's avatar
David Haynes committed
304
            # If today is the start_day
305
            if self.start_day == today.weekday():
David Haynes's avatar
David Haynes committed
306
                # If the start_time has not occurred
307
                if self.start_time > today.time():
David Haynes's avatar
David Haynes committed
308
                    # Closed
309
                    return False
David Haynes's avatar
David Haynes committed
310
            # If the end_day is today
311
            if self.end_day == today.weekday():
David Haynes's avatar
David Haynes committed
312
                # If the end_time has already occurred
313
                if self.end_time < today.time():
David Haynes's avatar
David Haynes committed
314
                    # Closed
315
                    return False
David Haynes's avatar
David Haynes committed
316 317
            # If the current date takes place after the end_date but before
            # start_day
318
            if self.end_day < today.weekday() < self.start_day:
David Haynes's avatar
David Haynes committed
319
                # Closed
320
                return False
David Haynes's avatar
David Haynes committed
321
        # All checks passed, it's Open
322
        return True
323

324
    def __str__(self):
David Haynes's avatar
David Haynes committed
325 326 327
        """
        String representation of a OpenTime object.
        """
328
        weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
329
                    'Saturday', 'Sunday']
330
        return '%s %s to %s %s' % (weekdays[self.start_day],
331 332 333 334
                                   self.start_time.strftime("%H:%M:%S"),
                                   # to
                                   weekdays[self.end_day],
                                   self.end_time.strftime("%H:%M:%S"))
David Haynes's avatar
David Haynes committed
335 336 337 338 339 340 341 342 343 344

class Alert(TimeStampedModel):
    """
    Some type of notification that is displayed to clients that conveys a
    message. Past examples include: random closings, modified schedules being
    in effect, election reminder, advertising for other SRCT projects.

    Alerts last for a period of time until the information is no longer dank.
    """
    # Define string constants to represent urgency tag levels
345 346 347 348
    INFO = 'info'  # SRCT announcements
    MINOR = 'minor'  # Holiday hours are in effect
    MAJOR = 'major'  # The hungry patriot is closed today
    EMERGENCY = 'emergency'  # Extreme weather
David Haynes's avatar
David Haynes committed
349

David Haynes's avatar
David Haynes committed
350
    # Tuple that ties a urgency tag with a string representation
David Haynes's avatar
David Haynes committed
351 352
    URGENCY_CHOICES = (
        (INFO, 'Info'),
353
        (MINOR, 'Minor'),
David Haynes's avatar
David Haynes committed
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
        (MAJOR, 'Major'),
        (EMERGENCY, 'Emergency'),
    )

    # The urgency tag for this Alert
    urgency_tag = models.CharField(max_length=10, default='Info',
                                   choices=URGENCY_CHOICES)

    # The text that is displayed that describes the Alert
    message = models.CharField(max_length=140)

    # The date + time that the alert will be start being served
    start_datetime = models.DateTimeField()

    # The date + time that the alert will stop being served
    end_datetime = models.DateTimeField()

David Haynes's avatar
David Haynes committed
371 372 373 374 375 376 377 378
    def is_active(self):
        """
        Check if the current Alert object is active (Alert-able).
        """
        # Get the current datetime
        now = timezone.now()
        return self.start_datetime < now < self.end_datetime

David Haynes's avatar
David Haynes committed
379 380 381 382
    def __str__(self):
        """
        String representation of an Alert object.
        """
David Haynes's avatar
David Haynes committed
383
        return "%s" % (self.message)