Skip to content

Commit

Permalink
Announcements (#168)
Browse files Browse the repository at this point in the history
* attempt to setup announcements

* first pass

* add populate command

* oops

* Use CharField & Remove "Banner" (#166)

* fix(announcement): use char instead of integer for announcement_type

* lint: lol I didn't even have flake8 installed

* update Announcement serializer

* unpin black version + lint

* isort

---------

Co-authored-by: Rohan Moniz <rohan.moniz@gmail.com>

* add filtering on product and end time

* add test cases

* lint

* migration

* address comments

* more validation

---------

Co-authored-by: Eunsoo Shin <me@esinx.net>
Co-authored-by: Eunsoo Shin <62971511+esinx@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 9, 2023
1 parent 4de6262 commit 239130c
Show file tree
Hide file tree
Showing 18 changed files with 525 additions and 11 deletions.
22 changes: 11 additions & 11 deletions backend/Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/Platform/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"options.apps.OptionsConfig",
"accounts.apps.AccountsConfig",
"identity.apps.IdentityConfig",
"announcements.apps.AnnouncementsConfig",
"storages",
]

Expand Down
1 change: 1 addition & 0 deletions backend/Platform/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

urlpatterns = [
path("admin/", admin.site.urls),
path("announcements/", include("announcements.urls", namespace="announcements")),
path("accounts/", include("accounts.urls")),
path("options/", include("options.urls", namespace="options")),
path("identity/", include("identity.urls", namespace="identity")),
Expand Down
Empty file.
6 changes: 6 additions & 0 deletions backend/announcements/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from announcements.models import Announcement, Audience
from django.contrib import admin


admin.site.register(Audience)
admin.site.register(Announcement)
5 changes: 5 additions & 0 deletions backend/announcements/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from django.apps import AppConfig


class AnnouncementsConfig(AppConfig):
name = "announcements"
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from announcements.models import Audience
from django.core.management import BaseCommand


class Command(BaseCommand):
def handle(self, *args, **kwargs):
for audience_name, _ in Audience.AUDIENCE_CHOICES:
Audience.objects.get_or_create(name=audience_name)
76 changes: 76 additions & 0 deletions backend/announcements/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Generated by Django 4.2.7 on 2023-11-09 05:29

from django.db import migrations, models
import django.utils.timezone


class Migration(migrations.Migration):
initial = True

dependencies = []

operations = [
migrations.CreateModel(
name="Audience",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"name",
models.CharField(
choices=[
("MOBILE", "Penn Mobile"),
("OHQ", "OHQ"),
("CLUBS", "Penn Clubs"),
("COURSE_PLAN", "Penn Course Plan"),
("COURSE_REVIEW", "Penn Course Review"),
("COURSE_ALERT", "Penn Course Alert"),
],
max_length=20,
),
),
],
),
migrations.CreateModel(
name="Announcement",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("title", models.CharField(blank=True, max_length=255, null=True)),
("message", models.TextField()),
(
"announcement_type",
models.CharField(
choices=[("NOTICE", "Notice"), ("ISSUE", "Issue")],
default="NOTICE",
max_length=20,
),
),
(
"release_time",
models.DateTimeField(default=django.utils.timezone.now),
),
("end_time", models.DateTimeField(blank=True, null=True)),
(
"audiences",
models.ManyToManyField(
related_name="announcements", to="announcements.audience"
),
),
],
),
]
Empty file.
71 changes: 71 additions & 0 deletions backend/announcements/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
from django.db import models
from django.utils import timezone


class Audience(models.Model):
"""
Represents a product that an announcement is intended for.
"""

AUDIENCE_MOBILE = "MOBILE"
AUDIENCE_OHQ = "OHQ"
AUDIENCE_CLUBS = "CLUBS"
AUDIENCE_COURSE_PLAN = "COURSE_PLAN"
AUDIENCE_COURSE_REVIEW = "COURSE_REVIEW"
AUDIENCE_COURSE_ALERT = "COURSE_ALERT"

AUDIENCE_CHOICES = [
(AUDIENCE_MOBILE, "Penn Mobile"),
(AUDIENCE_OHQ, "OHQ"),
(AUDIENCE_CLUBS, "Penn Clubs"),
(AUDIENCE_COURSE_PLAN, "Penn Course Plan"),
(AUDIENCE_COURSE_REVIEW, "Penn Course Review"),
(AUDIENCE_COURSE_ALERT, "Penn Course Alert"),
]

name = models.CharField(choices=AUDIENCE_CHOICES, max_length=20)

def __str__(self):
return self.name


class Announcement(models.Model):
"""
Represents an announcement for any of the Penn Labs services.
"""

ANNOUNCEMENT_NOTICE = "NOTICE"
ANNOUNCEMENT_ISSUE = "ISSUE"

ANNOUNCEMENT_CHOICES = [
(ANNOUNCEMENT_NOTICE, "Notice"),
(ANNOUNCEMENT_ISSUE, "Issue"),
]

title = models.CharField(
max_length=255,
blank=True,
null=True,
)
message = models.TextField()
announcement_type = models.CharField(
max_length=20,
choices=ANNOUNCEMENT_CHOICES,
default=ANNOUNCEMENT_NOTICE,
)
audiences = models.ManyToManyField("Audience", related_name="announcements")
release_time = models.DateTimeField(default=timezone.now)
end_time = models.DateTimeField(null=True, blank=True)

def __str__(self):
rtime = self.release_time.strftime("%m-%d-%Y %H:%M:%S")
etime = (
f" to {self.end_time.strftime('%m-%d-%Y %H:%M:%S')}"
if self.end_time
else ""
)
aud_str = ",".join([audience.name for audience in self.audiences.all()])
title_str = f"{self.title}: " if self.title else ""

return f"[{self.get_announcement_type_display()} for {aud_str}] \
starting at {rtime}{etime} | {title_str}{self.message}"
17 changes: 17 additions & 0 deletions backend/announcements/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from rest_framework import permissions


class AnnouncementPermissions(permissions.BasePermission):
"""
Grants permission if the current user is a superuser.
"""

def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_authenticated and request.user.is_superuser

def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
return request.user.is_authenticated and request.user.is_superuser
64 changes: 64 additions & 0 deletions backend/announcements/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from announcements.models import Announcement, Audience
from rest_framework import serializers


class AudienceSerializer(serializers.ModelSerializer):
class Meta:
model = Audience
fields = ("name",)


class AnnouncementSerializer(serializers.ModelSerializer):
audiences = serializers.SlugRelatedField(
many=True, slug_field="name", queryset=Audience.objects.all()
)

class Meta:
model = Announcement
fields = (
"id",
"title",
"message",
"announcement_type",
"release_time",
"end_time",
"audiences",
)

def to_representation(self, instance):
representation = super().to_representation(instance)
representation["audiences"] = [
audience.name for audience in instance.audiences.all()
]
return representation

def to_internal_value(self, data):
audiences = data.get("audiences")
if isinstance(audiences, list):
if not audiences:
raise serializers.ValidationError(
{"detail": "You must provide at least one audience"}
)
audience_objs = []
for audience_name in audiences:
audience = Audience.objects.filter(name=audience_name).first()
if not audience:
raise serializers.ValidationError(
{"detail": f"Invalid audience name: {audience_name}"}
)
audience_objs.append(audience)
data["audiences"] = audience_objs
return super().to_internal_value(data)

def create(self, validated_data):
audiences = validated_data.pop("audiences")
instance = Announcement.objects.create(**validated_data)
instance.audiences.set(audiences)
return instance

def update(self, instance, validated_data):
audiences = validated_data.pop("audiences", None)
super().update(instance, validated_data)
if audiences:
instance.audiences.set(audiences)
return instance
8 changes: 8 additions & 0 deletions backend/announcements/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from announcements.views import AnnouncementsViewSet
from rest_framework import routers


app_name = "announcements"
router = routers.SimpleRouter()
router.register("", AnnouncementsViewSet, basename="announcements")
urlpatterns = router.urls
Loading

0 comments on commit 239130c

Please sign in to comment.