Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Club announcements #1724

Merged
merged 42 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
7d0a636
feat(announcements): implement models/UI for club announcements
krishnans2006 Feb 11, 2024
ca4ddd4
feat(announcements): animate club announcements on dashboard
krishnans2006 Mar 12, 2024
94777f4
feat(announcements): filter club announcements by subscription status
krishnans2006 Mar 13, 2024
9353de5
refactor: update year on search, announcement request pages
alanzhu0 Mar 29, 2024
d50a7eb
refactor(announcements): reword guidelines and remove redundant code
alanzhu0 Mar 29, 2024
790c928
perf(announcements): add prefetch query in status view and tighten perms
alanzhu0 Mar 29, 2024
ff65c64
feat(announcements): continue adding functionality and permissions
alanzhu0 Mar 29, 2024
ae02f8d
style(announcements): dark theme fixes
krishnans2006 Apr 7, 2024
4246911
fix(announcements): handle some edge cases
krishnans2006 Apr 7, 2024
b6e91d7
style(announcements): improve club announcement form
krishnans2006 Apr 7, 2024
347047f
feat(eighth): add subscribe and unsubscribe buttons everywhere
krishnans2006 Apr 7, 2024
d548e8a
feat(eighth): add activity settings page for club announcements control
krishnans2006 Apr 8, 2024
6b3147d
fix: format and build sources
krishnans2006 Apr 8, 2024
b7c7712
fix(eighth): fix club announcements settings back button
alanzhu0 Apr 8, 2024
e2f9673
refactor(announcements): text changes
alanzhu0 May 1, 2024
d8a39b4
fix: format code, build sources, build docs
krishnans2006 Apr 8, 2024
07c6d70
fix: add club ann. text and remove hoco banner for ann. pages
shrysjain Sep 29, 2024
60872bb
fix: invalid button logic for dashboard
JasonGrace2282 Oct 2, 2024
112c042
fix: show banner for everyone
shrysjain Oct 2, 2024
66cf9ff
format: remove extraneous space
shrysjain Oct 2, 2024
e4420ba
feat: add club announcement archive
JasonGrace2282 Oct 2, 2024
51c00ac
chore: fix linting errors
JasonGrace2282 Oct 2, 2024
8d4abda
feat: implement "other club announcements" filter
JasonGrace2282 Oct 3, 2024
fa826b9
feat: search for club announcements
JasonGrace2282 Oct 4, 2024
6823c43
feat: force club sponsors to be subscribed
JasonGrace2282 Oct 5, 2024
5b0b822
fix: don't create normal announcement on club announcement edit
alanzhu0 Oct 5, 2024
226a2a0
feat: add show expired button
JasonGrace2282 Oct 5, 2024
c10a7b3
fix: show hidden announcements on club announcements view
alanzhu0 Oct 5, 2024
dfab287
refactor: announcement banner color change
alanzhu0 Oct 5, 2024
bc8fa41
fix: allow club sponsors to edit settings
shrysjain Oct 5, 2024
b5cce88
fix: unsubscribed button not showing
JasonGrace2282 Oct 5, 2024
7de2cb9
chore: fix pre-commit lints
JasonGrace2282 Oct 5, 2024
fbab690
fix: actually allow club announcements sponsors to modify
alanzhu0 Oct 5, 2024
51721c0
fix: allow club officers to expire announcements
alanzhu0 Oct 5, 2024
2f65a62
feat: add announcement edit history
alanzhu0 Oct 5, 2024
19d9c25
refactor: better errors and subscriber sticking
JasonGrace2282 Oct 5, 2024
df32926
feat: improve pagination
JasonGrace2282 Oct 5, 2024
f64049b
feat: send club announcements emails
alanzhu0 Oct 5, 2024
3efd6f6
fix: allow both sponsors and moderators to edit club settings
alanzhu0 Oct 5, 2024
81fb856
fix: get expiration date to show correctly on edit
alanzhu0 Oct 5, 2024
a2f35f3
chore: resolve some linting errors
JasonGrace2282 Oct 5, 2024
bd2828e
fix: prevent jitterclicking causing invalid results
JasonGrace2282 Oct 6, 2024
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
11 changes: 11 additions & 0 deletions docs/sourcedoc/intranet.apps.eighth.forms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ Subpackages

intranet.apps.eighth.forms.admin

Submodules
----------

intranet.apps.eighth.forms.activities module
--------------------------------------------

.. automodule:: intranet.apps.eighth.forms.activities
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

Expand Down
9 changes: 5 additions & 4 deletions intranet/apps/announcements/admin.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
from django.contrib import admin
from simple_history.admin import SimpleHistoryAdmin

from .models import Announcement, WarningAnnouncement


class AnnouncementAdmin(admin.ModelAdmin):
list_display = ("title", "user", "author", "added")
list_filter = ("added", "updated")
class AnnouncementAdmin(SimpleHistoryAdmin):
list_display = ("title", "user", "author", "activity", "added")
list_filter = ("added", "updated", "activity")
ordering = ("-added",)
raw_id_fields = ("user",)
search_fields = ("title", "content", "user__first_name", "user__last_name", "user__username")


class WarningAnnouncementAdmin(admin.ModelAdmin):
class WarningAnnouncementAdmin(SimpleHistoryAdmin):
list_display = ("title", "content", "active")
list_filter = ("active",)
search_fields = ("title", "content")
Expand Down
92 changes: 70 additions & 22 deletions intranet/apps/announcements/forms.py
Original file line number Diff line number Diff line change
@@ -1,45 +1,96 @@
import logging

from django import forms
from django.conf import settings
from django.contrib.auth import get_user_model

from ..eighth.models import EighthActivity
from ..users.forms import SortedTeacherMultipleChoiceField
from .models import Announcement, AnnouncementRequest

logger = logging.getLogger(__name__)


class AnnouncementForm(forms.ModelForm):
"""A form for generating an announcement."""

def __init__(self, *args, **kwargs):
expiration_date = forms.DateTimeInput()
notify_email_all = forms.BooleanField(required=False, label="Send Email to All")
update_added_date = forms.BooleanField(required=False, label="Update Added Date")

class Meta:
model = Announcement
fields = ["title", "author", "content", "groups", "expiration_date", "notify_post", "notify_email_all", "update_added_date", "pinned"]
help_texts = {
"expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
"notify_post": "If this box is checked, students who have signed up for notifications will receive an email.",
"notify_email_all": (
"This will send an email notification to all of the users who can see this post. "
"This option does NOT take users' email notification preferences into account, so please use with care."
),
"update_added_date": (
"If this announcement has already been added, update the added date to now so that the announcement is pushed to the top. "
"If this option is not selected, the announcement will stay in its current position."
),
}


class ClubAnnouncementForm(forms.ModelForm):
"""A form for posting a club announcement."""

def __init__(self, user, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."

self.fields["notify_post"].help_text = "If this box is checked, students who have signed up for notifications will receive an email."
if user.is_announcements_admin:
self.fields["activity"].queryset = EighthActivity.objects.filter(subscriptions_enabled=True)
elif user.is_club_officer:
self.fields["activity"].queryset = EighthActivity.objects.filter(subscriptions_enabled=True, officers=user)
elif user.is_club_sponsor:
self.fields["activity"].queryset = user.club_sponsor_for_set.filter(subscriptions_enabled=True)
else:
self.fields["activity"].queryset = []
self.fields["activity"].required = True
self.fields[
"notify_post"
].help_text = "If this box is checked, students who have subscribed to your club's announcements will receive an email."

if "instance" in kwargs: # Don't allow changing the activity once the announcement has been created
self.fields["activity"].widget.attrs["disabled"] = True
self.fields["activity"].initial = kwargs["instance"].activity

self.fields["notify_email_all"].help_text = (
"This will send an email notification to all of the users who can see this post. This option "
"does NOT take users' email notification preferences into account, so please use with care."
)
expiration_date = forms.DateTimeInput()
notify_post = forms.BooleanField(required=False, initial=True)

class Meta:
model = Announcement
fields = ["activity", "title", "content", "expiration_date", "notify_post"]
help_texts = {
"expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
}

self.fields["update_added_date"].help_text = (
"If this announcement has already been added, update the added date to now so that the "
"announcement is pushed to the top. If this option is not selected, the announcement will stay in "
"its current position."
)

class ClubAnnouncementEditForm(forms.ModelForm):
"""A form for editing a club announcement."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

expiration_date = forms.DateTimeInput()
notify_email_all = forms.BooleanField(required=False, label="Send Email to All")
update_added_date = forms.BooleanField(required=False, label="Update Added Date")

class Meta:
model = Announcement
fields = ["title", "author", "content", "groups", "expiration_date", "notify_post", "notify_email_all", "update_added_date", "pinned"]
fields = ["title", "content", "expiration_date"]
help_texts = {
"expiration_date": "By default, announcements expire after two weeks. Choose the shortest time necessary.",
}


class AnnouncementEditForm(forms.ModelForm):
"""A form for generating an announcement."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."
self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. Choose the shortest time necessary."

self.fields["notify_post_resend"].help_text = "If this box is checked, students who have signed up for notifications will receive an email."

Expand Down Expand Up @@ -69,21 +120,18 @@ class AnnouncementRequestForm(forms.ModelForm):

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["title"].help_text = (
"The title of the announcement that will appear on Intranet. Please enter "
"a title more specific than just \"[Club name]'s Intranet Posting'."
)
self.fields["title"].help_text = "The title of the announcement that will appear on Intranet."
self.fields["author"].help_text = (
"If you want this post to have a custom author entry, such as "
'"Basket Weaving Club" or "TJ Faculty," enter that name here. '
"Otherwise, your name will appear in this field automatically."
)
self.fields["content"].help_text = "The contents of the news post which will appear on Intranet."
self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. To change this, click in the box above."
self.fields["expiration_date"].help_text = "By default, announcements expire after two weeks. Choose the shortest time necessary."
self.fields["notes"].help_text = (
"Any information about this announcement you wish to share with the Intranet "
"administrators and teachers selected above. If you want to restrict this posting "
"to a specific group of students, such as the Class of 2016, enter that request here."
f"to a specific group of students, such as the Class of {settings.SENIOR_GRADUATION_YEAR}, enter that request here."
)
self.fields["teachers_requested"] = SortedTeacherMultipleChoiceField(
queryset=get_user_model().objects.get_approve_announcements_users_sorted(), show_username=True
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Generated by Django 3.2.20 on 2024-02-14 00:06

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


class Migration(migrations.Migration):

dependencies = [
('eighth', '0067_eighthactivity_subscribers'),
('announcements', '0032_alter_warningannouncement_type'),
]

operations = [
migrations.AddField(
model_name='announcement',
name='activity',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='eighth.eighthactivity'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Generated by Django 3.2.25 on 2024-10-05 05:34

import datetime
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from django.utils.timezone import utc
import simple_history.models


class Migration(migrations.Migration):

dependencies = [
('eighth', '0070_eighthactivity_club_sponsors'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('announcements', '0033_announcement_activity'),
]

operations = [
migrations.CreateModel(
name='HistoricalWarningAnnouncement',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('title', models.CharField(max_length=127)),
('content', models.TextField(help_text='Content of the warning. You can use HTML here.')),
('type', models.CharField(choices=[('dashboard', 'Dashboard Warning (displays on dashboard)'), ('login', 'Login Warning (displays on login page)'), ('dashboard_login', 'Dashboard and Login Warning (displays on dashboard and login pages)'), ('global', 'Global Warning (displays on all pages)')], default='dashboard', max_length=127)),
('active', models.BooleanField(default=True, help_text='Whether or not to show the warning.')),
('added', models.DateTimeField(blank=True, editable=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical warning announcement',
'verbose_name_plural': 'historical warning announcements',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
migrations.CreateModel(
name='HistoricalAnnouncement',
fields=[
('id', models.IntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('title', models.CharField(max_length=127)),
('content', models.TextField()),
('author', models.CharField(blank=True, max_length=63)),
('added', models.DateTimeField(blank=True, editable=False)),
('updated', models.DateTimeField(blank=True, editable=False)),
('expiration_date', models.DateTimeField(default=datetime.datetime(3000, 1, 1, 5, 0, tzinfo=utc))),
('notify_post', models.BooleanField(default=True)),
('notify_email_all', models.BooleanField(default=False)),
('pinned', models.BooleanField(default=False)),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),
('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('activity', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='eighth.eighthactivity')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'historical announcement',
'verbose_name_plural': 'historical announcements',
'ordering': ('-history_date', '-history_id'),
'get_latest_by': ('history_date', 'history_id'),
},
bases=(simple_history.models.HistoricalChanges, models.Model),
),
]
25 changes: 22 additions & 3 deletions intranet/apps/announcements/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
from django.db import models
from django.db.models import Manager, Q
from django.utils import timezone
from simple_history.models import HistoricalRecords

from ...utils.date import get_date_range_this_year, is_current_year
from ...utils.deletion import set_historical_user
from ...utils.html import nullify_links
from ..eighth.models import EighthActivity


class AnnouncementManager(Manager):
Expand Down Expand Up @@ -88,7 +90,7 @@ class Announcement(models.Model):
The title of the announcement
content
The HTML content of the news post
authors
author
The name of the author
added
The date the announcement was added
Expand All @@ -110,13 +112,17 @@ class Announcement(models.Model):
updated = models.DateTimeField(auto_now=True)
groups = models.ManyToManyField(DjangoGroup, blank=True)

activity = models.ForeignKey(EighthActivity, null=True, blank=True, on_delete=models.CASCADE)

expiration_date = models.DateTimeField(auto_now=False, default=timezone.make_aware(datetime(3000, 1, 1)))

notify_post = models.BooleanField(default=True)
notify_email_all = models.BooleanField(default=False)

pinned = models.BooleanField(default=False)

history = HistoricalRecords()

def get_author(self) -> str:
"""Returns 'author' if it is set. Otherwise, returns the name of the user who created the announcement.

Expand All @@ -141,9 +147,20 @@ def is_this_year(self):
"""Return whether the announcement was created after July 1st of this school year."""
return is_current_year(self.added)

@property
def is_club_announcement(self):
return self.activity is not None

def is_visible(self, user):
return self in Announcement.objects.visible_to_user(user)

def can_modify(self, user):
return (
user.is_announcements_admin
or self.is_club_announcement
and (user in self.activity.officers.all() or user in self.activity.sponsors.all() or user in self.activity.club_sponsors.all())
)

# False, not None. This can be None if no AnnouncementRequest exists for this Announcement,
# and we should not reevaluate in that case.
_announcementrequest = False # type: AnnouncementRequest
Expand All @@ -157,13 +174,13 @@ def announcementrequest(self):

def is_visible_requester(self, user):
try:
return self.announcementrequest_set.filter(teachers_requested__id=user.id).exists()
return self.announcementrequest_set.filter(teachers_requested=user).exists()
except get_user_model().DoesNotExist:
return False

def is_visible_submitter(self, user):
try:
return (self.announcementrequest and user.id == self.announcementrequest.user_id) or self.user_id == user.id
return self.user == user or self.announcementrequest and user == self.announcementrequest.user
except get_user_model().DoesNotExist:
return False

Expand Down Expand Up @@ -280,6 +297,8 @@ class WarningAnnouncement(models.Model):
active = models.BooleanField(default=True, help_text="Whether or not to show the warning.")
added = models.DateTimeField(auto_now_add=True)

history = HistoricalRecords()

@property
def show_on_dashboard(self):
return self.type in ("dashboard", "dashboard_login") # global is not included. It will show on all pages and this logic isn't needed.
Expand Down
18 changes: 17 additions & 1 deletion intranet/apps/announcements/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ def announcement_posted_email(request, obj, send_all=False):
.objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year())
.union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"]))
)
elif obj.activity:
subject = f"Club Announcement for {obj.activity.name}: {obj.title}"
users = (
get_user_model()
.objects.filter(
user_type="student",
graduation_year__gte=get_senior_graduation_year(),
receive_news_emails=True,
subscribed_activity_set=obj.activity,
)
.union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_activity_set=obj.activity))
)

else:
users = (
get_user_model()
Expand All @@ -144,7 +157,10 @@ def announcement_posted_email(request, obj, send_all=False):
email_send_task.delay(
"announcements/emails/announcement_posted.txt", "announcements/emails/announcement_posted.html", data, subject, emails, bcc=True
)
messages.success(request, f"Sent email to {len(users_send)} users")
if request.user.is_announcements_admin:
messages.success(request, f"Sent email to {len(users_send)} users")
else:
messages.success(request, "Sent notification emails.")
else:
logger.info("Emailing announcements disabled")

Expand Down
Loading
Loading