Skip to content

Commit 157923b

Browse files
committed
feat(announcements): continue adding functionality and permissions
1 parent ffdd800 commit 157923b

30 files changed

+634
-369
lines changed

Ion.egg-info/SOURCES.txt

+8
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ intranet/apps/announcements/migrations/0029_alter_warningannouncement_type.py
199199
intranet/apps/announcements/migrations/0030_alter_warningannouncement_type.py
200200
intranet/apps/announcements/migrations/0031_alter_warningannouncement_type.py
201201
intranet/apps/announcements/migrations/0032_alter_warningannouncement_type.py
202+
intranet/apps/announcements/migrations/0033_announcement_activity.py
202203
intranet/apps/announcements/migrations/__init__.py
203204
intranet/apps/api/__init__.py
204205
intranet/apps/api/authentication.py
@@ -385,6 +386,11 @@ intranet/apps/eighth/migrations/0062_auto_20200116_1926.py
385386
intranet/apps/eighth/migrations/0063_auto_20201224_1745.py
386387
intranet/apps/eighth/migrations/0064_auto_20210205_1153.py
387388
intranet/apps/eighth/migrations/0065_auto_20220903_0038.py
389+
intranet/apps/eighth/migrations/0066_eighthactivity_officers.py
390+
intranet/apps/eighth/migrations/0067_eighthactivity_subscribers.py
391+
intranet/apps/eighth/migrations/0068_auto_20240213_1938.py
392+
intranet/apps/eighth/migrations/0069_alter_eighthsponsor_user.py
393+
intranet/apps/eighth/migrations/0070_eighthactivity_club_sponsors.py
388394
intranet/apps/eighth/migrations/__init__.py
389395
intranet/apps/eighth/tests/__init__.py
390396
intranet/apps/eighth/tests/eighth_test.py
@@ -886,6 +892,7 @@ intranet/static/css/_reset.scss
886892
intranet/static/css/about.scss
887893
intranet/static/css/admin.scss
888894
intranet/static/css/announcements.form.scss
895+
intranet/static/css/announcements.request.scss
889896
intranet/static/css/api.scss
890897
intranet/static/css/base.scss
891898
intranet/static/css/board.scss
@@ -3267,6 +3274,7 @@ intranet/templates/page_with_nav.html
32673274
intranet/templates/announcements/add_modify.html
32683275
intranet/templates/announcements/announcement.html
32693276
intranet/templates/announcements/approve.html
3277+
intranet/templates/announcements/club-request.html
32703278
intranet/templates/announcements/delete.html
32713279
intranet/templates/announcements/request.html
32723280
intranet/templates/announcements/request_status.html

intranet/apps/announcements/admin.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55

66
class AnnouncementAdmin(admin.ModelAdmin):
7-
list_display = ("title", "user", "author", "added")
8-
list_filter = ("added", "updated")
7+
list_display = ("title", "user", "author", "activity", "added")
8+
list_filter = ("added", "updated", "activity")
99
ordering = ("-added",)
1010
raw_id_fields = ("user",)
1111

intranet/apps/announcements/forms.py

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import logging
2+
13
from django import forms
24
from django.conf import settings
35
from django.contrib.auth import get_user_model
46

7+
from ..eighth.models import EighthActivity
58
from ..users.forms import SortedTeacherMultipleChoiceField
69
from .models import Announcement, AnnouncementRequest
710

11+
logger = logging.getLogger(__name__)
12+
813

914
class AnnouncementForm(forms.ModelForm):
1015
"""A form for generating an announcement."""
@@ -29,17 +34,28 @@ class ClubAnnouncementForm(forms.ModelForm):
2934

3035
def __init__(self, user, *args, **kwargs):
3136
super().__init__(*args, **kwargs)
32-
self.fields["activity"].queryset = user.officer_for_set
37+
38+
if user.is_announcements_admin:
39+
self.fields["activity"].queryset = EighthActivity.objects.filter(subscriptions_enabled=True)
40+
elif user.is_club_officer:
41+
self.fields["activity"].queryset = EighthActivity.objects.filter(subscriptions_enabled=True, officers=user)
42+
elif user.is_club_sponsor:
43+
self.fields["activity"].queryset = user.club_sponsor_for_set.filter(subscriptions_enabled=True)
44+
else:
45+
self.fields["activity"].queryset = []
46+
self.fields["activity"].required = True
47+
48+
if "instance" in kwargs: # Don't allow changing the activity once the announcement has been created
49+
self.fields["activity"].widget.attrs["disabled"] = True
50+
self.fields["activity"].required = False
3351

3452
expiration_date = forms.DateTimeInput()
35-
update_added_date = forms.BooleanField(required=False, label="Update Added Date")
3653

3754
class Meta:
3855
model = Announcement
39-
fields = ["title", "author", "content", "activity", "expiration_date", "update_added_date"]
56+
fields = ["activity", "title", "content", "expiration_date"]
4057
help_texts = {
4158
"expiration_date": "By default, announcements expire after two weeks. To change this, click in the box above.",
42-
"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.",
4359
}
4460

4561

intranet/apps/announcements/models.py

+10-4
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from ...utils.date import get_date_range_this_year, is_current_year
1111
from ...utils.deletion import set_historical_user
1212
from ...utils.html import nullify_links
13-
1413
from ..eighth.models import EighthActivity
1514

1615

@@ -90,7 +89,7 @@ class Announcement(models.Model):
9089
The title of the announcement
9190
content
9291
The HTML content of the news post
93-
authors
92+
author
9493
The name of the author
9594
added
9695
The date the announcement was added
@@ -152,6 +151,13 @@ def is_club_announcement(self):
152151
def is_visible(self, user):
153152
return self in Announcement.objects.visible_to_user(user)
154153

154+
def can_modify(self, user):
155+
return (
156+
user.is_announcements_admin
157+
or self.is_club_announcement
158+
and (self.is_visible_submitter(user) or user.club_sponsor_for_set.filter(id=self.activity.id).exists())
159+
)
160+
155161
# False, not None. This can be None if no AnnouncementRequest exists for this Announcement,
156162
# and we should not reevaluate in that case.
157163
_announcementrequest = False # type: AnnouncementRequest
@@ -165,13 +171,13 @@ def announcementrequest(self):
165171

166172
def is_visible_requester(self, user):
167173
try:
168-
return self.announcementrequest_set.filter(teachers_requested__id=user.id).exists()
174+
return self.announcementrequest_set.filter(teachers_requested=user).exists()
169175
except get_user_model().DoesNotExist:
170176
return False
171177

172178
def is_visible_submitter(self, user):
173179
try:
174-
return (self.announcementrequest and user.id == self.announcementrequest.user_id) or self.user_id == user.id
180+
return self.user == user or self.announcementrequest and user == self.announcementrequest.user
175181
except get_user_model().DoesNotExist:
176182
return False
177183

intranet/apps/announcements/notifications.py

+4-7
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
from django.contrib import messages
1111
from django.contrib.auth import get_user_model
1212
from django.core import exceptions
13-
from django.db.models import Q
1413
from django.urls import reverse
1514

1615
from ...utils.date import get_senior_graduation_year
@@ -120,14 +119,12 @@ def announcement_posted_email(request, obj, send_all=False):
120119
.objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year())
121120
.union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"]))
122121
)
123-
elif obj.club:
124-
filter = Q(subscribed_to_set__contains=obj.club) & (
125-
Q(user_type="student") & Q(graduation_year__gte=get_senior_graduation_year()) | Q(user_type__in=["teacher", "counselor"])
126-
)
122+
elif obj.activity:
123+
subject = f"Club Announcement for {obj.activity.name}: {obj.title}"
127124
users = (
128125
get_user_model()
129-
.objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year(), subscribed_to_set__contains=obj.club)
130-
.union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_to_set__contains=obj.club))
126+
.objects.filter(user_type="student", graduation_year__gte=get_senior_graduation_year(), subscribed_to_set__contains=obj.activity)
127+
.union(get_user_model().objects.filter(user_type__in=["teacher", "counselor"], subscribed_to_set__contains=obj.activity))
131128
)
132129

133130
else:

intranet/apps/announcements/urls.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
re_path(r"^/club$", views.view_club_announcements, name="club_announcements"),
99
re_path(r"^/add$", views.add_announcement_view, name="add_announcement"),
1010
re_path(r"^/request$", views.request_announcement_view, name="request_announcement"),
11-
re_path(r"^/club/post$", views.post_club_announcement_view, name="post_club_announcement"),
11+
re_path(r"^/club/add$", views.add_club_announcement_view, name="add_club_announcement"),
12+
re_path(r"^/club/modify/(?P<announcement_id>\d+)$", views.modify_club_announcement_view, name="modify_club_announcement"),
1213
re_path(r"^/request/success$", views.request_announcement_success_view, name="request_announcement_success"),
1314
re_path(r"^/request/success_self$", views.request_announcement_success_self_view, name="request_announcement_success_self"),
1415
re_path(r"^/approve/(?P<req_id>\d+)$", views.approve_announcement_view, name="approve_announcement"),

intranet/apps/announcements/views.py

+47-12
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,8 @@
1515
from ..groups.models import Group
1616
from .forms import AnnouncementAdminForm, AnnouncementEditForm, AnnouncementForm, AnnouncementRequestForm, ClubAnnouncementForm
1717
from .models import Announcement, AnnouncementRequest
18-
from .notifications import (
19-
admin_request_announcement_email,
20-
announcement_approved_email,
21-
announcement_posted_email,
22-
announcement_posted_twitter,
23-
request_announcement_email,
24-
)
18+
from .notifications import (admin_request_announcement_email, announcement_approved_email, announcement_posted_email, announcement_posted_twitter,
19+
request_announcement_email)
2520

2621
logger = logging.getLogger(__name__)
2722

@@ -131,7 +126,13 @@ def request_announcement_view(request):
131126
return render(request, "announcements/request.html", {"form": form, "action": "add"})
132127

133128

134-
def post_club_announcement_view(request):
129+
@login_required
130+
@deny_restricted
131+
def add_club_announcement_view(request):
132+
if not (request.user.is_announcements_admin or request.user.is_club_officer or request.user.is_club_sponsor):
133+
messages.error(request, "You do not have permission to post club announcements.")
134+
return redirect("club_announcements")
135+
135136
if request.method == "POST":
136137
form = ClubAnnouncementForm(request.user, request.POST)
137138

@@ -143,13 +144,47 @@ def post_club_announcement_view(request):
143144

144145
obj.save()
145146

146-
messages.success(request, "Successfully posted announcement.")
147+
messages.success(request, "Successfully posted club announcement.")
147148
return redirect("club_announcements")
148149
else:
149-
messages.error(request, "Error adding announcement")
150+
messages.error(request, "Error adding club announcement")
150151
else:
151152
form = ClubAnnouncementForm(request.user)
152-
return render(request, "announcements/club-request.html", {"form": form, "action": "add"})
153+
return render(request, "announcements/club-request.html", {"form": form, "action": "post"})
154+
155+
156+
@login_required
157+
@deny_restricted
158+
def modify_club_announcement_view(request, announcement_id):
159+
announcement = get_object_or_404(Announcement, id=announcement_id)
160+
161+
if not announcement.is_club_announcement:
162+
messages.error(request, "This announcement is not a club announcement.")
163+
return redirect("club_announcements")
164+
165+
if not announcement.can_modify(request.user):
166+
messages.error(request, "You do not have permission to modify this club announcement.")
167+
return redirect("club_announcements")
168+
169+
if request.method == "POST":
170+
form = ClubAnnouncementForm(request.user, request.POST, instance=announcement)
171+
172+
if form.is_valid():
173+
obj = form.save(commit=True)
174+
obj.user = request.user
175+
obj.activity = announcement.activity
176+
# SAFE HTML
177+
obj.content = safe_html(obj.content)
178+
179+
obj.save()
180+
181+
messages.success(request, "Successfully modified club announcement.")
182+
return redirect("club_announcements")
183+
else:
184+
messages.error(request, "Error modifying club announcement")
185+
else:
186+
form = ClubAnnouncementForm(request.user, instance=announcement)
187+
return render(request, "announcements/club-request.html", {"form": form, "action": "modify"})
153188

154189

155190
@login_required
@@ -355,7 +390,7 @@ def modify_announcement_view(request, announcement_id=None):
355390
logger.info("Admin %s modified announcement: %s (%s)", request.user, announcement, announcement.id)
356391
return redirect("index")
357392
else:
358-
messages.error(request, "Error adding announcement")
393+
messages.error(request, "Error modifying announcement")
359394
else:
360395
announcement = get_object_or_404(Announcement, id=announcement_id)
361396
form = AnnouncementEditForm(instance=announcement)

intranet/apps/dashboard/views.py

+13-9
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,11 @@ def get_announcements_list(request, context):
239239

240240
# Load information on the user who posted the announcement
241241
# Unless the announcement has a custom author (some do, but not all), we will need the user information to construct the byline,
242-
announcements = announcements.select_related("user")
242+
announcements = announcements.select_related("user", "activity")
243243

244244
# We may query the announcement request multiple times while checking if the user submitted or approved the announcement.
245245
# prefetch_related() will still make a separate query for each request, but the results are cached if we check them multiple times
246-
announcements = announcements.prefetch_related("announcementrequest_set")
246+
announcements = announcements.prefetch_related("announcementrequest_set", "groups")
247247

248248
if context["events_admin"] and context["show_all"]:
249249
events = Event.objects.all()
@@ -255,6 +255,8 @@ def get_announcements_list(request, context):
255255
midnight = timezone.localtime().replace(hour=0, minute=0, second=0, microsecond=0)
256256
events = Event.objects.visible_to_user(user).filter(time__gte=midnight, show_on_dashboard=True)
257257

258+
events = events.select_related("user").prefetch_related("groups")
259+
258260
items = sorted(chain(announcements, events), key=lambda item: (item.pinned, item.added))
259261
items.reverse()
260262

@@ -266,7 +268,8 @@ def split_club_announcements(items):
266268

267269
for item in items:
268270
if item.dashboard_type == "announcement" and item.is_club_announcement:
269-
club.append(item)
271+
if item.activity.subscriptions_enabled:
272+
club.append(item)
270273
else:
271274
standard.append(item)
272275

@@ -277,12 +280,13 @@ def filter_club_announcements(user, user_hidden_announcements, club_items):
277280
visible, hidden, unsubscribed = [], [], []
278281

279282
for item in club_items:
280-
if user not in item.activity.subscribers.all():
281-
unsubscribed.append(item)
282-
elif item.id in user_hidden_announcements:
283-
hidden.append(item)
284-
else:
285-
visible.append(item)
283+
if item.activity.subscriptions_enabled:
284+
if user not in item.activity.subscribers.all():
285+
unsubscribed.append(item)
286+
elif item.id in user_hidden_announcements:
287+
hidden.append(item)
288+
else:
289+
visible.append(item)
286290

287291
return visible, hidden, unsubscribed
288292

intranet/apps/eighth/forms/admin/activities.py

+22
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,18 @@ def __init__(self, *args, **kwargs):
135135
student_objects = get_user_model().objects.get_students()
136136
self.fields["users_allowed"].queryset = student_objects
137137
self.fields["users_blacklisted"].queryset = student_objects
138+
self.fields["officers"].queryset = student_objects
139+
self.fields["club_sponsors"].queryset = get_user_model().objects.filter(user_type__in=["teacher", "counselor"])
138140

139141
self.fields["presign"].label = "2 day pre-signup"
140142
self.fields["default_capacity"].help_text = "Overrides the sum of each room's capacity above, if set."
143+
self.fields["subscriptions_enabled"].label = "Enable club announcements"
144+
self.fields["subscriptions_enabled"].help_text = "Allow students to subscribe to receive announcements for this activity through Ion."
145+
self.fields["officers"].help_text = "Student officers can send club announcements to subscribers."
146+
self.fields["club_sponsors"].help_text = (
147+
"Club sponsors can manage this club's announcements. May be different from the activity's scheduled sponsors."
148+
)
149+
self.fields["subscribers"].help_text = "Students who subscribe to this activity will receive club announcements."
141150

142151
# These fields are rendered on the right of the page on the edit activity page.
143152
self.right_fields = set(
@@ -153,6 +162,15 @@ def __init__(self, *args, **kwargs):
153162
]
154163
)
155164

165+
self.club_announcements_fields = set(
166+
[
167+
"subscriptions_enabled",
168+
"club_sponsors",
169+
"officers",
170+
"subscribers",
171+
]
172+
)
173+
156174
class Meta:
157175
model = EighthActivity
158176
fields = [
@@ -182,6 +200,10 @@ class Meta:
182200
"fri_a",
183201
"fri_b",
184202
"admin_comments",
203+
"subscriptions_enabled",
204+
"club_sponsors",
205+
"officers",
206+
"subscribers",
185207
]
186208
widgets = {
187209
"description": forms.Textarea(attrs={"rows": 9, "cols": 46}),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 3.2.25 on 2024-03-30 04:11
2+
3+
from django.conf import settings
4+
from django.db import migrations, models
5+
import intranet.utils.deletion
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12+
('eighth', '0068_auto_20240213_1938'),
13+
]
14+
15+
operations = [
16+
migrations.AlterField(
17+
model_name='eighthsponsor',
18+
name='user',
19+
field=models.OneToOneField(blank=True, null=True, on_delete=intranet.utils.deletion.set_historical_user, related_name='sponsor_obj', to=settings.AUTH_USER_MODEL),
20+
),
21+
]

0 commit comments

Comments
 (0)