alternative-backend-service/meetings/models.py

299 lines
9.7 KiB
Python
Raw Normal View History

import uuid
from django.db import models
from django.conf import settings
from django.utils import timezone
from cryptography.fernet import Fernet
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import base64
import os
class EncryptionManager:
def __init__(self):
self.fernet = self._get_fernet()
def _get_fernet_key(self):
key = getattr(settings, 'ENCRYPTION_KEY', None) or os.environ.get('ENCRYPTION_KEY')
if not key:
key = Fernet.generate_key().decode()
key = key.encode()
return key
def _get_fernet(self):
key = self._get_fernet_key()
return Fernet(key)
def encrypt_value(self, value):
if value is None or value == "":
return value
encrypted_value = self.fernet.encrypt(value.encode())
return base64.urlsafe_b64encode(encrypted_value).decode()
def decrypt_value(self, encrypted_value):
if encrypted_value is None or encrypted_value == "":
return encrypted_value
try:
encrypted_bytes = base64.urlsafe_b64decode(encrypted_value.encode())
decrypted_value = self.fernet.decrypt(encrypted_bytes)
return decrypted_value.decode()
except Exception as e:
print(f"Decryption error: {e}")
return encrypted_value
encryption_manager = EncryptionManager()
class EncryptedCharField(models.CharField):
def from_db_value(self, value, expression, connection):
if value is None:
return value
return encryption_manager.decrypt_value(value)
def get_prep_value(self, value):
if value is None:
return value
return encryption_manager.encrypt_value(value)
class EncryptedEmailField(EncryptedCharField):
def __init__(self, *args, **kwargs):
kwargs['max_length'] = 254
super().__init__(*args, **kwargs)
def from_db_value(self, value, expression, connection):
if value is None:
return value
return encryption_manager.decrypt_value(value)
def get_prep_value(self, value):
if value is None:
return value
return encryption_manager.encrypt_value(value)
class EncryptedTextField(models.TextField):
def from_db_value(self, value, expression, connection):
if value is None:
return value
return encryption_manager.decrypt_value(value)
def get_prep_value(self, value):
if value is None:
return value
return encryption_manager.encrypt_value(value)
class AdminWeeklyAvailability(models.Model):
DAYS_OF_WEEK = [
(0, 'Monday'),
(1, 'Tuesday'),
(2, 'Wednesday'),
(3, 'Thursday'),
(4, 'Friday'),
(5, 'Saturday'),
(6, 'Sunday'),
]
available_days = models.JSONField(
default=list,
help_text="List of weekdays (0-6) when appointments are accepted"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
verbose_name = 'Admin Weekly Availability'
verbose_name_plural = 'Admin Weekly Availability'
def __str__(self):
days = [self.DAYS_OF_WEEK[day][1] for day in self.available_days]
return f"Available: {', '.join(days)}"
class AppointmentRequest(models.Model):
STATUS_CHOICES = [
('pending_review', 'Pending Review'),
('scheduled', 'Scheduled'),
('rejected', 'Rejected'),
('completed', 'Completed'),
('cancelled', 'Cancelled'),
]
TIME_SLOT_CHOICES = [
('morning', 'Morning (9AM - 12PM)'),
('afternoon', 'Afternoon (1PM - 5PM)'),
('evening', 'Evening (6PM - 9PM)'),
]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
first_name = EncryptedCharField(max_length=100)
last_name = EncryptedCharField(max_length=100)
email = EncryptedEmailField()
phone = EncryptedCharField(max_length=20, blank=True)
reason = EncryptedTextField(blank=True)
preferred_dates = models.JSONField(
help_text="List of preferred dates (YYYY-MM-DD format)"
)
preferred_time_slots = models.JSONField(
help_text="List of preferred time slots (morning/afternoon/evening)"
)
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='pending_review'
)
scheduled_datetime = models.DateTimeField(null=True, blank=True)
scheduled_duration = models.PositiveIntegerField(
default=60,
help_text="Duration in minutes"
)
rejection_reason = EncryptedTextField(blank=True)
jitsi_meet_url = models.URLField(blank=True, help_text="Jitsi Meet URL for the video session")
jitsi_room_id = models.CharField(max_length=100, unique=True, blank=True, help_text="Jitsi room ID")
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'Appointment Request'
verbose_name_plural = 'Appointment Requests'
indexes = [
models.Index(fields=['status', 'scheduled_datetime']),
models.Index(fields=['email', 'created_at']),
]
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
@property
def formatted_created_at(self):
return self.created_at.strftime("%B %d, %Y at %I:%M %p")
@property
def formatted_scheduled_datetime(self):
if self.scheduled_datetime:
return self.scheduled_datetime.strftime("%B %d, %Y at %I:%M %p")
return None
@property
def has_jitsi_meeting(self):
return bool(self.jitsi_meet_url and self.jitsi_room_id)
@property
def meeting_in_future(self):
if not self.scheduled_datetime:
return False
return self.scheduled_datetime > timezone.now()
@property
def meeting_duration_display(self):
hours = self.scheduled_duration // 60
minutes = self.scheduled_duration % 60
if hours > 0:
return f"{hours}h {minutes}m"
return f"{minutes}m"
def get_preferred_dates_display(self):
try:
dates = [timezone.datetime.strptime(date, '%Y-%m-%d').strftime('%b %d, %Y')
for date in self.preferred_dates]
return ', '.join(dates)
except:
return ', '.join(self.preferred_dates)
def get_preferred_time_slots_display(self):
slot_display = {
'morning': 'Morning',
'afternoon': 'Afternoon',
'evening': 'Evening'
}
return ', '.join([slot_display.get(slot, slot) for slot in self.preferred_time_slots])
def generate_jitsi_room_id(self):
if not self.jitsi_room_id:
self.jitsi_room_id = f"therapy_session_{self.id.hex[:16]}"
return self.jitsi_room_id
def create_jitsi_meeting(self):
if not self.jitsi_room_id:
self.generate_jitsi_room_id()
jitsi_base_url = getattr(settings, 'JITSI_BASE_URL', 'https://meet.jit.si')
self.jitsi_meet_url = f"{jitsi_base_url}/{self.jitsi_room_id}"
return self.jitsi_meet_url
def get_jitsi_join_info(self):
if not self.has_jitsi_meeting:
return None
return {
'meeting_url': self.jitsi_meet_url,
'room_id': self.jitsi_room_id,
'scheduled_time': self.formatted_scheduled_datetime,
'duration': self.meeting_duration_display,
'join_instructions': 'Click the meeting URL to join the video session. No password required.'
}
def schedule_appointment(self, datetime_obj, duration=60, commit=True):
self.status = 'scheduled'
self.scheduled_datetime = datetime_obj
self.scheduled_duration = duration
self.rejection_reason = ''
self.create_jitsi_meeting()
if commit:
self.save()
def reject_appointment(self, reason='', commit=True):
self.status = 'rejected'
self.rejection_reason = reason
self.scheduled_datetime = None
self.jitsi_meet_url = ''
self.jitsi_room_id = ''
if commit:
self.save()
def cancel_appointment(self, reason='', commit=True):
self.status = 'cancelled'
self.rejection_reason = reason
if commit:
self.save()
def complete_appointment(self, commit=True):
self.status = 'completed'
if commit:
self.save()
def can_join_meeting(self):
if not self.scheduled_datetime or not self.has_jitsi_meeting:
return False
if self.status != 'scheduled':
return False
now = timezone.now()
meeting_start = self.scheduled_datetime
meeting_end = meeting_start + timezone.timedelta(minutes=self.scheduled_duration + 15) # 15 min buffer
return meeting_start - timezone.timedelta(minutes=10) <= now <= meeting_end
def get_meeting_status(self):
if not self.scheduled_datetime:
return "Not scheduled"
now = timezone.now()
meeting_start = self.scheduled_datetime
if now < meeting_start - timezone.timedelta(minutes=10):
return "Scheduled"
elif self.can_join_meeting():
return "Ready to join"
elif now > meeting_start + timezone.timedelta(minutes=self.scheduled_duration):
return "Completed"
else:
return "Ended"
def __str__(self):
return f"{self.full_name} - {self.get_status_display()} - {self.created_at.strftime('%Y-%m-%d')}"