Skip to content
Closed
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
7 changes: 7 additions & 0 deletions web/context_processors.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,10 @@ def last_modified(request):
last_modified_time = "Unknown"

return {"last_modified": last_modified_time}


def invitation_notifications(request):
if request.user.is_authenticated:
pending_invites = request.user.received_group_invites.filter(status="pending").count()
return {"pending_invites_count": pending_invites}
return {}
8 changes: 8 additions & 0 deletions web/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
Review,
Session,
Storefront,
StudyGroup,
Subject,
SuccessStory,
TeamGoal,
Expand Down Expand Up @@ -1622,3 +1623,10 @@ class Meta:
"reminder_days_before": forms.NumberInput(attrs={"min": 1, "max": 14}),
"reminder_hours_before": forms.NumberInput(attrs={"min": 1, "max": 72}),
}


class StudyGroupForm(forms.ModelForm):
class Meta:
model = StudyGroup
# You might exclude fields that are set automatically.
fields = ["name", "description", "course", "max_members", "is_private"]
64 changes: 64 additions & 0 deletions web/migrations/0043_alter_studygroup_members_studygroupinvite.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Generated by Django 5.1.6 on 2025-03-24 06:45

import uuid

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


class Migration(migrations.Migration):

dependencies = [
("web", "0042_profile_avatar_accessories_and_more"),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]

operations = [
migrations.AlterField(
model_name="studygroup",
name="members",
field=models.ManyToManyField(blank=True, related_name="joined_groups", to=settings.AUTH_USER_MODEL),
),
migrations.CreateModel(
name="StudyGroupInvite",
fields=[
("id", models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
("created_at", models.DateTimeField(auto_now_add=True)),
("responded_at", models.DateTimeField(blank=True, null=True)),
(
"status",
models.CharField(
choices=[("pending", "Pending"), ("accepted", "Accepted"), ("declined", "Declined")],
default="pending",
max_length=20,
),
),
(
"group",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, related_name="invites", to="web.studygroup"
),
),
(
"recipient",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="received_group_invites",
to=settings.AUTH_USER_MODEL,
),
),
(
"sender",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="sent_group_invites",
to=settings.AUTH_USER_MODEL,
),
),
],
options={
"unique_together": {("group", "recipient")},
},
),
]
46 changes: 45 additions & 1 deletion web/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -801,7 +801,7 @@ class StudyGroup(models.Model):
description = models.TextField()
course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name="study_groups")
creator = models.ForeignKey(User, on_delete=models.CASCADE, related_name="created_groups")
members = models.ManyToManyField(User, related_name="joined_groups")
members = models.ManyToManyField(User, related_name="joined_groups", blank=True)
max_members = models.IntegerField(default=10)
is_private = models.BooleanField(default=False)
created_at = models.DateTimeField(auto_now_add=True)
Expand All @@ -810,6 +810,50 @@ class StudyGroup(models.Model):
def __str__(self):
return self.name

def can_add_memeber(self):
return self.members.count() < self.max_members

def add_member(self, user):
if self.can_add_memeber():
self.members.add(user)
return True
return False

def is_full(self):
return self.members.count() >= self.max_members


class StudyGroupInvite(models.Model):
"""Invitations to join study groups."""

id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
group = models.ForeignKey(StudyGroup, on_delete=models.CASCADE, related_name="invites")
sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name="sent_group_invites")
recipient = models.ForeignKey(User, on_delete=models.CASCADE, related_name="received_group_invites")
created_at = models.DateTimeField(auto_now_add=True)
responded_at = models.DateTimeField(null=True, blank=True)
STATUS_CHOICES = [("pending", "Pending"), ("accepted", "Accepted"), ("declined", "Declined")]
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")

class Meta:
unique_together = ["group", "recipient"]

def __str__(self):
return f"Invitation to {self.group.name} for {self.recipient.username}"

def accept(self):
"""Accept the invitation and add the recipient to the study group."""
self.status = "accepted"
self.responded_at = timezone.now()
self.save()
self.group.add_member(self.recipient)

def decline(self):
"""Decline the invitation."""
self.status = "declined"
self.responded_at = timezone.now()
self.save()


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
Expand Down
1 change: 1 addition & 0 deletions web/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"web.context_processors.last_modified",
"web.context_processors.invitation_notifications",
],
},
},
Expand Down
10 changes: 10 additions & 0 deletions web/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,16 @@
</span>
{% endif %}
</a>
<!-- New Notification Button for Invitations -->
<a href="{% url 'user_invitations' %}"
class="relative hover:underline flex items-center p-2 hover:bg-teal-700 rounded-lg">
<i class="fas fa-bell"></i>
{% if pending_invites_count %}
<span class="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-red-600 flex items-center justify-center text-white text-xs">
{{ pending_invites_count }}
</span>
{% endif %}
</a>
<div class="relative">
<button class="focus:outline-none hover:underline flex items-center p-2 hover:bg-teal-700 rounded-lg"
onclick="toggleLanguageDropdown()">
Expand Down
14 changes: 9 additions & 5 deletions web/templates/dashboard/student.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@
{% endblock title %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<h1 class="text-3xl font-bold mb-8">Student Dashboard</h1>
<div class="flex justify-between items-center mb-8">
<h1 class="text-3xl font-bold">Student Dashboard</h1>
<!-- Create Study Group Button -->
<a href="{% url 'create_study_group' %}"
class="bg-blue-500 hover:bg-blue-600 text-white font-semibold px-4 py-2 rounded-lg flex items-center">
<i class="fas fa-users mr-2"></i> Create Study Group
</a>
</div>
<!-- Overview Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Total Courses -->
Expand Down Expand Up @@ -121,7 +128,7 @@ <h4 class="font-medium">{{ session.title }}</h4>
</div>
</div>
</div>
<!-- Learning Streak Summary Section -->
<!-- Additional Sections (Learning Streak, Achievements, Certificates) -->
<div class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Your Learning Streak</h2>
{% if streak %}
Expand All @@ -139,7 +146,6 @@ <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Your Learni
View Detailed Streak Info
</a>
</div>
<!-- Achievements Section -->
<section class="mt-8 bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-xl font-semibold mb-4">Your Achievements</h2>
{% if achievements %}
Expand Down Expand Up @@ -167,7 +173,6 @@ <h3 class="mt-2 text-lg font-semibold">{{ achievement.title }}</h3>
<p class="text-gray-600 dark:text-gray-300">You haven't earned any achievements yet. Keep learning!</p>
{% endif %}
</section>
<!-- Your Certificates Section -->
<div class="mt-12">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
<h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Your Certificates</h2>
Expand All @@ -186,7 +191,6 @@ <h2 class="text-xl font-semibold text-gray-800 dark:text-white mb-4">Your Certif
{% else %}
<p class="text-gray-500 dark:text-gray-400"></p>
{% endif %}
<!-- Display generate buttons for enrollments with completed status but no certificate -->
{% for enrollment in enrollments %}
{% if enrollment.status|lower == "completed" %}
<div class="mb-4 border-b border-gray-200 dark:border-gray-700 pb-2">
Expand Down
37 changes: 37 additions & 0 deletions web/templates/web/study/create_group.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% extends "base.html" %}

{% load static %}

{% block title %}
Create Study Group
{% endblock title %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="max-w-md mx-auto bg-white dark:bg-gray-800 p-8 rounded-lg shadow border border-gray-300 dark:border-gray-700">
<div class="text-center mb-6">
<h2 class="text-3xl font-bold text-gray-900 dark:text-gray-100">Create a Study Group</h2>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-300">Fill in the details below to get started.</p>
</div>
<!-- Inline styles to add borders to form inputs -->
<style>
form input,
form textarea,
form select {
border: 1px solid #ccc;
padding: 0.5rem;
border-radius: 0.375rem;
}
</style>
<form method="post" class="space-y-6">
{% csrf_token %}
<div class="space-y-4">{{ form.as_p }}</div>
<div>
<button type="submit"
class="w-full py-3 px-4 bg-green-600 hover:bg-green-700 text-white rounded-md font-semibold focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2">
Create Group
</button>
</div>
</form>
</div>
</div>
{% endblock content %}
27 changes: 23 additions & 4 deletions web/templates/web/study/group_detail.html
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ <h2 class="text-lg font-semibold flex items-center">
{% for member in group.members.all %}
<div class="p-4">
<div class="flex items-center">
{% if member.user.profile.avatar %}
<img src="{{ member.user.profile.avatar.url }}"
alt="{{ member.user.get_full_name|default:member.user.username }}"
{% if member.profile.avatar %}
<img src="{{ member.profile.avatar.url }}"
alt="{{ member.get_full_name|default:member.username }}"
class="h-8 w-8 rounded-full mr-3"
width="32"
height="32" />
{% else %}
<img src="{% static 'images/default_teacher.png' %}"
alt="{{ member.user.get_full_name|default:member.user.username }}"
alt="{{ member.get_full_name|default:member.username }}"
class="h-8 w-8 rounded-full mr-3"
width="32"
height="32" />
Expand All @@ -158,6 +158,25 @@ <h2 class="text-lg font-semibold flex items-center">
</div>
</div>
</div>
{% if request.user in group.members.all %}
<div class="mt-6">
<div class="border border-gray-300 rounded p-4">
<h2 class="text-lg font-semibold mb-2">Invite Friends</h2>
<form method="post" action="{% url 'invite_to_study_group' group.id %}">
{% csrf_token %}
<div class="flex items-center">
<input type="text"
name="email_or_username"
placeholder="Enter email or username"
required
class="flex-1 border border-gray-400 rounded p-2 mr-2" />
<button type="submit"
class="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded">Send Invite</button>
</div>
</form>
</div>
</div>
{% endif %}
</div>
</main>
{% endblock content %}
46 changes: 46 additions & 0 deletions web/templates/web/study/invitations.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{% extends "base.html" %}

{% block title %}
Study Group Invitations
{% endblock title %}
{% block content %}
<div class="container mx-auto px-4 py-8">
<div class="max-w-4xl mx-auto">
<h1 class="text-3xl font-bold mb-4">Study Group Invitations</h1>
<p class="text-gray-600">Manage your study group invitations</p>
<div class="bg-white shadow rounded p-6 mt-4">
{% if invitations %}
{% for invite in invitations %}
<div class="border-b border-gray-200 py-4">
<div class="flex justify-between items-center">
<div>
<h2 class="text-xl font-semibold">{{ invite.group.name }}</h2>
<p class="text-gray-600">{{ invite.group.description }}</p>
<p class="text-sm text-gray-500">Invited by {{ invite.sender.username }} – {{ invite.created_at|timesince }} ago</p>
</div>
<div class="flex space-x-2">
<form method="post" action="{% url 'respond_to_invitation' invite.id %}">
{% csrf_token %}
<input type="hidden" name="response" value="accept" />
<button type="submit"
onclick="return confirm('Are you sure you want to accept this invitation?');"
class="bg-green-500 hover:bg-green-600 text-white py-1 px-3 rounded">Accept</button>
</form>
<form method="post" action="{% url 'respond_to_invitation' invite.id %}">
{% csrf_token %}
<input type="hidden" name="response" value="decline" />
<button type="submit"
onclick="return confirm('Are you sure you want to decline this invitation?');"
class="bg-red-500 hover:bg-red-600 text-white py-1 px-3 rounded">Decline</button>
</form>
</div>
</div>
</div>
{% endfor %}
{% else %}
<p class="text-gray-500">You don't have any pending invitations.</p>
{% endif %}
</div>
</div>
</div>
{% endblock content %}
Loading