137 lines
4.5 KiB
Python
137 lines
4.5 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
|
|
|
|
def save(self):
|
|
self.update_html_content(commit=False)
|
|
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
|
|
|
|
# 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"]
|
|
widgets = {
|
|
"timezone_offset": forms.NumberInput,
|
|
}
|
|
|
|
error_messages = {
|
|
"text": {
|
|
"max_length": "Text must be at most 140 characters."
|
|
},
|
|
}
|