Unit Tests / test (push) Successful in 5s
## Summary Closes #32. - Keep an event on Upcoming until its end date is over. If `end_time` is set, it drops off once that time passes on the end date, so an in-progress multi-day event stays visible. - Render the Past tab from `past_events` instead of `future_events`. - Show the start–end date range when the dates differ. ## Test plan - [ ] Multi-day event that started yesterday and ends tomorrow appears under Upcoming - [ ] Event whose end time has already passed appears under Past - [ ] Future single-day event still appears under Upcoming - [ ] Past tab lists finished events only - [ ] `uv run python manage.py test schasite.tests.CalendarEventTests` Reviewed-on: #33
243 lines
9.5 KiB
Python
243 lines
9.5 KiB
Python
from django.db import models
|
|
from phonenumber_field.modelfields import PhoneNumberField
|
|
import datetime
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
|
|
class TimeInfoBase(models.Model):
|
|
|
|
created = models.DateTimeField(default=timezone.now)
|
|
last_modified = models.DateTimeField(default=timezone.now)
|
|
|
|
class Meta:
|
|
abstract = True
|
|
|
|
def save(self, **kwargs):
|
|
if not kwargs.pop("skip_last_modified", False) and not hasattr(self, "skip_last_modified"):
|
|
self.last_modified = timezone.now()
|
|
if kwargs.get("update_fields") is not None:
|
|
kwargs["update_fields"] = list({*kwargs["update_fields"], "last_modified"})
|
|
|
|
super().save(**kwargs)
|
|
|
|
# Create your models here.
|
|
class UsefulLinks(TimeInfoBase):
|
|
name = models.CharField(max_length=256)
|
|
url = models.CharField(max_length=256)
|
|
|
|
|
|
class Membership(TimeInfoBase):
|
|
children = models.CharField(max_length=256, default="", blank=True, null=True)
|
|
|
|
def get_address_str(self):
|
|
if self.addressmodel1 is None:
|
|
return "No Address"
|
|
elif self.addressmodel1.address_1 is None:
|
|
return "No Address"
|
|
else:
|
|
return self.addressmodel1.address_1
|
|
|
|
def get_person_1(self):
|
|
emails = [
|
|
item.email
|
|
for item in MembershipPerson.objects.filter(membership_id=self.id)
|
|
]
|
|
# remove all the Nones
|
|
filtered_emails = [i for i in emails if i is not None]
|
|
if len(filtered_emails) == 0:
|
|
return "No Person"
|
|
else:
|
|
return " | ".join(filtered_emails)
|
|
|
|
def __str__(self):
|
|
return self.get_address_str() + " | " + self.get_person_1()
|
|
|
|
|
|
class AddressModel1(TimeInfoBase):
|
|
membership = models.OneToOneField(Membership, on_delete=models.CASCADE)
|
|
address_1 = models.CharField(max_length=128)
|
|
address_2 = models.CharField(max_length=128, blank=True)
|
|
city = models.CharField(max_length=128)
|
|
state = models.CharField(max_length=2)
|
|
zip_code = models.CharField(max_length=5)
|
|
|
|
|
|
class CalendarEvent(TimeInfoBase):
|
|
event_name = models.CharField(max_length=256)
|
|
start_date = models.DateField(blank=True, null=True)
|
|
end_date = models.DateField(blank=True, null=True)
|
|
start_time = models.TimeField(blank=True, null=True)
|
|
end_time = models.TimeField(blank=True, null=True)
|
|
location_name = models.CharField(max_length=256, blank=True, null=True)
|
|
coordinator_email = models.EmailField(max_length=256, blank=True, null=True)
|
|
event_link_name = models.CharField(max_length=64, blank=True, null=True)
|
|
event_url = models.URLField(max_length=256, blank=True, null=True)
|
|
#description= models.CharField(max_length=1024, default="")
|
|
|
|
def has_date(self):
|
|
return not self.start_date is None
|
|
|
|
def _has_ended(self, now=None):
|
|
"""True once the event is over. In-progress multi-day events stay open
|
|
through end_date, and through end_time on that date when one is set.
|
|
"""
|
|
if not self.has_date():
|
|
return None
|
|
if now is None:
|
|
now = datetime.datetime.now()
|
|
|
|
end_date = self.end_date or self.start_date
|
|
if self.end_time:
|
|
return now >= datetime.datetime.combine(end_date, self.end_time)
|
|
return now.date() > end_date
|
|
|
|
def past_event(self):
|
|
ended = self._has_ended()
|
|
if ended is None:
|
|
return False
|
|
return ended
|
|
|
|
def future_event(self):
|
|
"""Upcoming or currently in progress."""
|
|
ended = self._has_ended()
|
|
if ended is None:
|
|
return False
|
|
return not ended
|
|
|
|
def no_date(self):
|
|
return not self.has_date()
|
|
|
|
def time_range_display(self):
|
|
"""Format start/end times for templates, e.g. '11:00 AM - 3:00 PM'."""
|
|
from django.utils.formats import time_format
|
|
|
|
if not self.start_time and not self.end_time:
|
|
return ""
|
|
|
|
start = time_format(self.start_time, "g:i A") if self.start_time else ""
|
|
end = time_format(self.end_time, "g:i A") if self.end_time else ""
|
|
if start and end:
|
|
return f"{start} - {end}"
|
|
return start or end
|
|
|
|
|
|
class CalendarEventAddressModel(TimeInfoBase):
|
|
calendar_event = models.OneToOneField(CalendarEvent, on_delete=models.CASCADE)
|
|
address_1 = models.CharField(max_length=128)
|
|
address_2 = models.CharField(max_length=128, blank=True)
|
|
city = models.CharField(max_length=128)
|
|
state = models.CharField(max_length=2)
|
|
zip_code = models.CharField(max_length=5)
|
|
|
|
def __str__(self):
|
|
return (
|
|
self.address_1 + ", " + self.city + ", " + self.state + " " + self.zip_code
|
|
)
|
|
|
|
|
|
class MembershipPerson(TimeInfoBase):
|
|
membership = models.ForeignKey(
|
|
Membership, on_delete=models.CASCADE, blank=True, null=True
|
|
)
|
|
first_name = models.CharField(max_length=256, blank=True, null=True)
|
|
last_name = models.CharField(max_length=256, blank=True, null=True)
|
|
phone_number = PhoneNumberField(blank=False, null=True)
|
|
email = models.EmailField(max_length=254, blank=True, null=True)
|
|
|
|
def __str__(self):
|
|
return self.email if self.email else "No email"
|
|
|
|
|
|
class MembershipCommittee(TimeInfoBase):
|
|
membership = models.OneToOneField(Membership, on_delete=models.CASCADE)
|
|
block_captain = models.BooleanField(default=False, blank=True, null=True)
|
|
coordinator = models.BooleanField(default=False, blank=True, null=True)
|
|
egg_hunt = models.BooleanField(default=False, blank=True, null=True)
|
|
spring_garage_sale = models.BooleanField(default=False, blank=True, null=True)
|
|
golf_outing = models.BooleanField(default=False, blank=True, null=True)
|
|
ice_cream_social = models.BooleanField(default=False, blank=True, null=True)
|
|
fall_garage_sale = models.BooleanField(default=False, blank=True, null=True)
|
|
halloween_party = models.BooleanField(default=False, blank=True, null=True)
|
|
santa_visit = models.BooleanField(default=False, blank=True, null=True)
|
|
website = models.BooleanField(default=False, blank=True, null=True)
|
|
civic_affair = models.BooleanField(default=False, blank=True, null=True)
|
|
phone_directory = models.BooleanField(default=False, blank=True, null=True)
|
|
no_preference = models.BooleanField(default=False, blank=True, null=True)
|
|
|
|
|
|
class MembershipServices(TimeInfoBase):
|
|
membership = models.OneToOneField(Membership, on_delete=models.CASCADE)
|
|
babysitting = models.BooleanField(default=False, blank=True, null=True)
|
|
lawn_mowing = models.BooleanField(default=False, blank=True, null=True)
|
|
snow_shoveling = models.BooleanField(default=False, blank=True, null=True)
|
|
leaf_raking = models.BooleanField(default=False, blank=True, null=True)
|
|
petsitting = models.BooleanField(default=False, blank=True, null=True)
|
|
house_sitting = models.BooleanField(default=False, blank=True, null=True)
|
|
other = models.BooleanField(default=False, blank=True, null=True)
|
|
other_desc = models.CharField(default="", blank=True, null=True, max_length=256)
|
|
|
|
|
|
class Payments(TimeInfoBase):
|
|
date = models.DateField(default=timezone.now)
|
|
status = models.CharField(default="Completed", max_length=256)
|
|
email = models.EmailField(blank=True, null=True)
|
|
person = models.ForeignKey(
|
|
MembershipPerson, on_delete=models.CASCADE, blank=True, null=True
|
|
)
|
|
|
|
|
|
class SCHAOfficer(TimeInfoBase):
|
|
name = models.CharField(max_length=255)
|
|
position = models.CharField(max_length=255)
|
|
email = models.EmailField(max_length=255)
|
|
|
|
### NOT USED YET ###
|
|
|
|
class CommunitySchools(TimeInfoBase):
|
|
name = models.CharField(max_length=255)
|
|
name_school_url = models.URLField(max_length=256)
|
|
rating_url = models.URLField(max_length=256)
|
|
rating = models.DecimalField(max_digits=5, decimal_places=2)
|
|
distance = models.DecimalField(max_digits=3, decimal_places=1)
|
|
principal = models.CharField(max_length=255)
|
|
grades = models.CharField(max_length=255)
|
|
|
|
class CommunityShoppingAndDining(TimeInfoBase):
|
|
name = models.CharField(max_length=255)
|
|
distance = models.DecimalField(max_digits=3, decimal_places=1)
|
|
description = models.CharField(max_length=1024)
|
|
|
|
class CommunityParks(TimeInfoBase):
|
|
name = models.CharField(max_length=255)
|
|
distance = models.DecimalField(max_digits=3, decimal_places=1)
|
|
description = models.CharField(max_length=1024)
|
|
|
|
### Member only models
|
|
|
|
class CommunityMember(TimeInfoBase):
|
|
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
|
|
membership_person = models.OneToOneField(MembershipPerson, on_delete=models.CASCADE)
|
|
|
|
|
|
class CommunityPost(TimeInfoBase):
|
|
title = models.CharField(max_length=255)
|
|
category = models.CharField(max_length=255)
|
|
content = models.CharField(max_length=1024*8)
|
|
likes = models.IntegerField(default=0)
|
|
author = models.OneToOneField(CommunityMember, on_delete=models.CASCADE, blank=True, null=True)
|
|
|
|
class CommunityPostReports(TimeInfoBase):
|
|
# for anyone who reports a post
|
|
post = models.ForeignKey(CommunityPost, on_delete=models.CASCADE)
|
|
reporter = models.OneToOneField(CommunityMember, on_delete=models.CASCADE, blank=True, null=True)
|
|
|
|
class CommunityPostLikes(TimeInfoBase):
|
|
# for anyone who likes a post
|
|
post = models.ForeignKey(CommunityPost, on_delete=models.CASCADE)
|
|
reporter = models.OneToOneField(CommunityMember, on_delete=models.CASCADE)
|
|
|
|
class CommunityComment(TimeInfoBase):
|
|
post = models.ForeignKey(CommunityPost, on_delete=models.CASCADE)
|
|
content = models.CharField(max_length=1024*8)
|
|
likes = models.IntegerField(default=0)
|
|
author = models.OneToOneField(CommunityMember, on_delete=models.CASCADE, blank=True, null=True) |