models.py 13.5 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,
115
116
117
118
119
                                               help_text="""This schedule will
                                                            come into effect
                                                            only for its
                                                            specified duration.
                                                            """)
120
121
122
    # 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
123
                                                                         message='The link is not a valid tapingo link. Example: https://www.tapingo.com/order/restaurant/starbucks-gmu-johnson/',
124
                                                                         code='invalid_tapingo_url')])
125
126
127
    # 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
128

David Haynes's avatar
David Haynes committed
129
    def is_open(self):
130
        """
131
        Return true if this facility is currently open.
132

David Haynes's avatar
David Haynes committed
133
134
        First checks any valid special schedules and then checks the main,
        default, schedule.
135
        """
David Haynes's avatar
David Haynes committed
136
        # Get the current date
137
        today = datetime.datetime.today().date()
David Haynes's avatar
David Haynes committed
138
        # Check special schedules first, loop through all of them
139
140
141
        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
142
                # If a special schedule in in effect
143
                if schedule.valid_start <= today <= schedule.valid_end:
David Haynes's avatar
David Haynes committed
144
145
146
147
                    # Check if the facility is open or not based on that 
                    # special schedule
                    if schedule.is_open_now():
                        # Open
148
                        return True
149
                    else:
David Haynes's avatar
David Haynes committed
150
                        # Closed
151
                        return False
David Haynes's avatar
David Haynes committed
152
153
154
155
        # 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
156
            return True
David Haynes's avatar
David Haynes committed
157
158
159
160
        else:
            # Closed
            return False

161
162
163
164
165
166
167
168
169
170
    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
            if special_schedule.valid_end < datetime.date.today():
                self.special_schedules.remove(special_schedule)

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

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

Ben Waters's avatar
Ben Waters committed
183
class Schedule(TimeStampedModel):
184
    """
David Haynes's avatar
David Haynes committed
185
186
    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.
187
    """
David Haynes's avatar
David Haynes committed
188
    # The name of the schedule
Tyler Hallada's avatar
Tyler Hallada committed
189
    name = models.CharField(max_length=100)
David Haynes's avatar
David Haynes committed
190
191

    # The start date of the schedule
192
    # (inclusive)
193
    valid_start = models.DateField('Start Date', null=True, blank=True,
194
195
                                   help_text="""Date that this schedule goes
                                                into effect""")
David Haynes's avatar
David Haynes committed
196
197
    # The end date of the schedule
    # (inclusive)
198
    valid_end = models.DateField('End Date', null=True, blank=True,
199
200
201
                                 help_text="""Last day that this schedule is
                                              in effect""")

David Haynes's avatar
David Haynes committed
202
    def is_open_now(self):
203
204
205
        """
        Return true if this schedule is open right now.
        """
David Haynes's avatar
David Haynes committed
206
207
208
209
210
211
        # 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
212
                return True
David Haynes's avatar
David Haynes committed
213
        # Closed (all open times are not open)
214
215
        return False

David Haynes's avatar
David Haynes committed
216
217
218
219
    class Meta:
        # Sort by name in admin view
        ordering = ['name']

220
    def __str__(self):
David Haynes's avatar
David Haynes committed
221
222
223
        """
        String representation of a Schedule object.
        """
224
225
226
        return self.name


Ben Waters's avatar
Ben Waters committed
227
class OpenTime(TimeStampedModel):
228
    """
David Haynes's avatar
David Haynes committed
229
230
231
    Represents a time period when a Facility is open.

    Monday = 0, Sunday = 6.
232
    """
David Haynes's avatar
David Haynes committed
233
    # Define integer constants to represent days of the week
234
235
236
237
238
239
240
241
    MONDAY = 0
    TUESDAY = 1
    WEDNESDAY = 2
    THURSDAY = 3
    FRIDAY = 4
    SATURDAY = 5
    SUNDAY = 6

David Haynes's avatar
David Haynes committed
242
    # Tuple that ties a day of the week with an integer representation
243
244
245
246
247
248
249
250
251
252
    DAY_CHOICES = (
        (MONDAY, 'Monday'),
        (TUESDAY, 'Tuesday'),
        (WEDNESDAY, 'Wednesday'),
        (THURSDAY, 'Thursday'),
        (FRIDAY, 'Friday'),
        (SATURDAY, 'Saturday'),
        (SUNDAY, 'Sunday'),
    )

David Haynes's avatar
David Haynes committed
253
    # The schedule that this period of open time is a part of
254
    schedule = models.ForeignKey('Schedule', related_name='open_times')
David Haynes's avatar
David Haynes committed
255
256

    # The day that the open time begins on
257
    start_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
258
    # The day that the open time ends on
259
    end_day = models.IntegerField(default=0, choices=DAY_CHOICES)
David Haynes's avatar
David Haynes committed
260
261
262
263

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

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

316
    def __str__(self):
David Haynes's avatar
David Haynes committed
317
318
319
        """
        String representation of a OpenTime object.
        """
320
        weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday',
321
                    'Saturday', 'Sunday']
322
        return '%s %s to %s %s' % (weekdays[self.start_day],
323
324
325
326
                                   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
327
328
329
330
331
332
333
334
335
336

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
337
338
339
340
    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
341

David Haynes's avatar
David Haynes committed
342
    # Tuple that ties a urgency tag with a string representation
David Haynes's avatar
David Haynes committed
343
344
    URGENCY_CHOICES = (
        (INFO, 'Info'),
345
        (MINOR, 'Minor'),
David Haynes's avatar
David Haynes committed
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
        (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
363
364
365
366
367
368
369
370
    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
371
372
373
374
    def __str__(self):
        """
        String representation of an Alert object.
        """
David Haynes's avatar
David Haynes committed
375
        return "%s" % (self.message)