Add assignments system

This commit is contained in:
EEKIM10 2022-10-09 19:27:02 +01:00
parent 132458e080
commit 647e138f2f
9 changed files with 2177 additions and 13 deletions

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="dataSourceStorageLocal" created-in="PY-222.4167.33">
<data-source name="main" uuid="28efee07-d306-4126-bf69-01008b4887e2">
<database-info product="SQLite" version="3.39.2" jdbc-version="2.1" driver-name="SQLite JDBC" driver-version="3.39.2.0" dbms="SQLITE" exact-version="3.39.2" exact-driver-version="3.39">
<identifier-quote-string>&quot;</identifier-quote-string>
</database-info>
<case-sensitivity plain-identifiers="mixed" quoted-identifiers="mixed" />
<auth-provider>no-auth</auth-provider>
<schema-mapping>
<introspection-scope>
<node kind="schema" qname="@" />
</introspection-scope>
</schema-mapping>
</data-source>
</component>
</project>

12
.idea/dataSources.xml Normal file
View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="main" uuid="28efee07-d306-4126-bf69-01008b4887e2">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:$PROJECT_DIR$/main.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

File diff suppressed because it is too large Load diff

6
.idea/sqldialects.xml Normal file
View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

578
cogs/assignments.py Normal file
View file

@ -0,0 +1,578 @@
import datetime
import sqlite3
import textwrap
from typing import Optional
import discord
from discord.ext import commands, tasks
import config
from utils import Assignments, Tutors, simple_embed_paginator, get_or_none, Student, hyperlink
TUTOR_OPTION = discord.Option(
str,
"The tutor who assigned the project",
default=None,
choices=[x.title() for x in dir(Tutors) if not x.startswith("__") and x not in ("name", "value")]
)
BOOL_EMOJI = {
True: "\N{white heavy check mark}",
False: "\N{cross mark}"
}
class TutorSelector(discord.ui.View):
value: Optional[Tutors] = None
@discord.ui.select(
placeholder="Select a tutor name",
options=[
discord.SelectOption(
label=x.title(),
value=x.upper()
)
for x in [y.name for y in TUTOR_OPTION.choices]
]
)
async def select_tutor(self, select: discord.ui.Select, interaction2: discord.Interaction):
await interaction2.response.defer(invisible=True)
self.value = getattr(Tutors, select.values[0].upper())
self.stop()
async def assignment_autocomplete(ctx: discord.AutocompleteContext) -> list[str]:
if not ctx.value:
results: list[Assignments] = await Assignments.objects.order_by("-due_by").limit(7).all()
else:
results: list[Assignments] = await Assignments.objects.filter(
title__icontains=ctx.value
).limit(30).order_by("-entry_id").all()
return [
textwrap.shorten(f"{x.entry_id}: {x.title}", 100, placeholder="...")
for x in results
]
# noinspection DuplicatedCode
class AssignmentsCog(commands.Cog):
def __init__(self, bot: commands.Bot):
self.bot = bot
self.reminder_loop.start()
def cog_unload(self):
self.reminder_loop.stop()
@tasks.loop(minutes=10)
async def reminder_loop(self):
if not self.bot.is_ready():
await self.bot.wait_until_ready()
view_command = "</{0.name} view:{0.id}>".format(
self.bot.get_application_command(
"assignments",
type=discord.SlashCommandGroup
)
)
edit_command = "</{0.name} edit:{0.id}>".format(
self.bot.get_application_command(
"assignments",
type=discord.SlashCommandGroup
)
)
allowed_mentions = discord.AllowedMentions(everyone=True) if not config.dev else discord.AllowedMentions.none()
guild = self.bot.get_guild(config.guilds[0])
general = discord.utils.get(guild.text_channels, name="general")
if not general.can_send():
return
msg_format = "@everyone {reminder_name} reminder for project {project_title} for **{project_tutor}**!\n" \
"Run '%s {project_title}' to view information on the assignment.\n" \
"*You can mark this assignment as complete with '%s {project_title}', which will prevent" \
" further reminders.*" % (view_command, edit_command)
now = datetime.datetime.now()
assignments: list[Assignments] = await Assignments.objects.filter(submitted=False).all()
for assignment in assignments:
due = datetime.datetime.fromtimestamp(assignment.due_by)
for reminder_name, reminder_time in config.reminders.items():
if reminder_name in assignment.reminders:
# already sent
continue
elif reminder_time != 3600 * 3 and assignment.finished is True:
continue
elif isinstance(reminder_time, int) and reminder_time >= (assignment.due_by - assignment.created_at):
await assignment.update(reminders=assignment.reminders + [reminder_name])
else:
if isinstance(reminder_time, datetime.time):
if now.date() == due.date:
if now.time().hour == reminder_time.hour:
try:
await general.send(
msg_format.format(
reminder_name=reminder_name,
project_title=textwrap.shorten(assignment.title, 100, placeholder="..."),
project_tutor=assignment.tutor.name.title()
),
allowed_mentions=allowed_mentions
)
except discord.HTTPException:
pass
else:
await assignment.update(reminders=assignment.reminders + [reminder_name])
else:
time = due - datetime.timedelta(seconds=reminder_time)
if time <= now:
try:
await general.send(
msg_format.format(
reminder_name=reminder_name,
project_title=textwrap.shorten(assignment.title, 100, placeholder="..."),
project_tutor=assignment.tutor.name.title()
),
allowed_mentions=allowed_mentions
)
except discord.HTTPException:
pass
else:
await assignment.update(reminders=assignment.reminders + [reminder_name])
@staticmethod
def generate_assignment_embed(assignment: Assignments) -> discord.Embed:
embed = discord.Embed(
title=f"Assignment #{assignment.entry_id}",
description=f"**Title:**\n>>> {assignment.title}",
colour=discord.Colour.random()
)
if assignment.classroom:
classroom = hyperlink(assignment.classroom, max_length=1024)
else:
classroom = "No classroom link."
if assignment.shared_doc:
shared_doc = hyperlink(assignment.shared_doc, max_length=1024)
else:
shared_doc = "No shared document."
embed.add_field(name="Classroom URL:", value=classroom, inline=False),
embed.add_field(name="Shared Document URL:", value=shared_doc)
embed.add_field(name="Tutor:", value=assignment.tutor.name.title(), inline=False)
embed.add_field(
name="Created:",
value=f"<t:{assignment.created_at:.0f}:R> by <@{assignment.created_by.user_id}>",
inline=False
)
embed.add_field(
name="Due:",
value=f"<t:{assignment.due_by:.0f}:R> "
f"(finished: {BOOL_EMOJI[assignment.finished]} | Submitted: {BOOL_EMOJI[assignment.submitted]})",
inline=False
)
if assignment.reminders:
embed.set_footer(text="Reminders sent: " + ", ".join(assignment.reminders))
return embed
assignments_command = discord.SlashCommandGroup(
"assignments",
"Assignment/project management",
guild_only=True
)
@assignments_command.command(name="list")
async def list_assignments(
self,
ctx: discord.ApplicationContext,
limit: int = 20,
upcoming_only: bool = False,
tutor_name: TUTOR_OPTION = None,
unfinished_only: bool = False,
unsubmitted_only: bool = False
):
"""Lists assignments."""
tutor_name: Optional[str]
query = Assignments.objects.limit(limit).order_by("-due_by")
if upcoming_only is True:
now = datetime.datetime.now().timestamp()
query = query.filter(due_by__gte=now)
if tutor_name is not None:
query = query.filter(tutor=getattr(Tutors, tutor_name.upper()))
if unfinished_only is True:
query = query.filter(finished=False)
if unsubmitted_only:
query = query.filter(submitted=False)
await ctx.defer()
lines = []
for assignment in await query.all():
assignment: Assignments
due_by = datetime.datetime.fromtimestamp(assignment.due_by)
lines.append(
f"#{assignment.entry_id!s}: Set by **{assignment.tutor.name.title()}**, "
f"due {discord.utils.format_dt(due_by, 'R')}"
)
embeds = simple_embed_paginator(lines, assert_ten=True, colour=ctx.author.colour)
embeds = embeds or [
discord.Embed(description="No projects match the provided criteria.")
]
return await ctx.respond(
embeds=embeds
)
@assignments_command.command(name="add")
async def create_assignment(self, ctx: discord.ApplicationContext):
"""Adds/creates an assignment."""
author = await get_or_none(Student, user_id=ctx.author.id)
if author is None:
return await ctx.respond("\N{cross mark} You must have verified to use this command.", ephemeral=True)
class AddModal(discord.ui.Modal):
def __init__(self, kwargs: dict = None):
self.msg: Optional[discord.WebhookMessage] = None
self.create_kwargs = kwargs or {
"created_by": author,
"title": None,
"classroom": None,
"shared_doc": None,
"due_by": None,
"tutor": None
}
super().__init__(
discord.ui.InputText(
custom_id="title",
label="Assignment Title",
min_length=2,
max_length=2000,
value=self.create_kwargs["title"]
),
discord.ui.InputText(
custom_id="classroom",
label="Google Classroom Link",
max_length=4000,
required=False,
placeholder="Optional, can be added later.",
value=self.create_kwargs["classroom"]
),
discord.ui.InputText(
custom_id="shared_doc",
label="Shared Document Link",
max_length=4000,
required=False,
placeholder="Google docs, slides, powerpoint, etc. Optional.",
value=self.create_kwargs["shared_doc"]
),
discord.ui.InputText(
custom_id="due_by",
label="Due by",
max_length=16,
min_length=14,
placeholder="dd/mm/yy hh:mm".upper(),
value=(
self.create_kwargs["due_by"].strftime("%d/%m/%y %H:%M")
if self.create_kwargs["due_by"] else None
)
),
title="Add an assignment",
timeout=300
)
async def callback(self, interaction: discord.Interaction):
await interaction.response.defer()
self.create_kwargs["title"] = self.children[0].value
self.create_kwargs["classroom"] = self.children[1].value or None
self.create_kwargs["shared_doc"] = self.children[2].value or None
try:
self.create_kwargs["due_by"] = datetime.datetime.strptime(
self.children[3].value,
"%d/%m/%y %H:%M" if len(self.children[3].value) == 14 else "%d/%m/%Y %H:%M"
)
except ValueError:
class TryAgainView(discord.ui.View):
def __init__(self, kw):
self._mod = None
self.kw = kw
super().__init__(timeout=330)
@property
def modal(self) -> Optional[AddModal]:
return self._mod
@discord.ui.button(label="Try again", style=discord.ButtonStyle.primary)
async def try_again(self, _, interaction2: discord.Interaction):
self.disable_all_items()
self._mod = AddModal(self.kw)
await interaction2.response.send_modal(self._mod)
await interaction2.edit_original_response(view=self)
await self._mod.wait()
self.stop()
v = TryAgainView(self.create_kwargs)
msg = await interaction.followup.send(
"\N{cross mark} Failed to parse date - try again?",
view=v
)
await v.wait()
if v.modal:
self.create_kwargs = v.modal.create_kwargs
else:
return
else:
view = TutorSelector()
msg = await interaction.followup.send(
"Which tutor assigned this project?",
view=view
)
await view.wait()
self.create_kwargs["tutor"] = view.value
self.msg = msg
self.stop()
modal = AddModal()
await ctx.send_modal(modal)
await modal.wait()
if not modal.msg:
return
await modal.msg.edit(
content="Creating assignment...",
view=None
)
try:
modal.create_kwargs["due_by"] = modal.create_kwargs["due_by"].timestamp()
await Assignments.objects.create(**modal.create_kwargs)
except sqlite3.Error as e:
return await modal.msg.edit(content="SQL Error: %s.\nAssignment not saved." % e)
else:
return await modal.msg.edit(
content=f"\N{white heavy check mark} Created assignment!"
)
@assignments_command.command(name="view")
async def get_assignment(
self,
ctx: discord.ApplicationContext,
title: discord.Option(str, autocomplete=assignment_autocomplete)
):
"""Views an assignment's details"""
try:
entry_id, *_ = title.split(":")
except ValueError:
return await ctx.respond("\N{cross mark} Invalid Input.")
assignment: Assignments = await get_or_none(Assignments, entry_id=int(entry_id))
if not assignment:
return await ctx.respond("\N{cross mark} Unknown assignment.")
await assignment.created_by.load()
return await ctx.respond(embed=self.generate_assignment_embed(assignment))
@assignments_command.command(name="edit")
async def edit_assignment(
self,
ctx: discord.ApplicationContext,
title: discord.Option(str, autocomplete=assignment_autocomplete)
):
"""Edits an assignment"""
try:
entry_id, *_ = title.split(":")
except ValueError:
return await ctx.respond("\N{cross mark} Invalid Input.")
assignment: Assignments = await get_or_none(Assignments, entry_id=int(entry_id))
if not assignment:
return await ctx.respond("\N{cross mark} Unknown assignment.")
await assignment.created_by.load()
class EditAssignmentView(discord.ui.View):
def __init__(self):
super().__init__(timeout=300)
async def interaction_check(self, interaction: discord.Interaction) -> bool:
return interaction.user == ctx.author
async def on_timeout(self) -> None:
await self.message.delete(delay=0.1)
@discord.ui.button(label="Update title")
async def update_title(self, _, interaction: discord.Interaction):
class UpdateTitleModal(discord.ui.Modal):
def __init__(self):
super().__init__(
discord.ui.InputText(
style=discord.InputTextStyle.long,
label="New title",
value=assignment.title,
min_length=2,
max_length=4000,
),
title="Update assignment title"
)
async def callback(self, _interaction: discord.Interaction):
await _interaction.response.defer()
await assignment.update(title=self.children[0].value)
await _interaction.followup.send(
"\N{white heavy check mark} Changed assignment title!",
delete_after=5
)
self.stop()
modal = UpdateTitleModal()
await interaction.response.send_modal(modal)
await modal.wait()
@discord.ui.button(label="Update classroom URL")
async def update_classroom_url(self, _, interaction: discord.Interaction):
class UpdateClassroomURL(discord.ui.Modal):
def __init__(self):
super().__init__(
discord.ui.InputText(
style=discord.InputTextStyle.long,
label="New Classroom URL",
value=assignment.classroom,
required=False,
max_length=4000,
),
title="Update Classroom url"
)
async def callback(self, _interaction: discord.Interaction):
await _interaction.response.defer()
try:
await assignment.update(classroom=self.children[0].value)
await _interaction.followup.send(
"\N{white heavy check mark} Changed classroom URL!",
delete_after=5
)
except sqlite3.Error:
await _interaction.followup.send(
"\N{cross mark} Failed to apply changes - are you sure you put a valid URL in?"
)
finally:
self.stop()
modal = UpdateClassroomURL()
await interaction.response.send_modal(modal)
await modal.wait()
@discord.ui.button(label="Update shared document url")
async def update_shared_document_url(self, _, interaction: discord.Interaction):
class UpdateSharedDocumentModal(discord.ui.Modal):
def __init__(self):
super().__init__(
discord.ui.InputText(
style=discord.InputTextStyle.long,
label="New shared document URL",
value=assignment.shared_doc,
required=False,
max_length=4000,
),
title="Update shared document url"
)
async def callback(self, _interaction: discord.Interaction):
await _interaction.response.defer()
try:
await assignment.update(shared_doc=self.children[0].value)
await _interaction.followup.send(
"\N{white heavy check mark} Changed shared doc URL!",
delete_after=5
)
except sqlite3.Error:
await _interaction.followup.send(
"\N{cross mark} Failed to apply changes - are you sure you put a valid URL in?"
)
finally:
self.stop()
modal = UpdateSharedDocumentModal()
await interaction.response.send_modal(modal)
await modal.wait()
@discord.ui.button(label="Update tutor")
async def update_tutor(self, _, interaction: discord.Interaction):
await interaction.response.defer()
view = TutorSelector()
msg: discord.WebhookMessage = await interaction.followup.send(
"Which tutor assigned this project?",
view=view
)
await view.wait()
await assignment.update(tutor=view.value)
await msg.edit(
content=f"\N{white heavy check mark} Changed tutor to {view.value.name.title()}",
view=None
)
await msg.delete(delay=5)
@discord.ui.button(label="Update due date", disabled=True)
async def update_due(self, _, interaction: discord.Interaction):
pass
@discord.ui.button(label="Mark as [in]complete")
async def mark_as_complete(self, _, interaction: discord.Interaction):
await interaction.response.defer()
if assignment.submitted is True and assignment.submitted is True:
return await interaction.followup.send(
"\N{cross mark} You cannot mark an assignment as incomplete if it is marked as submitted!"
)
await assignment.update(finished=not assignment.finished)
return await interaction.followup.send(
"\N{white heavy check mark} Assignment is now marked as {}complete.".format(
"in" if assignment.finished is False else ""
)
)
@discord.ui.button(label="Mark as [un]submitted")
async def mark_as_submitted(self, _, interaction: discord.Interaction):
await interaction.response.defer()
if assignment.finished is False and assignment.submitted is False:
return await interaction.followup.send(
"\N{cross mark} You cannot mark an assignment as submitted if it is not marked as complete!",
delete_after=10
)
await assignment.update(submitted=not assignment.submitted)
return await interaction.followup.send(
"\N{white heavy check mark} Assignment is now marked as {}submitted.".format(
"in" if assignment.submitted is False else ""
),
delete_after=5
)
@discord.ui.button(label="Save & Exit")
async def finish(self, _, interaction: discord.Interaction):
await interaction.response.defer()
await interaction.delete_original_response(delay=0.1)
self.stop()
@discord.ui.button(label="View details")
async def view_details(self, _, interaction: discord.Interaction):
await interaction.response.defer(ephemeral=True)
await assignment.created_by.load()
await interaction.followup.send(
embed=AssignmentsCog.generate_assignment_embed(assignment),
ephemeral=True
)
await ctx.respond(view=EditAssignmentView())
@assignments_command.command(name="remove")
async def remove_assignment(
self,
ctx: discord.ApplicationContext,
title: discord.Option(str, autocomplete=assignment_autocomplete)
):
"""Edits an assignment"""
try:
entry_id, *_ = title.split(":")
except ValueError:
return await ctx.respond("\N{cross mark} Invalid Input.")
assignment: Assignments = await get_or_none(Assignments, entry_id=int(entry_id))
if not assignment:
return await ctx.respond("\N{cross mark} Unknown assignment.")
await assignment.delete()
return await ctx.respond(f"\N{white heavy check mark} Deleted assignment #{assignment.entry_id}.")
def setup(bot):
bot.add_cog(AssignmentsCog(bot))

View file

@ -16,21 +16,15 @@ class Events(commands.Cog):
@commands.Cog.listener("on_raw_reaction_add") @commands.Cog.listener("on_raw_reaction_add")
async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent): async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
print("[raw_add]", payload)
channel: Optional[discord.TextChannel] = self.bot.get_channel(payload.channel_id) channel: Optional[discord.TextChannel] = self.bot.get_channel(payload.channel_id)
print("[raw_add]", channel)
if channel is not None: if channel is not None:
try: try:
message: discord.Message = await channel.fetch_message(payload.message_id) message: discord.Message = await channel.fetch_message(payload.message_id)
print("[raw_add]", message)
except discord.HTTPException: except discord.HTTPException:
return return
if message.author.id == self.bot.user.id: if message.author.id == self.bot.user.id:
print("[raw_add]", message.author) if payload.emoji.name == "\N{wastebasket}\U0000fe0f":
if payload.emoji == discord.PartialEmoji.from_str("\N{wastebasket}"):
print("[raw_add]", payload.emoji)
await message.delete(delay=1) await message.delete(delay=1)
print("[raw_add] removed")
@commands.Cog.listener() @commands.Cog.listener()
async def on_member_join(self, member: discord.Member): async def on_member_join(self, member: discord.Member):
@ -61,6 +55,16 @@ class Events(commands.Cog):
f"{RTL} {member.mention} {f'({student.id})' if student else '(pending verification)'}" f"{RTL} {member.mention} {f'({student.id})' if student else '(pending verification)'}"
) )
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
if message.channel.name == "pinboard":
try:
await message.pin(reason="Automatic pinboard pinning")
except discord.HTTPException as e:
return await message.reply(f"Failed to auto-pin: {e}", delete_after=10)
elif message.type == discord.MessageType.pins_add:
await message.delete(delay=0.01)
def setup(bot): def setup(bot):
bot.add_cog(Events(bot)) bot.add_cog(Events(bot))

View file

@ -14,6 +14,7 @@ bot.load_extension("jishaku")
bot.load_extension("cogs.verify") bot.load_extension("cogs.verify")
bot.load_extension("cogs.mod") bot.load_extension("cogs.mod")
bot.load_extension("cogs.events") bot.load_extension("cogs.events")
bot.load_extension("cogs.assignments")
bot.loop.run_until_complete(registry.create_all()) bot.loop.run_until_complete(registry.create_all())
@ -29,4 +30,5 @@ async def ping(ctx: discord.ApplicationContext):
return await ctx.respond(f"\N{white heavy check mark} Pong! `{gateway}ms`.") return await ctx.respond(f"\N{white heavy check mark} Pong! `{gateway}ms`.")
if __name__ == "__main__":
bot.run(config.token) bot.run(config.token)

View file

@ -1,4 +1,52 @@
from typing import Optional
from urllib.parse import urlparse
from ._email import * from ._email import *
from .db import * from .db import *
from .console import * from .console import *
from .views import * from .views import *
def simple_embed_paginator(
lines: list[str],
*,
assert_ten: bool = False,
empty_is_none: bool = True,
**kwargs
) -> Optional[list[discord.Embed]]:
"""Paginates x lines into x embeds."""
if not lines and empty_is_none is True:
return
kwargs.setdefault("description", "")
embeds = [discord.Embed(**kwargs)]
for line in lines:
embed = embeds[-1]
total_length = len(embed)
description_length = len(embed.description)
if total_length + len(line) > 6000 or description_length + len(line) > 4096:
embed = discord.Embed(**kwargs)
embed.description += line + "\n"
embeds.append(embed)
else:
embed.description += line + "\n"
if assert_ten:
assert len(embeds) <= 10, "Too many embeds."
return embeds
def hyperlink(url: str, *, text: str = None, max_length: int = None) -> str:
if max_length < len(url):
raise ValueError(f"Max length ({max_length}) is too low for provided URL ({len(url)}). Hyperlink impossible.")
fmt = "[{}]({})"
if text:
fmt = fmt.format(text, url)
else:
parsed = urlparse(url)
fmt = fmt.format(parsed.hostname, url)
if len(fmt) > max_length:
return url
return fmt

View file

@ -1,25 +1,35 @@
import datetime import datetime
import uuid import uuid
from typing import TYPE_CHECKING, Optional, TypeVar from typing import TYPE_CHECKING, Optional, TypeVar
from enum import IntEnum, auto
import orm import orm
from databases import Database from databases import Database
import os import os
from pathlib import Path from pathlib import Path
class Tutors(IntEnum):
JAY = auto()
ZACH = auto()
IAN = auto()
REBECCA = auto()
LUPUPA = auto()
OTHER = -1 # not auto() because if we add more it could mess things up.
os.chdir(Path(__file__).parent.parent) os.chdir(Path(__file__).parent.parent)
__all__ = [ __all__ = [
"registry", "registry",
"get_or_none" "get_or_none",
]
_models = [
"VerifyCode", "VerifyCode",
"Student", "Student",
"BannedStudentID" "BannedStudentID",
"Assignments",
"Tutors"
] ]
__all__ += _models
T = TypeVar('T') T = TypeVar('T')
T_co = TypeVar("T_co", covariant=True) T_co = TypeVar("T_co", covariant=True)
@ -80,3 +90,32 @@ class BannedStudentID(orm.Model):
student_id: str student_id: str
associated_account: Optional[int] associated_account: Optional[int]
banned_at_timestamp: float banned_at_timestamp: float
class Assignments(orm.Model):
registry = registry
fields = {
"entry_id": orm.Integer(primary_key=True, default=None),
"created_by": orm.ForeignKey(Student, allow_null=True),
"title": orm.String(min_length=2, max_length=2000),
"classroom": orm.URL(allow_null=True, default=None, max_length=4096),
"shared_doc": orm.URL(allow_null=True, default=None, max_length=4096),
"created_at": orm.Float(default=lambda: datetime.datetime.now().timestamp()),
"due_by": orm.Float(),
"tutor": orm.Enum(Tutors),
"reminders": orm.JSON(default=[]),
"finished": orm.Boolean(default=False),
"submitted": orm.Boolean(default=False)
}
if TYPE_CHECKING:
entry_id: int
created_by: Student
title: str
classroom: Optional[str]
shared_doc: Optional[str]
created_at: float
due_by: float
tutor: Tutors
reminders: list[str]
finished: bool
submitted: bool