Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 5 additions & 5 deletions controllers/endpoints/courses.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,13 +428,13 @@ def add_users(course_id):
new_role = add_form.role.data
# TODO: Update this to reflect new enums
if new_role in ('student', 'learner') and not new_user.is_student(course_id):
new_user.add_role('learner', course_id=course_id)
new_user.add_role('learner', course_id=course_id, authorizer_id=g.user.id)
newly_added.append(new_user)
elif new_role == 'teachingassistant' and not new_user.is_grader(course_id):
new_user.add_role('teachingassistant', course_id=course_id)
new_user.add_role('teachingassistant', course_id=course_id, authorizer_id=g.user.id)
newly_added.append(new_user)
elif new_role == 'instructor' and not new_user.is_instructor(course_id):
new_user.add_role('instructor', course_id=course_id)
new_user.add_role('instructor', course_id=course_id, authorizer_id=g.user.id)
newly_added.append(new_user)
# TODO: Add an invite for the course and that user
# TODO: Send an email to reset their password
Expand All @@ -455,7 +455,7 @@ def remove_role(role_id):
is_instructor = g.user.is_instructor(course_id)
if not is_instructor:
return "You're not an instructor in this course!"
Role.remove(int(role_id))
Role.remove(int(role_id), authorizer_id=g.user.id)
return redirect(url_for('courses.manage_users', course_id=course_id))


Expand All @@ -475,7 +475,7 @@ def change_role():
is_instructor = g.user.is_instructor(course_id)
if not is_instructor:
return "You're not an instructor in this course!"
role.update_role(new_role)
role.update_role(new_role, authorizer_id=g.user.id)
return ajax_success({"new_role": new_role})


Expand Down
16 changes: 15 additions & 1 deletion models/assignment.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from common.databases import optional_encoded_field, make_copy, get_enum_values
from common.maybe import maybe_int
from common.text import make_flavored_uuid_generator
from models.enums import AssignmentStatus, AssignmentTypes
from models.enums import AssignmentStatus, AssignmentTypes, AssignmentLogEvent
from models.generics.definitions import LatePolicy
from models.generics.models import db, ma
from models.generics.base import EnhancedBase, Base
Expand Down Expand Up @@ -223,6 +223,10 @@ def new(owner_id, course_id, type="blockpy", name=None, level=None, url=None) ->
type=type, name=level if type == 'maze' else name)
db.session.add(assignment)
db.session.commit()
# Log the assignment creation
models.AssignmentLog.new(assignment.id, assignment.version, assignment.course_id,
owner_id, AssignmentLogEvent.CREATE, "assignment", "",
client_timestamp="", client_timezone="")
return assignment

def move_course(self, new_course_id: int):
Expand All @@ -234,6 +238,12 @@ def move_course(self, new_course_id: int):
@staticmethod
def remove(assignment_id: int):
""" Delete the assignment with the given ID. """
# Log the assignment deletion before removing it
assignment = Assignment.query.get(assignment_id)
if assignment:
models.AssignmentLog.new(assignment.id, assignment.version, assignment.course_id,
assignment.owner_id, AssignmentLogEvent.DELETE, "assignment", "",
client_timestamp="", client_timezone="")
# TODO: Clear anyone's Fork that is me
# TODO: Clear assignment tag membership
# TODO: Clear submission sample
Expand Down Expand Up @@ -391,6 +401,10 @@ def fork(self, new_owner_id: int, new_course_id: int, new_name=None, new_url=Non
# TODO: Copy tags, sample_submissions, submissions, files
db.session.add(assignment)
db.session.commit()
# Log the fork event for the new assignment
models.AssignmentLog.new(assignment.id, assignment.version, assignment.course_id,
new_owner_id, AssignmentLogEvent.FORK, "forked_id", str(self.id),
client_timestamp="", client_timezone="")
return assignment

def is_allowed(self, ip: str) -> bool:
Expand Down
35 changes: 28 additions & 7 deletions models/course.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import models
from common.maybe import maybe_int
from common.text import make_flavored_uuid_generator
from models.enums import CourseVisibility, CourseKind, CourseService
from models.enums import CourseVisibility, CourseKind, CourseService, CourseLogEvent, RoleLogEvent
from models.generics.models import db, ma
from models.generics.base import EnhancedBase, Base, VersionedBase
from common.dates import datetime_to_string, string_to_datetime
Expand Down Expand Up @@ -120,6 +120,11 @@ def get_public():
@staticmethod
def remove(course_id, remove_linked=False):
course_id = maybe_int(course_id)
# Get course info before deleting
course = Course.query.get(course_id)
if course:
# Log the course deletion
models.CourseLog.new(course_id, course.owner_id, CourseLogEvent.DELETE)
Course.query.filter_by(id=course_id).delete()
if remove_linked:
for m in models.AssignmentGroupMembership.by_course(course_id):
Expand Down Expand Up @@ -291,22 +296,33 @@ def rename(course_id, name=None):

def edit(self, name=None, url=None, visibility=None, term=None, settings=None):
modified = False
if name is not None:
changes = {}
if name is not None and self.name != name:
changes['name'] = name
self.name = name
modified = True
if url is not None:
if url is not None and self.url != url:
changes['url'] = url
self.url = url
modified = True
if visibility is not None:
if visibility is not None and self.visibility != visibility:
changes['visibility'] = visibility
self.visibility = visibility
modified = True
if term is not None:
if term is not None and self.term != term:
changes['term'] = term
self.term = term
modified = True
if settings is not None:
if settings is not None and self.settings != settings:
changes['settings'] = settings
self.settings = settings
modified = True
db.session.commit()
# Log each field change
if modified:
for field, value in changes.items():
models.CourseLog.new(self.id, self.owner_id, CourseLogEvent.EDIT,
field=field, value=str(value))
return modified

@staticmethod
Expand All @@ -321,10 +337,15 @@ def new(name, owner_id, visibility, term, url):
return None
new_course = Course(name=name, owner_id=owner_id, visibility=visibility, term=term, url=url)
db.session.add(new_course)
db.session.flush()
db.session.flush() # Ensure the course gets an ID before creating the role
new_role = models.Role(name='instructor', user_id=owner_id, course_id=new_course.id)
db.session.add(new_role)
db.session.commit()
# Log the course creation
models.CourseLog.new(new_course.id, owner_id, CourseLogEvent.CREATE)
# Log the initial instructor role creation
models.RoleLog.new(new_role.id, new_course.id, owner_id, owner_id,
RoleLogEvent.GIVEN, new_role.name)
return new_course

@staticmethod
Expand Down
27 changes: 22 additions & 5 deletions models/role.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import Column, String, Integer, ForeignKey, Enum, Index

import models
from common.maybe import maybe_int
from common.databases import get_enum_values
from models.enums import UserRoles
from models.enums import UserRoles, RoleLogEvent
from models.generics.models import db, ma
from models.generics.base import Base

Expand Down Expand Up @@ -44,20 +45,36 @@ def encode_json(self, use_owner=True):
'course_id': self.course_id
}

def update_role(self, new_role):
def update_role(self, new_role, authorizer_id=None):
if new_role in [id for id, name in self.CHOICES]:
old_role = self.name
self.name = new_role
db.session.commit()
# Log the role change
# If no authorizer specified, assume the user is changing their own role
if authorizer_id is None:
authorizer_id = self.user_id
models.RoleLog.new(self.id, self.course_id, self.user_id, authorizer_id,
RoleLogEvent.CHANGED, f"{old_role} -> {new_role}")
return new_role
return None

def __str__(self):
return '<User {} is {}>'.format(self.user_id, self.name)

@staticmethod
def remove(role_id):
Role.query.filter_by(id=role_id).delete()
db.session.commit()
def remove(role_id, authorizer_id=None):
role = Role.query.get(role_id)
if role:
# If no authorizer specified, assume the user is removing their own role
if authorizer_id is None:
authorizer_id = role.user_id
# Log the role removal
models.RoleLog.new(role.id, role.course_id, role.user_id, authorizer_id,
RoleLogEvent.REMOVED, role.name)
# Delete the role
Role.query.filter_by(id=role_id).delete()
db.session.commit()

@staticmethod
def by_course(course_id):
Expand Down
27 changes: 24 additions & 3 deletions models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

import models
from common.maybe import maybe_int
from models.enums import RolePermissions, USER_DISPLAY_ROLES, UserRoles
from models.enums import RolePermissions, USER_DISPLAY_ROLES, UserRoles, RoleLogEvent
from models.generics.models import db, ma
from models.generics.base import Base

Expand Down Expand Up @@ -233,26 +233,47 @@ def is_test_user(self, course_id=None):

### Adding and updating roles ###

def add_role(self, name, course_id):
def add_role(self, name, course_id, authorizer_id=None):
if name in [id for id, _ in USER_DISPLAY_ROLES.items()]:
new_role = models.Role(name=name, user_id=self.id,
course_id=maybe_int(course_id))
db.session.add(new_role)
db.session.commit()
# Log the role creation
# If no authorizer specified, assume self-authorization
if authorizer_id is None:
authorizer_id = self.id
models.RoleLog.new(new_role.id, maybe_int(course_id), self.id, authorizer_id,
RoleLogEvent.GIVEN, name)
return new_role
return None

def update_roles(self, new_roles, course_id):
def update_roles(self, new_roles, course_id, authorizer_id=None):
# If no authorizer specified, assume self-authorization
if authorizer_id is None:
authorizer_id = self.id
old_roles = [role for role in self.roles if role.course_id == maybe_int(course_id)]
new_role_names = set(new_role_name.lower() for new_role_name in new_roles)
for old_role in old_roles:
if old_role.name.lower() not in new_role_names:
# Log the role removal
models.RoleLog.new(old_role.id, maybe_int(course_id), self.id, authorizer_id,
RoleLogEvent.REMOVED, old_role.name)
models.Role.query.filter(models.Role.id == old_role.id).delete()
old_role_names = set(role.name.lower() for role in old_roles)
new_roles_to_add = []
for new_role_name in new_roles:
if new_role_name.lower() not in old_role_names:
new_role = models.Role(name=new_role_name.lower(), user_id=self.id, course_id=maybe_int(course_id))
db.session.add(new_role)
new_roles_to_add.append((new_role, new_role_name.lower()))
# Flush once to get IDs for all new roles
if new_roles_to_add:
db.session.flush()
# Log all new role creations
for new_role, role_name in new_roles_to_add:
models.RoleLog.new(new_role.id, maybe_int(course_id), self.id, authorizer_id,
RoleLogEvent.GIVEN, role_name)
db.session.commit()

def determine_role(self, assignments, submissions):
Expand Down