models.py 16.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
    # The building that the facility is located in (on campus).
    building = models.CharField(max_length=100)
66
67
68
69
    friendly_building = models.CharField('Building Abbreviation',
                                         help_text="Example: Exploratory Hall becomes EXPL",
                                         blank=True,
                                         max_length=10)
70
71
    # The physical address of the facility.
    address = models.CharField(max_length=100)
72
73
    campus_region = models.CharField(choices=CAMPUS_LOCATIONS,
                                     max_length=100, default="fairfax")
74
    # Boolean for whether or not the location is "on campus" or not.
David Haynes's avatar
David Haynes committed
75
    on_campus = models.BooleanField(default=True)
David Haynes's avatar
David Haynes committed
76
77
    # A GeoJson coordinate pair that represents the physical location
    coordinate_location = PointField()
David Haynes's avatar
David Haynes committed
78

79
80
81
82
83
84
85
86
87
88
89
90
    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
91
class Facility(TimeStampedModel):
David Haynes's avatar
David Haynes committed
92
    """
David Haynes's avatar
David Haynes committed
93
94
95
    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
96
97
    """
    # The name of the Facility
David Haynes's avatar
David Haynes committed
98
    facility_name = models.CharField(max_length=100)
David Haynes's avatar
David Haynes committed
99
    # Instead of id
100
    slug = AutoSlugField(populate_from='facility_name', unique=True)
101

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

109
110
111
112
113
    # A note that can be left on a Facility to provide some additional
    # information.
    note = models.TextField('Facility Note', blank=True,
                            help_text="Additional information that is sent with this Facility.")

114
115
116
117
118
    # A link to the logo image for this Facility
    logo = models.URLField('Logo URL', blank=True,
                           default="https://imgur.com/q2Phkn9.png",
                           help_text="The absolute URL to the logo image for this Facility.")

David Haynes's avatar
David Haynes committed
119
    # The User(s) that claim ownership over this facility
120
    owners = models.ManyToManyField(User)
David Haynes's avatar
David Haynes committed
121
122

    # The schedule that is defaulted to if no special schedule is in effect
123
    main_schedule = models.ForeignKey('Schedule',
David Haynes's avatar
David Haynes committed
124
125
                                      related_name='facility_main')
    # A schedule that has a specific start and end date
126
    special_schedules = models.ManyToManyField('Schedule',
David Haynes's avatar
David Haynes committed
127
128
                                               related_name='facility_special',
                                               blank=True,
David Haynes's avatar
David Haynes committed
129
130
                                               help_text="This schedule will come into effect only for its specified duration.")

131
132
133
    # 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
134
                                                                         message='The link is not a valid tapingo link. Example: https://www.tapingo.com/order/restaurant/starbucks-gmu-johnson/',
135
                                                                         code='invalid_tapingo_url')])
136
137
138
139
140

    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')])
                                                                  
141
142
143
    # 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
144

David Haynes's avatar
David Haynes committed
145
    def is_open(self):
146
        """
147
        Return true if this facility is currently open.
148

David Haynes's avatar
David Haynes committed
149
150
        First checks any valid special schedules and then checks the main,
        default, schedule.
151
        """
David Haynes's avatar
David Haynes committed
152
        # Get the current date
153
        today = datetime.datetime.today().date()
David Haynes's avatar
David Haynes committed
154
        # Check special schedules first, loop through all of them
155
156
157
        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
158
                # If a special schedule in in effect
159
                if schedule.valid_start <= today <= schedule.valid_end:
David Haynes's avatar
David Haynes committed
160
161
162
163
                    # Check if the facility is open or not based on that 
                    # special schedule
                    if schedule.is_open_now():
                        # Open
164
                        return True
165
                    else:
David Haynes's avatar
David Haynes committed
166
                        # Closed
167
                        return False
David Haynes's avatar
David Haynes committed
168
169
170
171
        # 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
172
            return True
David Haynes's avatar
David Haynes committed
173
174
175
176
        else:
            # Closed
            return False

177
    def clean_schedules(self):
178
179
        """
        Loop through every special_schedule and remove entries that have
180
        expired as well as promote special schedules to main if necessary.
181
182
183
        """
        for special_schedule in self.special_schedules.all():
            # If it ends before today
184
            if special_schedule.valid_end < datetime.date.today() and special_schedule.schedule_for_removal:
185
                self.special_schedules.remove(special_schedule)
186
            elif special_schedule.promote_to_main:
187
188
                if special_schedule.valid_start < datetime.date.today() and special_schedule.valid_end >= datetime.date.today():
                    self.main_schedule = special_schedule
189

David Haynes's avatar
David Haynes committed
190
191
192
193
    class Meta:
        verbose_name = "facility"
        verbose_name_plural = "facilities"
        # Sort by name in admin view
David Haynes's avatar
David Haynes committed
194
        ordering = ['facility_name']
195

196
    def __str__(self):
David Haynes's avatar
David Haynes committed
197
198
199
        """
        String representation of a Facility object.
        """
David Haynes's avatar
David Haynes committed
200
        return self.facility_name
201

Ben Waters's avatar
Ben Waters committed
202
class Schedule(TimeStampedModel):
203
    """
David Haynes's avatar
David Haynes committed
204
205
    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.
206
    """
David Haynes's avatar
David Haynes committed
207
    # The name of the schedule
Tyler Hallada's avatar
Tyler Hallada committed
208
    name = models.CharField(max_length=100)
David Haynes's avatar
David Haynes committed
209
210

    # The start date of the schedule
211
    # (inclusive)
212
213
    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
214
215
    # The end date of the schedule
    # (inclusive)
216
217
218
    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
219
220
221
    # 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.")
222

223
    # Boolean for if this schedule should never be removed.
224
225
226
227
228
229
230
231
    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.")
    # Boolean for if this schedule should become the main schedule at the point
    # it goes live 
    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.")
232

David Haynes's avatar
David Haynes committed
233
    def is_open_now(self):
234
235
236
        """
        Return true if this schedule is open right now.
        """
David Haynes's avatar
David Haynes committed
237
238
239
240
241
242
243
244
245
246
247
248
249
250
        # 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
251

David Haynes's avatar
David Haynes committed
252
253
254
255
    class Meta:
        # Sort by name in admin view
        ordering = ['name']

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


Ben Waters's avatar
Ben Waters committed
263
class OpenTime(TimeStampedModel):
264
    """
David Haynes's avatar
David Haynes committed
265
266
267
    Represents a time period when a Facility is open.

    Monday = 0, Sunday = 6.
268
    """
David Haynes's avatar
David Haynes committed
269
    # Define integer constants to represent days of the week
270
271
272
273
274
275
276
277
    MONDAY = 0
    TUESDAY = 1
    WEDNESDAY = 2
    THURSDAY = 3
    FRIDAY = 4
    SATURDAY = 5
    SUNDAY = 6

David Haynes's avatar
David Haynes committed
278
    # Tuple that ties a day of the week with an integer representation
279
280
281
282
283
284
285
286
287
288
    DAY_CHOICES = (
        (MONDAY, 'Monday'),
        (TUESDAY, 'Tuesday'),
        (WEDNESDAY, 'Wednesday'),
        (THURSDAY, 'Thursday'),
        (FRIDAY, 'Friday'),
        (SATURDAY, 'Saturday'),
        (SUNDAY, 'Sunday'),
    )

David Haynes's avatar
David Haynes committed
289
    # The schedule that this period of open time is a part of
290
    schedule = models.ForeignKey('Schedule', related_name='open_times')
David Haynes's avatar
David Haynes committed
291
292

    # The day that the open time begins on
293
    start_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
294
    # The day that the open time ends on
295
    end_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
296
297
298
299

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

David Haynes's avatar
David Haynes committed
302
    def is_open_now(self):
303
        """
David Haynes's avatar
David Haynes committed
304
        Return true if the current time is this OpenTime's range.
305
        """
David Haynes's avatar
David Haynes committed
306
        # Get the current datetime
307
        today = datetime.datetime.today()
David Haynes's avatar
David Haynes committed
308
        # Check that the start occurs before the end
309
        if self.start_day <= self.end_day:
David Haynes's avatar
David Haynes committed
310
            # If today is the start_day
311
            if self.start_day == today.weekday():
David Haynes's avatar
David Haynes committed
312
                # If the start_time has not occurred
313
                if self.start_time > today.time():
David Haynes's avatar
David Haynes committed
314
                    # Closed
315
                    return False
David Haynes's avatar
David Haynes committed
316
            # If the start_day has not occurred
317
            elif self.start_day > today.weekday():
David Haynes's avatar
David Haynes committed
318
                # Closed
319
                return False
David Haynes's avatar
David Haynes committed
320
            # If the end_day is today
321
            if self.end_day == today.weekday():
David Haynes's avatar
David Haynes committed
322
                # If the end_time has already occurred
323
                if self.end_time < today.time():
David Haynes's avatar
David Haynes committed
324
                    # Closed
325
                    return False
David Haynes's avatar
David Haynes committed
326
            # If the end_day has already occurred
327
            elif self.end_day < today.weekday():
David Haynes's avatar
David Haynes committed
328
                # Closed
329
                return False
David Haynes's avatar
David Haynes committed
330
        # The end_day > start_day
331
        else:
David Haynes's avatar
David Haynes committed
332
            # If today is the start_day
333
            if self.start_day == today.weekday():
David Haynes's avatar
David Haynes committed
334
                # If the start_time has not occurred
335
                if self.start_time > today.time():
David Haynes's avatar
David Haynes committed
336
                    # Closed
337
                    return False
David Haynes's avatar
David Haynes committed
338
            # If the end_day is today
339
            if self.end_day == today.weekday():
David Haynes's avatar
David Haynes committed
340
                # If the end_time has already occurred
341
                if self.end_time < today.time():
David Haynes's avatar
David Haynes committed
342
                    # Closed
343
                    return False
David Haynes's avatar
David Haynes committed
344
345
            # If the current date takes place after the end_date but before
            # start_day
346
            if self.end_day < today.weekday() < self.start_day:
David Haynes's avatar
David Haynes committed
347
                # Closed
348
                return False
David Haynes's avatar
David Haynes committed
349
        # All checks passed, it's Open
350
        return True
351

352
    def __str__(self):
David Haynes's avatar
David Haynes committed
353
354
355
        """
        String representation of a OpenTime object.
        """
356
        weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
357
                    'Saturday', 'Sunday']
358
        return '%s %s to %s %s' % (weekdays[self.start_day],
359
360
361
362
                                   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
363
364
365
366
367
368
369
370
371
372

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
373
374
375
376
    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
377

David Haynes's avatar
David Haynes committed
378
    # Tuple that ties a urgency tag with a string representation
David Haynes's avatar
David Haynes committed
379
380
    URGENCY_CHOICES = (
        (INFO, 'Info'),
381
        (MINOR, 'Minor'),
David Haynes's avatar
David Haynes committed
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
        (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
399
400
401
402
403
404
405
406
    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
407
408
409
410
    def __str__(self):
        """
        String representation of an Alert object.
        """
David Haynes's avatar
David Haynes committed
411
        return "%s" % (self.message)