WhisperMaPhone/thoughts/models.py

145 lines
4.7 KiB
Python

import os
import uuid
import magic
from django import forms
from django.db import models
from django.utils import timezone
from django.utils.text import normalize_newlines
from django.core.exceptions import ValidationError
from django.template.loader import render_to_string
class Thought(models.Model):
text = models.CharField(max_length=140, blank=True)
extended_text = models.TextField(blank=True)
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
posted = models.DateTimeField(default=timezone.now)
timezone_offset = models.IntegerField() # The number of minutes behind UTC we were when this was posted
media = models.FileField(upload_to="", blank=True) # A single image, video, or sound clip per post
media_alt = models.TextField(blank=True) # An optional transcription of the Thought's media
html_content = models.TextField(blank=True) # The rendered text for this thought
last_updated = models.DateTimeField(default=timezone.now)
def save(self):
self.update_html_content(commit=False)
self.last_updated = timezone.now()
super().save()
def get_media_type(self):
if not self.media:
return ""
else:
return os.path.splitext(self.media.path)[1][1:]
def get_timezone(self):
return timezone.get_fixed_timezone(-self.timezone_offset)
def get_offset_hours(self):
offset_hours = -self.timezone_offset / 60
# Convert 4.0 to 4
if offset_hours == int(offset_hours):
offset_hours = int(offset_hours)
if offset_hours > 0:
offset_hours = "+" + str(offset_hours)
return str(offset_hours)
def get_absolute_url(self):
return f"/?show={str(self.uuid)}"
def get_html_content(self, authenticated=False):
return render_to_string("thoughts/thought.html", {"thought": self, "authenticated": authenticated})
def get_html_content_authenticated(self):
return self.get_html_content(authenticated=True)
def update_html_content(self, commit=True):
self.html_content = self.get_html_content()
if commit:
self.save()
@classmethod
def find(cls, identifier):
try:
return cls.objects.get(uuid=identifier)
except cls.DoesNotExist:
try:
return cls.objects.get(id=identifier)
except cls.DoesNotExist:
return None
class Meta:
indexes = [
models.Index(fields=["-posted"]),
models.Index(fields=["-last_updated"])
]
# Honestly I'm so sick of this problem that writing out a comment here to explain why it is necessary is beyond me.
# I'm calling this CharField and not MySpecialLineNormalizingCharField
# because I think this should be the default behavior and I don't understand why it's not.
class CharField(forms.CharField):
def to_python(self, value):
# I am assuming that value is always a string and normalize_newlines isn't going to error on any string input
return super().to_python(normalize_newlines(value))
# A dict mapping allowed mime types to file extensions
ALLOWED_MEDIA_TYPES = {
"image/png": "png",
"image/jpeg": "jpeg",
"audio/x-m4a": "m4a",
"audio/mpegaudio/mpeg": "mp3",
"audio/mpeg": "mp3",
"audio/x-hx-aac-adts": "aac",
"video/mp4": "mp4",
"video/quicktime": "mov",
}
class ThoughtForm(forms.ModelForm):
text = CharField(
strip=False,
widget=forms.Textarea(attrs={"class": "thought", "rows": 1, "placeholder": "What are you thinking?"}),
required=False,
max_length=140
)
extended_text = CharField(
strip=False,
widget=forms.Textarea(attrs={"class": "thought", "rows": 1, "placeholder": "Anything else?"}),
required=False
)
media_alt = CharField(
widget=forms.Textarea(attrs={"placeholder": "Media alternate text, please", "rows": 1}),
required=False
)
def clean_media(self):
f = self.cleaned_data["media"]
if not f:
return f
# 16 MB file size limit.
if f.size > 2**24:
raise ValidationError("Content too long", code="max_size")
chunk = next(f.chunks(chunk_size=2048))
media_type = magic.from_buffer(chunk, mime="True")
if media_type not in ALLOWED_MEDIA_TYPES:
raise ValidationError("Invalid media type", code="invalid")
return self.cleaned_data["media"]
class Meta:
model = Thought
exclude = ["posted", "last_updated"]
widgets = {
"timezone_offset": forms.NumberInput,
}
error_messages = {
"text": {
"max_length": "Text must be at most 140 characters."
},
}