models.py 16.6 KB
Newer Older
1 2
#!/usr/bin/env python
# -*- coding: utf-8 -*-
David Haynes's avatar
David Haynes committed
3 4 5 6 7 8 9 10
"""
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/
"""
11 12 13 14
# Python stdlib Imports
import datetime

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

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

26

Ben Waters's avatar
Ben Waters committed
27
class Category(TimeStampedModel):
28
    """
David Haynes's avatar
David Haynes committed
29 30 31 32 33 34 35
    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)
36
    """
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
        ordering = ["name"]
46

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

53

David Haynes's avatar
David Haynes committed
54 55
class Location(TimeStampedModel):
    """
56
    Represents a specific location that a Facility can be found.
David Haynes's avatar
David Haynes committed
57
    """
58

59 60
    CAMPUS_LOCATIONS = (
        # (set in model, human readable version)
61
        ("front royal", "Front Royal"),
62 63
        ("prince william", "Prince William County Science and Technology"),
        ("fairfax", "Fairfax"),
64
        ("arlington", "Arlington"),
65
    )
66 67
    # The building that the facility is located in (on campus).
    building = models.CharField(max_length=100)
68 69 70 71 72 73
    friendly_building = models.CharField(
        "Building Abbreviation",
        help_text="Example: Exploratory Hall becomes EXPL",
        blank=True,
        max_length=10,
    )
74 75
    # The physical address of the facility.
    address = models.CharField(max_length=100)
76 77 78
    campus_region = models.CharField(
        choices=CAMPUS_LOCATIONS, max_length=100, default="fairfax"
    )
79
    # Boolean for whether or not the location is "on campus" or not.
David Haynes's avatar
David Haynes committed
80
    on_campus = models.BooleanField(default=True)
David Haynes's avatar
David Haynes committed
81 82
    # A GeoJson coordinate pair that represents the physical location
    coordinate_location = PointField()
David Haynes's avatar
David Haynes committed
83

84 85 86 87 88 89 90 91
    class Meta:
        verbose_name = "location"
        verbose_name_plural = "locations"

    def __str__(self):
        """
        String representation of a Location object.
        """
92 93 94 95 96 97
        return "Found in %s at %s | On Campus: %s" % (
            self.building,
            self.address,
            self.on_campus,
        )

98

Ben Waters's avatar
Ben Waters committed
99
class Facility(TimeStampedModel):
David Haynes's avatar
David Haynes committed
100
    """
David Haynes's avatar
David Haynes committed
101 102 103
    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
104
    """
105

David Haynes's avatar
David Haynes committed
106
    # The name of the Facility
David Haynes's avatar
David Haynes committed
107
    facility_name = models.CharField(max_length=100)
David Haynes's avatar
David Haynes committed
108
    # Instead of id
109
    slug = AutoSlugField(populate_from="facility_name", unique=True)
110

David Haynes's avatar
David Haynes committed
111
    # The category that this facility can be grouped with
112 113 114
    facility_category = models.ForeignKey(
        "Category", related_name="categories", on_delete=models.CASCADE
    )
David Haynes's avatar
David Haynes committed
115
    # The location object that relates to this facility
116 117 118
    facility_location = models.ForeignKey(
        "Location", related_name="facilities", on_delete=models.CASCADE
    )
119

120 121
    # A note that can be left on a Facility to provide some additional
    # information.
122 123 124 125 126
    note = models.TextField(
        "Facility Note",
        blank=True,
        help_text="Additional information that is sent with this Facility.",
    )
127

128
    # A link to the logo image for this Facility
129 130 131 132 133 134
    logo = models.URLField(
        "Logo URL",
        blank=True,
        default="https://wopen-cdn.dhaynes.xyz/default.png",
        help_text="The absolute URL to the logo image for this Facility.",
    )
135

David Haynes's avatar
David Haynes committed
136
    # The User(s) that claim ownership over this facility
137
    owners = models.ManyToManyField(User)
David Haynes's avatar
David Haynes committed
138 139

    # The schedule that is defaulted to if no special schedule is in effect
140 141 142
    main_schedule = models.ForeignKey(
        "Schedule", related_name="facility_main", on_delete=models.CASCADE
    )
David Haynes's avatar
David Haynes committed
143
    # A schedule that has a specific start and end date
144 145 146 147 148 149
    special_schedules = models.ManyToManyField(
        "Schedule",
        related_name="facility_special",
        blank=True,
        help_text="This schedule will come into effect only for its specified duration.",
    )
David Haynes's avatar
David Haynes committed
150

151 152
    # URL, if it exists, to the Tapingo page that is associated with this
    # facility
153 154 155 156 157 158 159 160 161 162
    tapingo_url = models.URLField(
        blank=True,
        validators=[
            RegexValidator(
                regex="^https:\/\/www.tapingo.com\/",
                message="The link is not a valid tapingo link. Example: https://www.tapingo.com/order/restaurant/starbucks-gmu-johnson/",
                code="invalid_tapingo_url",
            )
        ],
    )
163

164 165
    # Phone number for a location if provided. Accept both ###-###-#### or
    # without dashes
166 167 168 169 170 171 172 173 174 175 176
    phone_number = models.CharField(
        blank=True,
        max_length=18,
        validators=[
            RegexValidator(
                regex="^\(?([0-9]{3})\)?[-.●]?([0-9]{3})[-.●]?([0-9]{4})$",
                message="Invalid phone number",
                code="invalid_phone_number",
            )
        ],
    )
177

David Haynes's avatar
David Haynes committed
178
    # A comma seperate list of words that neatly and aptly describe the product
179
    # that this facility produces. (ex. for Taco Bell: mexican, taco, cheap)
David Haynes's avatar
David Haynes committed
180
    # These words are not shown to the use but are rather used in search.
181 182 183 184
    facility_product_tags = TaggableManager(
        related_name="product_tags",
        help_text="A comma seperate list of words that neatly and aptly describe the product that this facility produces. These words are not shown to the use but are rather used in search.",
    )
David Haynes's avatar
David Haynes committed
185 186 187 188 189

    # Tag a Facility to be shown on the ShopMason or Sodoxo (or both)
    # What's Open sites.
    FACILITY_CLASSES = (
        ("shopmason", "shopMason Facility"),
190 191 192 193 194 195 196
        ("sodoxo", "Sodoxo Facility"),
    )
    facility_classifier = models.CharField(
        choices=FACILITY_CLASSES,
        help_text="Tag this facility to be shown on the ShopMason or Sodoxo What's Open sites.",
        max_length=100,
        blank=True,
David Haynes's avatar
David Haynes committed
197
    )
David Haynes's avatar
David Haynes committed
198

David Haynes's avatar
David Haynes committed
199
    def is_open(self):
200
        """
201
        Return true if this facility is currently open.
202

David Haynes's avatar
David Haynes committed
203 204
        First checks any valid special schedules and then checks the main,
        default, schedule.
205
        """
David Haynes's avatar
David Haynes committed
206
        # Get the current date
207
        today = datetime.datetime.today().date()
David Haynes's avatar
David Haynes committed
208
        # Check special schedules first, loop through all of them
209 210 211
        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
212
                # If a special schedule in in effect
213
                if schedule.valid_start <= today <= schedule.valid_end:
Michael T Bailey's avatar
Michael T Bailey committed
214
                    # Check if the facility is open or not based on that
David Haynes's avatar
David Haynes committed
215 216 217
                    # special schedule
                    if schedule.is_open_now():
                        # Open
218
                        return True
219
                    else:
David Haynes's avatar
David Haynes committed
220
                        # Closed
221
                        return False
David Haynes's avatar
David Haynes committed
222 223 224 225
        # 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
226
            return True
David Haynes's avatar
David Haynes committed
227 228 229 230
        else:
            # Closed
            return False

231
    def clean_schedules(self):
232 233
        """
        Loop through every special_schedule and remove entries that have
234
        expired as well as promote special schedules to main if necessary.
235 236 237
        """
        for special_schedule in self.special_schedules.all():
            # If it ends before today
238 239 240 241
            if (
                special_schedule.valid_end < timezone.now()
                and special_schedule.schedule_for_removal
            ):
242
                self.special_schedules.remove(special_schedule)
243
            elif special_schedule.promote_to_main:
244 245 246 247
                if (
                    special_schedule.valid_start < timezone.now()
                    and special_schedule.valid_end >= timezone.now()
                ):
248
                    self.main_schedule = special_schedule
249

David Haynes's avatar
David Haynes committed
250 251 252 253
    class Meta:
        verbose_name = "facility"
        verbose_name_plural = "facilities"
        # Sort by name in admin view
254
        ordering = ["facility_name"]
255

256
    def __str__(self):
David Haynes's avatar
David Haynes committed
257 258 259
        """
        String representation of a Facility object.
        """
David Haynes's avatar
David Haynes committed
260
        return self.facility_name
261

262

Ben Waters's avatar
Ben Waters committed
263
class Schedule(TimeStampedModel):
264
    """
David Haynes's avatar
David Haynes committed
265 266
    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.
267
    """
268

David Haynes's avatar
David Haynes committed
269
    # The name of the schedule
Tyler Hallada's avatar
Tyler Hallada committed
270
    name = models.CharField(max_length=100)
David Haynes's avatar
David Haynes committed
271 272

    # The start date of the schedule
273
    # (inclusive)
274 275 276 277 278 279
    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
280 281
    # The end date of the schedule
    # (inclusive)
282 283 284 285 286 287
    valid_end = models.DateTimeField(
        "End Date",
        null=True,
        blank=True,
        help_text="Last date & time that this schedule is in effect",
    )
288

David Haynes's avatar
David Haynes committed
289
    # Boolean for if this schedule is 24 hours
290 291 292 293 294 295
    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.",
    )
296

297
    # Boolean for if this schedule should never be removed.
298 299 300 301 302 303
    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.",
    )
304
    # Boolean for if this schedule should become the main schedule at the point
Michael T Bailey's avatar
Michael T Bailey committed
305
    # it goes live
306 307 308 309 310 311
    promote_to_main = models.BooleanField(
        "Schedule for promotion?",
        blank=False,
        default=False,
        help_text="Upon the start of the schedule, it will be promoted to become the main schedule of the Facility it is attached to rather than a special schedule.",
    )
312

David Haynes's avatar
David Haynes committed
313
    def is_open_now(self):
314 315 316
        """
        Return true if this schedule is open right now.
        """
David Haynes's avatar
David Haynes committed
317 318 319 320 321 322 323
        # 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):
Michael T Bailey's avatar
Michael T Bailey committed
324
                # If the current time we are looking at is open, then the schedule
David Haynes's avatar
David Haynes committed
325 326 327 328 329 330
                # 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
331

David Haynes's avatar
David Haynes committed
332 333
    class Meta:
        # Sort by name in admin view
334
        ordering = ["name"]
David Haynes's avatar
David Haynes committed
335

336
    def __str__(self):
David Haynes's avatar
David Haynes committed
337 338 339
        """
        String representation of a Schedule object.
        """
340 341 342
        return self.name


Ben Waters's avatar
Ben Waters committed
343
class OpenTime(TimeStampedModel):
344
    """
David Haynes's avatar
David Haynes committed
345 346 347
    Represents a time period when a Facility is open.

    Monday = 0, Sunday = 6.
348
    """
349

David Haynes's avatar
David Haynes committed
350
    # Define integer constants to represent days of the week
351 352 353 354 355 356 357 358
    MONDAY = 0
    TUESDAY = 1
    WEDNESDAY = 2
    THURSDAY = 3
    FRIDAY = 4
    SATURDAY = 5
    SUNDAY = 6

David Haynes's avatar
David Haynes committed
359
    # Tuple that ties a day of the week with an integer representation
360
    DAY_CHOICES = (
361 362 363 364 365 366 367
        (MONDAY, "Monday"),
        (TUESDAY, "Tuesday"),
        (WEDNESDAY, "Wednesday"),
        (THURSDAY, "Thursday"),
        (FRIDAY, "Friday"),
        (SATURDAY, "Saturday"),
        (SUNDAY, "Sunday"),
368 369
    )

David Haynes's avatar
David Haynes committed
370
    # The schedule that this period of open time is a part of
371 372 373
    schedule = models.ForeignKey(
        "Schedule", related_name="open_times", on_delete=models.CASCADE
    )
David Haynes's avatar
David Haynes committed
374 375

    # The day that the open time begins on
376
    start_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
377
    # The day that the open time ends on
378
    end_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
379 380 381 382

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

David Haynes's avatar
David Haynes committed
385
    def is_open_now(self):
386
        """
David Haynes's avatar
David Haynes committed
387
        Return true if the current time is this OpenTime's range.
388
        """
David Haynes's avatar
David Haynes committed
389
        # Get the current datetime
390
        today = datetime.datetime.today()
David Haynes's avatar
David Haynes committed
391
        # Check that the start occurs before the end
392
        if self.start_day <= self.end_day:
David Haynes's avatar
David Haynes committed
393
            # If today is the start_day
394
            if self.start_day == today.weekday():
David Haynes's avatar
David Haynes committed
395
                # If the start_time has not occurred
396
                if self.start_time > today.time():
David Haynes's avatar
David Haynes committed
397
                    # Closed
398
                    return False
David Haynes's avatar
David Haynes committed
399
            # If the start_day has not occurred
400
            elif self.start_day > today.weekday():
David Haynes's avatar
David Haynes committed
401
                # Closed
402
                return False
David Haynes's avatar
David Haynes committed
403
            # If the end_day is today
404
            if self.end_day == today.weekday():
David Haynes's avatar
David Haynes committed
405
                # If the end_time has already occurred
406
                if self.end_time < today.time():
David Haynes's avatar
David Haynes committed
407
                    # Closed
408
                    return False
David Haynes's avatar
David Haynes committed
409
            # If the end_day has already occurred
410
            elif self.end_day < today.weekday():
David Haynes's avatar
David Haynes committed
411
                # Closed
412
                return False
David Haynes's avatar
David Haynes committed
413
        # The end_day > start_day
414
        else:
David Haynes's avatar
David Haynes committed
415
            # If today is the start_day
416
            if self.start_day == today.weekday():
David Haynes's avatar
David Haynes committed
417
                # If the start_time has not occurred
418
                if self.start_time > today.time():
David Haynes's avatar
David Haynes committed
419
                    # Closed
420
                    return False
David Haynes's avatar
David Haynes committed
421
            # If the end_day is today
422
            if self.end_day == today.weekday():
David Haynes's avatar
David Haynes committed
423
                # If the end_time has already occurred
424
                if self.end_time < today.time():
David Haynes's avatar
David Haynes committed
425
                    # Closed
426
                    return False
David Haynes's avatar
David Haynes committed
427 428
            # If the current date takes place after the end_date but before
            # start_day
429
            if self.end_day < today.weekday() < self.start_day:
David Haynes's avatar
David Haynes committed
430
                # Closed
431
                return False
David Haynes's avatar
David Haynes committed
432
        # All checks passed, it's Open
433
        return True
434

435
    def __str__(self):
David Haynes's avatar
David Haynes committed
436 437 438
        """
        String representation of a OpenTime object.
        """
439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455
        weekdays = [
            "Monday",
            "Tuesday",
            "Wednesday",
            "Thursday",
            "Friday",
            "Saturday",
            "Sunday",
        ]
        return "%s %s to %s %s" % (
            weekdays[self.start_day],
            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
456 457 458 459 460 461 462 463 464

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.
    """
465

David Haynes's avatar
David Haynes committed
466
    # Define string constants to represent urgency tag levels
467 468 469 470
    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
471

David Haynes's avatar
David Haynes committed
472
    # Tuple that ties a urgency tag with a string representation
David Haynes's avatar
David Haynes committed
473
    URGENCY_CHOICES = (
474 475 476 477
        (INFO, "Info"),
        (MINOR, "Minor"),
        (MAJOR, "Major"),
        (EMERGENCY, "Emergency"),
David Haynes's avatar
David Haynes committed
478 479 480
    )

    # The urgency tag for this Alert
481 482 483
    urgency_tag = models.CharField(
        max_length=10, default="Info", choices=URGENCY_CHOICES
    )
484

David Haynes's avatar
David Haynes committed
485
    # The text that is displayed that describes the Alert
486 487 488
    subject = models.CharField(max_length=130)
    body = models.TextField()
    url = models.URLField(max_length=200)
David Haynes's avatar
David Haynes committed
489 490 491 492 493 494 495

    # 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
496 497 498 499 500 501 502 503
    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
504 505 506 507
    def __str__(self):
        """
        String representation of an Alert object.
        """
508 509
        return "{0} \n {1} \n {3}".format(self.subject, self.body, self.url)
        # Returns the subject, body, and url fields