Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions web/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
CourseMaterial,
ForumCategory,
Goods,
Meme,
ProductImage,
Profile,
ProgressTracker,
Expand Down Expand Up @@ -60,6 +61,7 @@
"StorefrontForm",
"ProgressTrackerForm",
"SuccessStoryForm",
"MemeForm",
]


Expand Down Expand Up @@ -1033,6 +1035,88 @@ class Meta:
}


class MemeForm(forms.ModelForm):
new_subject = forms.CharField(
max_length=100,
required=False,
widget=TailwindInput(
attrs={
"placeholder": "Enter a new subject name",
"class": "w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500",
}
),
help_text="If your subject isn't listed, enter a new one here",
)

class Meta:
model = Meme
fields = ["title", "subject", "new_subject", "caption", "image"]
widgets = {
"title": TailwindInput(
attrs={
"placeholder": "Enter a descriptive title",
"required": True,
}
),
"subject": TailwindSelect(
attrs={
"class": "w-full px-3 py-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500"
}
),
"caption": TailwindTextarea(
attrs={
"placeholder": "Add a caption for your meme",
"rows": 3,
}
),
"image": TailwindFileInput(
attrs={
"accept": "image/png,image/jpeg,image/gif",
"required": True,
"help_text": "Upload a meme image (JPG, PNG, or GIF, max 2MB)",
}
),
}

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["subject"].required = False
self.fields["subject"].help_text = "Select an existing subject"

# Improve error messages
self.fields["image"].error_messages = {
"required": "Please select an image file.",
"invalid": "Please upload a valid image file.",
}

def clean(self):
cleaned_data = super().clean()
subject = cleaned_data.get("subject")
new_subject = cleaned_data.get("new_subject")

if not subject and not new_subject:
raise forms.ValidationError("You must either select an existing subject or create a new one.")

return cleaned_data

def save(self, commit=True):
meme = super().save(commit=False)

# Create new subject if provided
new_subject_name = self.cleaned_data.get("new_subject")
if new_subject_name and not self.cleaned_data.get("subject"):
from django.utils.text import slugify

subject, created = Subject.objects.get_or_create(
name=new_subject_name, defaults={"slug": slugify(new_subject_name)}
)
meme.subject = subject

if commit:
meme.save()
return meme


class StudentEnrollmentForm(forms.Form):
first_name = forms.CharField(
max_length=30, required=True, widget=TailwindInput(attrs={"placeholder": "First Name"}), label="First Name"
Expand Down
64 changes: 64 additions & 0 deletions web/migrations/0026_enhance_meme_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 5.1.6 on 2025-03-18 17:29

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models

import web.models


class Migration(migrations.Migration):

dependencies = [
("web", "0025_progresstracker"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.CreateModel(
name="Meme",
fields=[
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
("title", models.CharField(help_text="A descriptive title for the meme", max_length=200)),
("caption", models.TextField(blank=True, help_text="The text content of the meme")),
(
"image",
models.ImageField(
help_text="Upload a meme image (JPG, PNG, or GIF, max 2MB)",
upload_to="memes/",
validators=[web.models.validate_image_size, web.models.validate_image_extension],
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
(
"subject",
models.ForeignKey(
help_text="The educational subject this meme relates to",
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="memes",
to="web.subject",
),
),
(
"uploader",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="memes",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"verbose_name": "Meme",
"verbose_name_plural": "Memes",
"ordering": ["-created_at"],
"indexes": [
models.Index(fields=["-created_at"], name="web_meme_created_b43882_idx"),
models.Index(fields=["subject"], name="web_meme_subject_1b89ce_idx"),
],
},
),
]
49 changes: 49 additions & 0 deletions web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify
from django.utils.translation import gettext_lazy as _
from markdownx.models import MarkdownxField
from PIL import Image

Expand Down Expand Up @@ -1192,6 +1193,54 @@ def __str__(self):
return f"{self.quantity}x {self.goods.name}"


def validate_image_size(image):
"""Validate that the image file is not too large."""
file_size = image.size
limit_mb = 2
if file_size > limit_mb * 1024 * 1024:
raise ValidationError(f"Image file is too large. Size should not exceed {limit_mb} MB.")


def validate_image_extension(image):
"""Validate that the file is a valid image type."""
import os

ext = os.path.splitext(image.name)[1]
valid_extensions = [".jpg", ".jpeg", ".png", ".gif"]
if ext.lower() not in valid_extensions:
raise ValidationError("Unsupported file type. Please use JPEG, PNG, or GIF images.")


class Meme(models.Model):
title = models.CharField(max_length=200, blank=False, help_text=_("A descriptive title for the meme"))
subject = models.ForeignKey(
Subject,
on_delete=models.SET_NULL,
related_name="memes",
null=True,
blank=False,
help_text=_("The educational subject this meme relates to"),
)
caption = models.TextField(help_text=_("The text content of the meme"), blank=True)
image = models.ImageField(
upload_to="memes/",
validators=[validate_image_size, validate_image_extension],
help_text=_("Upload a meme image (JPG, PNG, or GIF, max 2MB)"),
)
uploader = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, related_name="memes", null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

def __str__(self):
return self.title

class Meta:
ordering = ["-created_at"]
indexes = [models.Index(fields=["-created_at"]), models.Index(fields=["subject"])]
verbose_name = _("Meme")
verbose_name_plural = _("Memes")


class Donation(models.Model):
"""Model for storing donation information."""

Expand Down
97 changes: 97 additions & 0 deletions web/templates/add_meme.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
{% extends "base.html" %}

{% block title %}
Add Educational Meme
{% endblock title %}
{% block content %}
<div class="container mx-auto mt-8 px-4 max-w-2xl">
<h1 class="text-2xl font-bold mb-6">Add a New Educational Meme</h1>
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6">
<form method="post" enctype="multipart/form-data" class="space-y-4">
{% csrf_token %}
<div>
<label for="{{ form.title.id_for_label }}" class="block mb-2 font-medium">Title</label>
{{ form.title }}
{% if form.title.errors %}<p class="text-red-500 text-sm mt-1">{{ form.title.errors.0 }}</p>{% endif %}
</div>
<div>
<label for="{{ form.subject.id_for_label }}" class="block mb-2 font-medium">Subject</label>
{{ form.subject }}
{% if form.subject.errors %}<p class="text-red-500 text-sm mt-1">{{ form.subject.errors.0 }}</p>{% endif %}
<p class="text-sm text-gray-500 mt-1">{{ form.subject.help_text }}</p>
</div>
<div>
<label for="{{ form.new_subject.id_for_label }}"
class="block mb-2 font-medium">Or create new subject</label>
{{ form.new_subject }}
<p class="text-sm text-gray-500 mt-1">If your subject isn't listed above, enter a new one here</p>
</div>
<div>
<label for="{{ form.caption.id_for_label }}" class="block mb-2 font-medium">Caption</label>
{{ form.caption }}
{% if form.caption.errors %}<p class="text-red-500 text-sm mt-1">{{ form.caption.errors.0 }}</p>{% endif %}
</div>
<div>
<label for="{{ form.image.id_for_label }}" class="block mb-2 font-medium">Meme Image</label>
{{ form.image }}
{% if form.image.errors %}<p class="text-red-500 text-sm mt-1">{{ form.image.errors.0 }}</p>{% endif %}
<p class="text-sm text-gray-500 mt-1">Recommended: PNG or JPEG, max 2MB</p>
<div id="imagePreview" class="hidden mt-4 border rounded p-4">
<p class="text-sm font-medium mb-2">Image Preview:</p>
<div class="flex justify-center">
<img id="preview"
src=""
alt="Preview"
class="max-h-64 max-w-full object-contain" />
</div>
<p class="text-xs text-gray-500 mt-2 text-center">
Preview is for reference only and may appear differently on the site.
</p>
</div>
</div>
{% if form.non_field_errors %}
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded">
{% for error in form.non_field_errors %}<p>{{ error }}</p>{% endfor %}
</div>
{% endif %}
<div class="pt-4">
<button type="submit"
class="bg-teal-600 hover:bg-teal-700 text-white py-2 px-6 rounded-lg">Upload Meme</button>
<a href="{% url 'meme_list' %}"
class="ml-4 text-gray-600 dark:text-gray-400 hover:underline">Cancel</a>
</div>
</form>
</div>
</div>
<script>
document.getElementById('{{ form.image.id_for_label }}').addEventListener('change', function(e) {
const preview = document.getElementById('preview');
const previewContainer = document.getElementById('imagePreview');

if (this.files && this.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
preview.src = e.target.result;
previewContainer.classList.remove('hidden');
}
reader.readAsDataURL(this.files[0]);
} else {
previewContainer.classList.add('hidden');
}
});
const subjectField = document.getElementById('{{ form.subject.id_for_label }}');
const newSubjectField = document.getElementById('{{ form.new_subject.id_for_label }}');

subjectField.addEventListener('change', function() {
if (this.value) {
newSubjectField.value = '';
}
});

newSubjectField.addEventListener('input', function() {
if (this.value.trim()) {
subjectField.selectedIndex = 0;
}
});
</script>
{% endblock content %}
9 changes: 9 additions & 0 deletions web/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@
class="hover:underline flex items-center">
<i class="fa-solid fa-store mr-1"></i> Products
</a>
<a href="{% url 'meme_list' %}"
class="hover:underline flex items-center">
<i class="fa-solid fa-face-smile mr-1"></i> Edu Memes
</a>
</div>
<!-- Language and Dark Mode -->
<div class="flex items-center space-x-2">
Expand Down Expand Up @@ -402,6 +406,11 @@
<i class="fa-solid fa-blog"></i>
<span>Blog</span>
</a>
<a href="{% url 'meme_list' %}"
class="flex items-center space-x-2 px-4 py-3 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
<i class="fa-solid fa-face-smile"></i>
<span>Edu Memes</span>
</a>
{% if user.is_authenticated %}
<a href="{% url 'profile' %}"
class="flex items-center space-x-2 px-4 py-3 text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg">
Expand Down
Loading