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