Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ad5d9b3
Add UserEvent model representing a user event in the server
RohanJnr Oct 10, 2020
812aeab
Make migration for the new UserEvent model
RohanJnr Oct 10, 2020
629f311
Refrence UserEvent model in __init__ files
RohanJnr Oct 10, 2020
4b51410
Register UserEvent model in admin interface with custom fitlering and…
RohanJnr Oct 10, 2020
35c244d
Serialize UserEvent model via UserEventSerializer
RohanJnr Oct 10, 2020
8e7d723
Add UserEventViewSet to provide CRUD operations on UserEvent model vi…
RohanJnr Oct 10, 2020
37b3ef9
Refrence UserEventViewSet in __init__ files
RohanJnr Oct 10, 2020
5db8756
New URL endpoint to access CRUD operations provided by UserEventViewSet
RohanJnr Oct 10, 2020
e46c162
Add CreationTests, UpdateTests and FilterTests for UserEventViewSet
RohanJnr Oct 10, 2020
0a9e562
Merge branch 'master' of https://github.com/python-discord/site into …
RohanJnr Oct 10, 2020
700b852
Remove duplicate line in documentation
RohanJnr Oct 10, 2020
efba585
Merge remote-tracking branch 'upstream/master' into user_events_manag…
RohanJnr Oct 11, 2020
84460c9
Add description field in UserEvents model and makemigrations
RohanJnr Oct 11, 2020
374b31e
add message_id field and make description a required field
RohanJnr Oct 26, 2020
f2fe82a
remove subscription filtering and update docs
RohanJnr Oct 26, 2020
4c336aa
modify tests to use updated model/serializer changes
RohanJnr Oct 26, 2020
eaddebb
Add db model for Scheduled User Event with validators/validation
RohanJnr Oct 26, 2020
2f60ec4
Register ScheduledEvent model in admin + minor changes to UserEventAdmin
RohanJnr Oct 26, 2020
2da9353
Migrations for new UserEvent and ScheduledEvent models
RohanJnr Oct 26, 2020
7707489
Add serializer for ScheduledEvent model + changes to UserEventSerializer
RohanJnr Oct 26, 2020
54e0379
Add ScheduledEventViewSet to provide CRUD operations on ScheduledEven…
RohanJnr Oct 26, 2020
012320c
New URL endpoint to access CRUD operations provided by ScheduledEvent…
RohanJnr Oct 26, 2020
e179a07
Add tests for ScheduledEventViewSet
RohanJnr Oct 26, 2020
7896eed
Merge branch 'master' into user_events_management
RohanJnr Oct 26, 2020
7cc8495
Update ViewSet documentation
RohanJnr Oct 28, 2020
a052ed7
Merge branch 'user_events_management' of https://github.com/RohanJnr/…
RohanJnr Oct 28, 2020
2274f95
Add comments for better clarity about code
RohanJnr Oct 28, 2020
ba50e91
Add help_text on model fields
RohanJnr Oct 28, 2020
dff697d
Add django reference link for better clarity
RohanJnr Oct 28, 2020
11d17ef
Fix typo
RohanJnr Oct 28, 2020
1aef505
Fix typo.
RohanJnr Nov 2, 2020
0185e81
Add validation check for scheduled user event duration.
RohanJnr Nov 4, 2020
0a7a57a
Add tests for new validation checks on scheduled event duration.
RohanJnr Nov 4, 2020
3777df8
Merge branch 'master' into user_events_management
RohanJnr Nov 4, 2020
4e25e9e
Run make migrations on field updates.
RohanJnr Nov 4, 2020
940c03a
Remove update endpoints for ScheduledEventViewSet.
RohanJnr Nov 4, 2020
76348a9
Fix typo.
RohanJnr Nov 5, 2020
31539a9
api/bot/user-events lookups are now case insensitive.
RohanJnr Nov 6, 2020
c060d7e
Remove subscriptions field in UserEvent model and references.
RohanJnr Nov 6, 2020
1ac4a7b
Update UserEventViewSet documentation.
RohanJnr Nov 6, 2020
a5ad2ea
Use select_related() on foreignkey relationships to optimize queries.
RohanJnr Nov 8, 2020
fdcd517
Merge branch 'master' into user_events_management
RohanJnr Dec 21, 2020
00afec1
Merge branch 'python-discord:main' into user_events_management
RohanJnr Aug 19, 2021
ffb07ec
Merge branch 'main' of https://github.com/python-discord/site into us…
RohanJnr Aug 20, 2021
288576d
Modify message_id help text.
RohanJnr Aug 20, 2021
cf196db
migrations merge.
RohanJnr Aug 20, 2021
406567c
Merge branch 'user_events_management' of https://github.com/RohanJnr/…
RohanJnr Aug 20, 2021
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
50 changes: 49 additions & 1 deletion pydis_site/apps/api/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
OffTopicChannelName,
OffensiveMessage,
Role,
User
ScheduledEvent,
User,
UserEvent
)
from .models.bot.nomination import NominationEntry

Expand Down Expand Up @@ -465,3 +467,49 @@ def has_add_permission(self, *args) -> bool:
def has_change_permission(self, *args) -> bool:
"""Prevent editing from django admin."""
return False


class UserEventOrganizerFilter(admin.SimpleListFilter):
"""List Filter for User Event list Admin page."""

title = "Organizer"
parameter_name = "organizer"

def lookups(self, request: HttpRequest, model: UserAdmin) -> Iterable[Tuple[str, str]]:
"""Selectable values for viewer to filter by."""
user_events = UserEvent.objects.select_related("organizer")
organizers = {event.organizer for event in user_events}
return ((organizer.id, organizer.username) for organizer in organizers)

def queryset(self, request: HttpRequest, queryset: QuerySet) -> Optional[QuerySet]:
"""Query to filter the list of User Events against."""
if not self.value():
return
return queryset.filter(organizer=self.value())


@admin.register(UserEvent)
class UserEventAdmin(admin.ModelAdmin):
"""Admin formatting for UserEvents model."""

def organizer_username(self, user_event: UserEvent) -> str:
"""Return organizer's username."""
return user_event.organizer.username

organizer_username.short_description = "Organizer"

list_display = ("name", "organizer_username")
list_filter = ("name", UserEventOrganizerFilter)


@admin.register(ScheduledEvent)
class ScheduledEventAdmin(admin.ModelAdmin):
"""Admin formatting for ScheduledEvent model."""

def user_event_name(self, scheduled_event: ScheduledEvent) -> str:
"""Return organizer's username."""
return scheduled_event.user_event.name

user_event_name.short_description = "User Event"

list_display = ("user_event_name", "start_time", "end_time")
36 changes: 36 additions & 0 deletions pydis_site/apps/api/migrations/0068_scheduledevent_userevent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Generated by Django 3.0.9 on 2020-10-26 18:49

import django.core.validators
from django.db import migrations, models
import django.db.models.deletion
import pydis_site.apps.api.models.mixins


class Migration(migrations.Migration):

dependencies = [
('api', '0067_add_voice_ban_infraction_type'),
]

operations = [
migrations.CreateModel(
name='UserEvent',
fields=[
('name', models.CharField(help_text='Name of the user event.', max_length=64, primary_key=True, serialize=False, verbose_name='Event Name')),
('description', models.TextField()),
('message_id', models.BigIntegerField(help_text='The message ID of the message sent in user events channel.', unique=True, validators=[django.core.validators.MinValueValidator(limit_value=0, message='message IDs cannot be negative.')])),
('organizer', models.ForeignKey(help_text='The event organizer.', on_delete=django.db.models.deletion.CASCADE, to='api.User')),
('subscriptions', models.ManyToManyField(blank=True, related_name='subscriptions', to='api.User')),
],
bases=(pydis_site.apps.api.models.mixins.ModelReprMixin, models.Model),
),
migrations.CreateModel(
name='ScheduledEvent',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('start_time', models.DateTimeField(unique=True)),
('end_time', models.DateTimeField(unique=True)),
('user_event', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='api.UserEvent')),
],
),
]
34 changes: 34 additions & 0 deletions pydis_site/apps/api/migrations/0069_auto_20201104_1833.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Generated by Django 3.0.9 on 2020-11-04 18:33

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


class Migration(migrations.Migration):

dependencies = [
('api', '0068_scheduledevent_userevent'),
]

operations = [
migrations.AlterField(
model_name='scheduledevent',
name='end_time',
field=models.DateTimeField(help_text='The end time of the scheduled user event.', unique=True),
),
migrations.AlterField(
model_name='scheduledevent',
name='start_time',
field=models.DateTimeField(help_text='The start time of the scheduled user event.', unique=True),
),
migrations.AlterField(
model_name='scheduledevent',
name='user_event',
field=models.OneToOneField(help_text='The user event which is scheduled.', on_delete=django.db.models.deletion.CASCADE, to='api.UserEvent'),
),
migrations.AlterField(
model_name='userevent',
name='subscriptions',
field=models.ManyToManyField(blank=True, help_text="List of discord user id's how those who have subscribed to this event.", related_name='subscriptions', to='api.User'),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 3.0.9 on 2020-11-06 17:55

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0069_auto_20201104_1833'),
]

operations = [
migrations.RemoveField(
model_name='userevent',
name='subscriptions',
),
]
14 changes: 14 additions & 0 deletions pydis_site/apps/api/migrations/0072_merge_20210820_0724.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Generated by Django 3.0.14 on 2021-08-20 07:24

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
('api', '0071_increase_message_content_4000'),
('api', '0070_remove_userevent_subscriptions'),
]

operations = [
]
4 changes: 3 additions & 1 deletion pydis_site/apps/api/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@
OffTopicChannelName,
Reminder,
Role,
User
ScheduledEvent,
User,
UserEvent
)
2 changes: 2 additions & 0 deletions pydis_site/apps/api/models/bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@
from .offensive_message import OffensiveMessage
from .reminder import Reminder
from .role import Role
from .scheduled_event import ScheduledEvent
from .user import User
from .user_event import UserEvent
78 changes: 78 additions & 0 deletions pydis_site/apps/api/models/bot/scheduled_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
from collections import namedtuple

from django.core.exceptions import ValidationError
from django.db import models

from .user_event import UserEvent

DATETIME_RANGE = namedtuple("datetime_range", ("start", "end"))


class ScheduledEvent(models.Model):
"""A scheduled user event."""

user_event = models.OneToOneField(
UserEvent,
on_delete=models.CASCADE,
help_text="The user event which is scheduled."
)
start_time = models.DateTimeField(
unique=True,
help_text="The start time of the scheduled user event."
)
end_time = models.DateTimeField(
unique=True,
help_text="The end time of the scheduled user event."
)

def clean(self) -> None:
"""
Check for event time overlap and if an organizer has already scheduled an event.

Note: not overriding the save() method as the save() method is not called when creating
model instances via the admin interface.
"""
# Check if organizer has already scheduled an event
scheduled_events_by_organizer = ScheduledEvent.objects.filter(
user_event__organizer=self.user_event.organizer
)
if scheduled_events_by_organizer:
raise ValidationError(
{
"user_event": [
f"Organizer {self.user_event.organizer} has already "
f"scheduled an event!"
]
}
)

# Check for event duration
# Event should be at least 30min or at most 5 hours
duration = (self.end_time - self.start_time).total_seconds()
if duration < 30*60 or duration > 5*60*60:
raise ValidationError(
{
"end_time": ["Event should be at least 30min or at most 5 hours."]
}
)

# Check for time overlap
new_dt_range = DATETIME_RANGE(self.start_time, self.end_time)

scheduled_events = ScheduledEvent.objects.select_related("user_event").all()

for event in scheduled_events:
event_dt_range = DATETIME_RANGE(event.start_time, event.end_time)
late_start = max(new_dt_range.start, event_dt_range.start)
early_end = min(new_dt_range.end, event_dt_range.end)

# 30min should be the minimum time gap between 2 events
if (late_start - early_end).total_seconds() < 30*60:
raise ValidationError(
{
"start_time": [
f"Event schedule overlaps with already "
f"scheduled event {event.user_event.name}."
]
}
)
35 changes: 35 additions & 0 deletions pydis_site/apps/api/models/bot/user_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from django.core.validators import MinValueValidator
from django.db import models

from pydis_site.apps.api.models.bot.user import User
from pydis_site.apps.api.models.mixins import ModelReprMixin


class UserEvent(ModelReprMixin, models.Model):
"""A user event in the server."""

name = models.CharField(
max_length=64,
verbose_name="Event Name",
primary_key=True,
help_text="Name of the user event."
)
organizer = models.ForeignKey(
User,
on_delete=models.CASCADE,
help_text="The event organizer."
)
description = models.TextField()
message_id = models.BigIntegerField(
unique=True,
validators=(
MinValueValidator(
limit_value=0,
message="message IDs cannot be negative."
),
),
help_text=(
"The ID of the message "
"listing the event."
)
)
64 changes: 63 additions & 1 deletion pydis_site/apps/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Converters from Django models to data interchange formats and back."""
from django.core.exceptions import ValidationError as DjangoValidationError
from django.db.models.query import QuerySet
from django.db.utils import IntegrityError
from rest_framework.exceptions import NotFound
Expand All @@ -25,7 +26,9 @@
OffensiveMessage,
Reminder,
Role,
User
ScheduledEvent,
User,
UserEvent
)


Expand Down Expand Up @@ -379,3 +382,62 @@ class Meta:

model = OffensiveMessage
fields = ('id', 'channel_id', 'delete_date')


class UserEventSerializer(ModelSerializer):
"""A class providing (de-)serialization of `UserEvent` instances."""

class Meta:
"""Metadata defined for the Django REST Framework."""

model = UserEvent
fields = ("name", "organizer", "description", "message_id")


class ScheduledEventSerializer(ModelSerializer):
"""A class providing (de-)serialization of `UserEvent` instances."""

user_event = UserEventSerializer(read_only=True)
user_event_name = PrimaryKeyRelatedField(
queryset=UserEvent.objects.select_related("organizer"),
source="user_event", write_only=True
)

class Meta:
"""Metadata defined for the Django REST Framework."""

model = ScheduledEvent
fields = ("id", "user_event_name", "user_event", "start_time", "end_time")

def validate(self, attrs: dict) -> dict:
"""
Catch model validation errors and send 400 response.

This is being done because the DRF uses model_instance.save()
method and the clean() method is not called in save()
hence, manually calling clean() and checking for validation errors.

reference of the above:
https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.full_clean
https://docs.djangoproject.com/en/3.1/ref/models/instances/#django.db.models.Model.clean
Note:
`
Note that full_clean() will not be called automatically when you call your model’s
save() method. You’ll need to call it manually when you want to run
one-step model validation for your own manually created models.
`

`
Note, however, that like Model.full_clean(), a model’s clean()
method is not invoked when you call your model’s save() method.
`
- from django docs (follow the above links).

Note: only works for POST requests.
"""
scheduled_event = ScheduledEvent(**attrs)
try:
scheduled_event.clean()
except DjangoValidationError as e:
raise ValidationError(e.message_dict)
return attrs
Loading