class UserCourse(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'user_course' # table columns user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False) course_id = db.Column(db.Integer, db.ForeignKey("course.id", ondelete="CASCADE"), nullable=False) group_id = db.Column(db.Integer, db.ForeignKey('group.id', ondelete="SET NULL"), nullable=True) course_role = db.Column(Enum(CourseRole), nullable=False, index=True) # relationships # user many-to-many course with association user_course user = db.relationship("User", foreign_keys=[user_id], back_populates="user_courses") course = db.relationship("Course", back_populates="user_courses") group = db.relationship("Group", back_populates="user_courses") # hybrid and other functions user_uuid = association_proxy('user', 'uuid') course_uuid = association_proxy('course', 'uuid') @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate user in course db.UniqueConstraint('course_id', 'user_id', name='_unique_user_and_course'), DefaultTableMixin.default_table_args )
class ComparisonCriterion(DefaultTableMixin, UUIDMixin, WriteTrackingMixin): __tablename__ = 'comparison_criterion' # table columns comparison_id = db.Column(db.Integer, db.ForeignKey('comparison.id', ondelete="CASCADE"), nullable=False) criterion_id = db.Column(db.Integer, db.ForeignKey('criterion.id', ondelete="CASCADE"), nullable=False) winner = db.Column(Enum(WinningAnswer), nullable=True) content = db.Column(db.Text) # relationships # comparison via Comparison Model # criterion via Criterion Model # hybrid and other functions criterion_uuid = association_proxy('criterion', 'uuid') comparison_uuid = association_proxy('comparison', 'uuid') answer1_id = association_proxy('comparison', 'answer1_id') answer2_id = association_proxy('comparison', 'answer2_id') answer1_uuid = association_proxy('comparison', 'answer1_uuid') answer2_uuid = association_proxy('comparison', 'answer2_uuid') @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Criterion Unavailable" if not message: message = "Sorry, this criterion was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() def comparison_pair_winner(self): from . import WinningAnswer winner = None if self.winner == WinningAnswer.answer1: winner = ComparisonWinner.key1 elif self.winner == WinningAnswer.answer2: winner = ComparisonWinner.key2 return winner def convert_to_comparison_pair(self): return ComparisonPair(key1=self.answer1_id, key2=self.answer2_id, winner=self.comparison_pair_winner())
class AnswerComment(DefaultTableMixin, UUIDMixin, AttemptMixin, ActiveMixin, WriteTrackingMixin): __tablename__ = 'answer_comment' # table columns answer_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) content = db.Column(db.Text) comment_type = db.Column(Enum(AnswerCommentType), nullable=False, index=True) draft = db.Column(db.Boolean(), default=False, nullable=False, index=True) # relationships # answer via Answer Model # user via User Model #readonly # hybrid and other functionsx course_id = association_proxy('answer', 'course_id', creator=lambda course_id: import_module('compair.models.answer').Answer(course_id=course_id)) course_uuid = association_proxy('answer', 'course_uuid') assignment_id = association_proxy('answer', 'assignment_id') assignment_uuid = association_proxy('answer', 'assignment_uuid') answer_uuid = association_proxy('answer', 'uuid') user_avatar = association_proxy('user', 'avatar') user_uuid = association_proxy('user', 'uuid') user_displayname = association_proxy('user', 'displayname') user_student_number = association_proxy('user', 'student_number') user_fullname = association_proxy('user', 'fullname') user_fullname_sortable = association_proxy('user', 'fullname_sortable') user_system_role = association_proxy('user', 'system_role') @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Feedback Unavailable" if not message: message = "Sorry, this feedback was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Feedback Unavailable" if not message: message = "Sorry, this feedback was deleted or is no longer accessible." return super(cls, cls).get_active_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__()
class ComparisonExample(DefaultTableMixin, UUIDMixin, ActiveMixin, WriteTrackingMixin): __tablename__ = 'comparison_example' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) answer1_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) answer2_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) # relationships # assignment via Assignment Model comparisons = db.relationship("Comparison", backref="comparison_example", lazy="dynamic") answer1 = db.relationship("Answer", foreign_keys=[answer1_id]) answer2 = db.relationship("Answer", foreign_keys=[answer2_id]) # hyprid and other functions course_id = association_proxy('assignment', 'course_id', creator=lambda course_id: import_module('compair.models.assignment').Assignment(course_id=course_id)) course_uuid = association_proxy('assignment', 'course_uuid') assignment_uuid = association_proxy('assignment', 'uuid') answer1_uuid = association_proxy('answer1', 'uuid') answer2_uuid = association_proxy('answer2', 'uuid') @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Comparison Example Unavailable" if not message: message = "Sorry, these practice answers were deleted or are no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Comparison Example Unavailable" if not message: message = "Sorry, these practice answers were deleted or are no longer accessible." return super(cls, cls).get_active_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__()
class Criterion(DefaultTableMixin, UUIDMixin, ActiveMixin, WriteTrackingMixin): __tablename__ = 'criterion' # table columns user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) name = db.Column(db.String(255), nullable=False) description = db.Column(db.Text) public = db.Column(db.Boolean(), default=False, nullable=False, index=True) default = db.Column(db.Boolean(), default=True, nullable=False, index=True) # relationships # user via User Model # assignment many-to-many criterion with association assignment_criteria user_uuid = association_proxy('user', 'uuid') assignment_criteria = db.relationship("AssignmentCriterion", back_populates="criterion", lazy='dynamic') comparison_criteria = db.relationship("ComparisonCriterion", backref="criterion", lazy='dynamic') answer_criteria_scores = db.relationship("AnswerCriterionScore", backref="criterion", lazy='dynamic') # hybrid and other functions @hybrid_property def compared(self): return self.compare_count > 0 @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Criterion Unavailable" if not message: message = "Sorry, this criterion was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Criterion Unavailable" if not message: message = "Sorry, this criterion was deleted or is no longer accessible." return super(cls, cls).get_active_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() cls.compare_count = column_property( select([func.count(ComparisonCriterion.id)]). where(ComparisonCriterion.criterion_id == cls.id). scalar_subquery(), deferred=True, group="counts" )
class ActivityLog(DefaultTableMixin): __tablename__ = 'activity_log' # table columns user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="SET NULL"), nullable=True) course_id = db.Column(db.Integer, db.ForeignKey('course.id', ondelete="SET NULL"), nullable=True) timestamp = db.Column( db.TIMESTAMP, default=func.current_timestamp(), nullable=False ) event = db.Column(db.String(50)) data = db.Column(db.Text) status = db.Column(db.String(20)) message = db.Column(db.Text) session_id = db.Column(db.String(100))
class AssignmentCriterion(DefaultTableMixin, ActiveMixin, WriteTrackingMixin): __tablename__ = 'assignment_criterion' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey("assignment.id", ondelete="CASCADE"), nullable=False) criterion_id = db.Column(db.Integer, db.ForeignKey("criterion.id", ondelete="CASCADE"), nullable=False) position = db.Column(db.Integer) weight = db.Column(db.Integer, default=1, nullable=False) # relationships # assignment many-to-many criterion with association assignment_criteria assignment = db.relationship("Assignment", back_populates="assignment_criteria") criterion = db.relationship("Criterion", back_populates="assignment_criteria", lazy='immediate') # hyprid and other functions course_id = association_proxy( 'assignment', 'course_id', creator=lambda course_id: import_module( 'compair.models.assignment').Assignment(course_id=course_id)) course_uuid = association_proxy('assignment', 'course_uuid') assignment_uuid = association_proxy('assignment', 'uuid') criterion_uuid = association_proxy('criterion', 'uuid') __table_args__ = ( # prevent duplicate criteria in assignment db.UniqueConstraint('assignment_id', 'criterion_id', name='_unique_assignment_and_criterion'), DefaultTableMixin.default_table_args) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__()
class KalturaMedia(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'kaltura_media' # table columns user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) service_url = db.Column(db.String(255), nullable=False) partner_id = db.Column(db.Integer, default=0, nullable=False) player_id = db.Column(db.Integer, default=0, nullable=False) upload_ks = db.Column(db.String(255), nullable=False) upload_token_id = db.Column(db.String(255), nullable=False, index=True) file_name = db.Column(db.String(255), nullable=True) entry_id = db.Column(db.String(255), nullable=True) download_url = db.Column(db.String(255), nullable=True) # relationships # user via User Model files = db.relationship("File", backref="kaltura_media", lazy='dynamic') # hyprid and other functions @hybrid_property def extension(self): return self.file_name.lower().rsplit( '.', 1)[1] if '.' in self.file_name else None @hybrid_property def media_type(self): from compair.kaltura import KalturaCore if self.extension in KalturaCore.video_extensions(): return 1 elif self.extension in KalturaCore.audio_extensions(): return 5 return None @hybrid_property def show_recent_warning(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) # modified will be when the upload was completed warning_period = self.modified.replace( tzinfo=pytz.utc) + datetime.timedelta(minutes=5) return now < warning_period # hyprid and other functions @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__()
class LTINonce(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'lti_nonce' # table columns lti_consumer_id = db.Column(db.Integer, db.ForeignKey("lti_consumer.id", ondelete="CASCADE"), nullable=False) oauth_nonce = db.Column(db.String(191), nullable=False) oauth_timestamp = db.Column(db.TIMESTAMP, nullable=False) # relationships # lti_consumer via LTIConsumer Model # hybrid and other functions @classmethod def is_valid_nonce(cls, oauth_consumer_key, oauth_nonce, oauth_timestamp): from . import LTIConsumer lti_consumer = LTIConsumer.get_by_consumer_key(oauth_consumer_key) if lti_consumer == None: return False try: # is valid if it is unique on consumer, nonce, and timestamp # validate based on insert passing the unique check or not lti_nonce = LTINonce(lti_consumer_id=lti_consumer.id, oauth_nonce=oauth_nonce, oauth_timestamp=datetime.fromtimestamp( float(oauth_timestamp))) db.session.add(lti_nonce) db.session.commit() except exc.IntegrityError: db.session.rollback() return False return True @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate user in course db.UniqueConstraint('lti_consumer_id', 'oauth_nonce', 'oauth_timestamp', name='_unique_lti_consumer_nonce_and_timestamp'), DefaultTableMixin.default_table_args)
class ThirdPartyUser(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'third_party_user' # table columns third_party_type = db.Column(EnumType(ThirdPartyType, name="third_party_type"), nullable=False) unique_identifier = db.Column(db.String(255), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False) _params = db.Column(db.Text) # relationships # user via User Model # hyprid and other functions @property def params(self): return json.loads(self._params) if self._params else None @params.setter def params(self, params): self._params = json.dumps(params) if params else None @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate user in course db.UniqueConstraint( 'third_party_type', 'unique_identifier', name='_unique_third_party_type_and_unique_identifier'), DefaultTableMixin.default_table_args)
class LTIResourceLink(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'lti_resource_link' # table columns lti_consumer_id = db.Column(db.Integer, db.ForeignKey("lti_consumer.id", ondelete="CASCADE"), nullable=False) lti_context_id = db.Column(db.Integer, db.ForeignKey("lti_context.id", ondelete="CASCADE"), nullable=True) resource_link_id = db.Column(db.String(191), nullable=False) resource_link_title = db.Column(db.String(255), nullable=True) launch_presentation_return_url = db.Column(db.Text, nullable=True) custom_param_assignment_id = db.Column(db.String(255), nullable=True) compair_assignment_id = db.Column(db.Integer, db.ForeignKey("assignment.id", ondelete="CASCADE"), nullable=True) # relationships # compair_assignment via Assignment Model # lti_consumer via LTIConsumer Model # lti_context via LTIContext Model lti_user_resource_links = db.relationship("LTIUserResourceLink", backref="lti_resource_link", lazy="dynamic") # hybrid and other functions context_id = association_proxy('lti_context', 'context_id') compair_assignment_uuid = association_proxy('compair_assignment', 'uuid') def is_linked_to_assignment(self): return self.compair_assignment_id != None def _update_link_to_compair_assignment(self, lti_context): from compair.models import Assignment if self.custom_param_assignment_id and lti_context and lti_context.compair_course_id: # check if assignment exists assignment = Assignment.query \ .filter_by( uuid=self.custom_param_assignment_id, course_id=lti_context.compair_course_id, active=True ) \ .one_or_none() if assignment: self.compair_assignment = assignment return self self.compair_assignment = None return self @classmethod def get_by_lti_consumer_id_and_resource_link_id(cls, lti_consumer_id, resource_link_id): return LTIResourceLink.query \ .filter_by( lti_consumer_id=lti_consumer_id, resource_link_id=resource_link_id ) \ .one_or_none() @classmethod def get_by_tool_provider(cls, lti_consumer, tool_provider, lti_context=None): lti_resource_link = LTIResourceLink.get_by_lti_consumer_id_and_resource_link_id( lti_consumer.id, tool_provider.resource_link_id) if lti_resource_link == None: lti_resource_link = LTIResourceLink( lti_consumer_id=lti_consumer.id, resource_link_id=tool_provider.resource_link_id) db.session.add(lti_resource_link) lti_resource_link.lti_context_id = lti_context.id if lti_context else None lti_resource_link.resource_link_title = tool_provider.resource_link_title lti_resource_link.launch_presentation_return_url = tool_provider.launch_presentation_return_url lti_resource_link.custom_param_assignment_id = tool_provider.custom_assignment lti_resource_link._update_link_to_compair_assignment(lti_context) db.session.commit() return lti_resource_link @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate resource link in consumer db.UniqueConstraint('lti_consumer_id', 'resource_link_id', name='_unique_lti_consumer_and_lti_resource_link'), DefaultTableMixin.default_table_args)
class AssignmentComment(DefaultTableMixin, UUIDMixin, ActiveMixin, WriteTrackingMixin): __tablename__ = 'assignment_comment' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) content = db.Column(db.Text) # relationships # assignment via Assignment Model # user via User Model # hyprid and other functions course_id = association_proxy( 'assignment', 'course_id', creator=lambda course_id: import_module( 'compair.models.assignment').Assignment(course_id=course_id)) course_uuid = association_proxy('assignment', 'course_uuid') assignment_uuid = association_proxy('assignment', 'uuid') user_avatar = association_proxy('user', 'avatar') user_uuid = association_proxy('user', 'uuid') user_displayname = association_proxy('user', 'displayname') user_fullname = association_proxy('user', 'fullname') user_fullname_sortable = association_proxy('user', 'fullname_sortable') user_system_role = association_proxy('user', 'system_role') @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Help Comment Unavailable" if not message: message = "Sorry, this help comment was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Help Comment Unavailable" if not message: message = "Sorry, this help comment was deleted or is no longer accessible." return super(cls, cls).get_active_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__()
class Assignment(DefaultTableMixin, UUIDMixin, ActiveMixin, WriteTrackingMixin): __tablename__ = 'assignment' # table columns user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) course_id = db.Column(db.Integer, db.ForeignKey('course.id', ondelete="CASCADE"), nullable=False) file_id = db.Column(db.Integer, db.ForeignKey('file.id', ondelete="SET NULL"), nullable=True) name = db.Column(db.String(255)) description = db.Column(db.Text) answer_start = db.Column(db.DateTime(timezone=True)) answer_end = db.Column(db.DateTime(timezone=True)) compare_start = db.Column(db.DateTime(timezone=True), nullable=True) compare_end = db.Column(db.DateTime(timezone=True), nullable=True) self_eval_start = db.Column(db.DateTime(timezone=True), nullable=True) self_eval_end = db.Column(db.DateTime(timezone=True), nullable=True) self_eval_instructions = db.Column(db.Text, nullable=True) number_of_comparisons = db.Column(db.Integer, nullable=False) students_can_reply = db.Column(db.Boolean(), default=False, nullable=False) enable_self_evaluation = db.Column(db.Boolean(), default=False, nullable=False) enable_group_answers = db.Column(db.Boolean(), default=False, nullable=False) scoring_algorithm = db.Column(Enum(ScoringAlgorithm), nullable=True, default=ScoringAlgorithm.elo) pairing_algorithm = db.Column(Enum(PairingAlgorithm), nullable=True, default=PairingAlgorithm.random) rank_display_limit = db.Column(db.Integer, nullable=True) educators_can_compare = db.Column(db.Boolean(), default=False, nullable=False) answer_grade_weight = db.Column(db.Integer, default=1, nullable=False) comparison_grade_weight = db.Column(db.Integer, default=1, nullable=False) self_evaluation_grade_weight = db.Column(db.Integer, default=1, nullable=False) peer_feedback_prompt = db.Column(db.Text) # relationships # user via User Model # course via Course Model # file via File Model # assignment many-to-many criterion with association assignment_criteria assignment_criteria = db.relationship( "AssignmentCriterion", back_populates="assignment", order_by=AssignmentCriterion.position.asc(), collection_class=ordering_list('position', count_from=0)) answers = db.relationship("Answer", backref="assignment", lazy="dynamic", order_by=Answer.submission_date.desc()) comparisons = db.relationship("Comparison", backref="assignment", lazy="dynamic") comparison_examples = db.relationship("ComparisonExample", backref="assignment", lazy="dynamic") scores = db.relationship("AnswerScore", backref="assignment", lazy="dynamic") criteria_scores = db.relationship("AnswerCriterionScore", backref="assignment", lazy="dynamic") grades = db.relationship("AssignmentGrade", backref="assignment", lazy='dynamic') # lti lti_resource_links = db.relationship("LTIResourceLink", backref="compair_assignment", lazy='dynamic') # hybrid and other functions course_uuid = association_proxy('course', 'uuid') user_avatar = association_proxy('user', 'avatar') user_uuid = association_proxy('user', 'uuid') user_displayname = association_proxy('user', 'displayname') user_student_number = association_proxy('user', 'student_number') user_fullname = association_proxy('user', 'fullname') user_fullname_sortable = association_proxy('user', 'fullname_sortable') user_system_role = association_proxy('user', 'system_role') lti_course_linked = association_proxy('course', 'lti_linked') @hybrid_property def lti_linked(self): return self.lti_resource_link_count > 0 @hybrid_property def criteria(self): criteria = [] for assignment_criterion in self.assignment_criteria: if assignment_criterion.active and assignment_criterion.criterion.active: criterion = assignment_criterion.criterion criterion.weight = assignment_criterion.weight criteria.append(criterion) return criteria @hybrid_property def compared(self): return self.all_compare_count > 0 @hybrid_property def answered(self): return self.comparable_answer_count > 0 def completed_comparison_count_for_user(self, user_id): return self.comparisons \ .filter_by( user_id=user_id, completed=True ) \ .count() def draft_comparison_count_for_user(self, user_id): return self.comparisons \ .filter_by( user_id=user_id, draft=True ) \ .count() def clear_lti_links(self): for lti_resource_link in self.lti_resource_links.all(): lti_resource_link.compair_assignment_id = None @hybrid_property def available(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) answer_start = self.answer_start.replace(tzinfo=pytz.utc) return answer_start <= now @hybrid_property def answer_period(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) answer_start = self.answer_start.replace(tzinfo=pytz.utc) answer_end = self.answer_end.replace(tzinfo=pytz.utc) return answer_start <= now < answer_end @hybrid_property def answer_grace(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) grace = self.answer_end.replace(tzinfo=pytz.utc) + datetime.timedelta( seconds=60) # add 60 seconds answer_start = self.answer_start.replace(tzinfo=pytz.utc) return answer_start <= now < grace @hybrid_property def compare_period(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) answer_end = self.answer_end.replace(tzinfo=pytz.utc) if not self.compare_start: return now >= answer_end else: return self.compare_start.replace( tzinfo=pytz.utc) <= now < self.compare_end.replace( tzinfo=pytz.utc) @hybrid_property def compare_grace(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) if self.compare_start and self.compare_end: grace = self.compare_end.replace( tzinfo=pytz.utc) + datetime.timedelta( seconds=60) # add 60 seconds compare_start = self.compare_start.replace(tzinfo=pytz.utc) return compare_start <= now < grace else: answer_end = self.answer_end.replace(tzinfo=pytz.utc) return now >= answer_end @hybrid_property def after_comparing(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) answer_end = self.answer_end.replace(tzinfo=pytz.utc) # compare period not set if not self.compare_start: return now >= answer_end # compare period is set else: return now >= self.compare_end.replace(tzinfo=pytz.utc) @hybrid_property def self_eval_period(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) if not self.enable_self_evaluation: return False elif self.self_eval_start: return self.self_eval_start.replace( tzinfo=pytz.utc) <= now < self.self_eval_end.replace( tzinfo=pytz.utc) else: if self.compare_start: return now >= self.compare_start.replace(tzinfo=pytz.utc) else: return now >= self.answer_end.replace(tzinfo=pytz.utc) @hybrid_property def self_eval_grace(self): now = dateutil.parser.parse( datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat()) if not self.enable_self_evaluation: return False elif self.self_eval_start: grace = self.self_eval_end.replace( tzinfo=pytz.utc) + datetime.timedelta( seconds=60) # add 60 seconds return self.self_eval_start.replace(tzinfo=pytz.utc) <= now < grace else: if self.compare_start: return now >= self.compare_start.replace(tzinfo=pytz.utc) else: return now >= self.answer_end.replace(tzinfo=pytz.utc) @hybrid_property def evaluation_count(self): return self.compare_count + self.self_evaluation_count @hybrid_property def total_comparisons_required(self): return self.number_of_comparisons + self.comparison_example_count @hybrid_property def total_steps_required(self): return self.total_comparisons_required + ( 1 if self.enable_self_evaluation else 0) def calculate_grade(self, user): from . import AssignmentGrade AssignmentGrade.calculate_grade(self, user) def calculate_group_grade(self, group): from . import AssignmentGrade AssignmentGrade.calculate_group_grade(self, group) def calculate_grades(self): from . import AssignmentGrade AssignmentGrade.calculate_grades(self) @classmethod def validate_periods(cls, course_start, course_end, answer_start, answer_end, compare_start, compare_end, self_eval_start, self_eval_end): # validate answer period if answer_start == None: return (False, "No answer period start time provided.") elif answer_end == None: return (False, "No answer period end time provided.") course_start = course_start.replace( tzinfo=pytz.utc) if course_start else None course_end = course_end.replace( tzinfo=pytz.utc) if course_end else None answer_start = answer_start.replace(tzinfo=pytz.utc) answer_end = answer_end.replace(tzinfo=pytz.utc) # course start <= answer start < answer end <= course end if course_start and course_start > answer_start: return ( False, "Answer period start time must be after the course start time." ) elif answer_start >= answer_end: return ( False, "Answer period end time must be after the answer start time.") elif course_end and course_end < answer_end: return ( False, "Answer period end time must be before the course end time.") # validate compare period if compare_start == None and compare_end != None: return (False, "No compare period start time provided.") elif compare_start != None and compare_end == None: return (False, "No compare period end time provided.") elif compare_start != None and compare_end != None: compare_start = compare_start.replace(tzinfo=pytz.utc) compare_end = compare_end.replace(tzinfo=pytz.utc) # answer start < compare start < compare end <= course end if answer_start > compare_start: return ( False, "Compare period start time must be after the answer start time." ) elif compare_start > compare_end: return ( False, "Compare period end time must be after the compare start time." ) elif course_end and course_end < compare_end: return ( False, "Compare period end time must be before the course end time." ) # validate self-eval period if self_eval_start == None and self_eval_end != None: return (False, "No self-evaluation start time provided.") elif self_eval_start != None and self_eval_end == None: return (False, "No self-evaluation end time provided.") elif self_eval_start != None and self_eval_end != None: self_eval_start = self_eval_start.replace(tzinfo=pytz.utc) self_eval_end = self_eval_end.replace(tzinfo=pytz.utc) # self_eval start < self_eval end <= course end if self_eval_start > self_eval_end: return ( False, "Self-evaluation end time must be after the self-evaluation start time." ) elif course_end and course_end < self_eval_end: return ( False, "Self-evaluation end time must be before the course end time." ) # if comparison period defined: compare start < self_eval start if compare_start != None and compare_start > self_eval_start: return ( False, "Self-evaluation start time must be after the compare start time." ) # else: answer end < self_eval start # elif compare_start == None and answer_end >= self_eval_start: # return (False, "Self-evaluation start time must be after the answer end time.") return (True, None) @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Assignment Unavailable" if not message: message = "Sorry, this assignment was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Assignment Unavailable" if not message: message = "Sorry, this assignment was deleted or is no longer accessible." return super(cls, cls).get_active_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): from . import UserCourse, CourseRole, LTIResourceLink, Group super(cls, cls).__declare_last__() cls.answer_count = column_property(select([ func.count(Answer.id) ]).select_from( join(Answer, UserCourse, UserCourse.user_id == Answer.user_id, isouter=True).join( Group, Group.id == Answer.group_id, isouter=True)).where( and_( Answer.assignment_id == cls.id, Answer.active == True, Answer.draft == False, Answer.practice == False, or_( and_( UserCourse.course_id == cls.course_id, UserCourse.course_role != CourseRole.dropped, UserCourse.id != None), and_(Group.course_id == cls.course_id, Group.active == True, Group.id != None), ))).scalar_subquery(), deferred=True, group="counts") cls.student_answer_count = column_property(select([ func.count(Answer.id) ]).select_from( join(Answer, UserCourse, UserCourse.user_id == Answer.user_id, isouter=True).join( Group, Group.id == Answer.group_id, isouter=True)).where( and_( Answer.assignment_id == cls.id, Answer.active == True, Answer.draft == False, Answer.practice == False, or_( and_( UserCourse.course_id == cls.course_id, UserCourse.course_role == CourseRole.student, UserCourse.id != None), and_(Group.course_id == cls.course_id, Group.active == True, Group.id != None), ))).scalar_subquery(), deferred=True, group="counts") # Comparable answer count # To be consistent with student_answer_count, we are not counting # answers from sys admin here cls.comparable_answer_count = column_property(select([ func.count(Answer.id) ]).select_from( join(Answer, UserCourse, UserCourse.user_id == Answer.user_id, isouter=True).join( Group, Group.id == Answer.group_id, isouter=True)).where( and_( Answer.assignment_id == cls.id, Answer.active == True, Answer.draft == False, Answer.practice == False, Answer.comparable == True, or_( and_( UserCourse.course_id == cls.course_id, UserCourse.course_role != CourseRole.dropped, UserCourse.id != None), and_(Group.course_id == cls.course_id, Group.active == True, Group.id != None), ))).scalar_subquery(), deferred=True, group="counts") cls.comparison_example_count = column_property(select( [func.count(ComparisonExample.id)]).where( and_(ComparisonExample.assignment_id == cls.id, ComparisonExample.active == True)).scalar_subquery(), deferred=True, group="counts") cls.all_compare_count = column_property(select([ func.count(Comparison.id) ]).where(and_(Comparison.assignment_id == cls.id)).scalar_subquery(), deferred=True, group="counts") cls.compare_count = column_property(select([func.count( Comparison.id)]).where( and_(Comparison.assignment_id == cls.id, Comparison.completed == True)).scalar_subquery(), deferred=True, group="counts") cls.self_evaluation_count = column_property(select([ func.count(AnswerComment.id) ]).select_from( join(AnswerComment, Answer, AnswerComment.answer_id == Answer.id)).where( and_( AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.active == True, AnswerComment.answer_id == Answer.id, AnswerComment.draft == False, Answer.assignment_id == cls.id)).scalar_subquery(), deferred=True, group="counts") cls.lti_resource_link_count = column_property(select([ func.count(LTIResourceLink.id) ]).where( LTIResourceLink.compair_assignment_id == cls.id).scalar_subquery(), deferred=True, group="counts")
class Group(DefaultTableMixin, UUIDMixin, ActiveMixin, WriteTrackingMixin): # table columns course_id = db.Column(db.Integer, db.ForeignKey("course.id", ondelete="CASCADE"), nullable=False) name = db.Column(db.String(255), nullable=True) # relationships # course though Course Model user_courses = db.relationship("UserCourse", back_populates="group", lazy="dynamic") answers = db.relationship("Answer", backref="group") # hybrid and other functions course_uuid = association_proxy('course', 'uuid') group_uuid = association_proxy('group', 'uuid') @hybrid_property def avatar(self): """ According to gravatar's hash specs 1.Trim leading and trailing whitespace from an email address 2.Force all characters to lower-case 3.md5 hash the final string """ hash_input = self.uuid + ".group.@compair" if self.uuid else None m = hashlib.md5() m.update(hash_input.strip().lower().encode('utf-8')) return m.hexdigest() @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Group Unavailable" if not message: message = "Sorry, this group was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Group Unavailable" if not message: message = "Sorry, this group was deleted or is no longer accessible." return super(cls, cls).get_active_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = (db.UniqueConstraint('course_id', 'name', name='uq_course_and_group_name'), DefaultTableMixin.default_table_args)
class LTIUserResourceLink(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'lti_user_resource_link' # table columns lti_resource_link_id = db.Column(db.Integer, db.ForeignKey("lti_resource_link.id", ondelete="CASCADE"), nullable=False) lti_user_id = db.Column(db.Integer, db.ForeignKey("lti_user.id", ondelete="CASCADE"), nullable=False) roles = db.Column(db.String(255), nullable=True) lis_result_sourcedid = db.Column(db.String(255), nullable=True) course_role = db.Column(EnumType(CourseRole), nullable=False) # relationships # lti_user via LTIUser Model # lti_resource_link via LTIResourceLink Model # hybrid and other functions context_id = association_proxy('lti_resource_link', 'context_id') resource_link_id = association_proxy('lti_resource_link', 'resource_link_id') user_id = association_proxy('lti_user', 'user_id') compair_user_id = association_proxy('lti_user', 'compair_user_id') @classmethod def get_by_lti_resource_link_id_and_lti_user_id(cls, lti_resource_link_id, lti_user_id): return LTIUserResourceLink.query \ .filter_by( lti_resource_link_id=lti_resource_link_id, lti_user_id=lti_user_id ) \ .one_or_none() @classmethod def get_by_tool_provider(cls, lti_resource_link, lti_user, tool_provider): from . import CourseRole lti_user_resource_link = LTIUserResourceLink.get_by_lti_resource_link_id_and_lti_user_id( lti_resource_link.id, lti_user.id) if lti_user_resource_link == None: lti_user_resource_link = LTIUserResourceLink( lti_resource_link_id=lti_resource_link.id, lti_user_id=lti_user.id ) db.session.add(lti_user_resource_link) lti_user_resource_link.roles = text_type(tool_provider.roles) lti_user_resource_link.lis_result_sourcedid = tool_provider.lis_result_sourcedid # set course role every time if tool_provider.roles and any( role.lower().find("instructor") >= 0 or role.lower().find("faculty") >= 0 or role.lower().find("staff") >= 0 for role in tool_provider.roles ): lti_user_resource_link.course_role = CourseRole.instructor elif tool_provider.roles and any(role.lower().find("teachingassistant") >= 0 for role in tool_provider.roles): lti_user_resource_link.course_role = CourseRole.teaching_assistant else: lti_user_resource_link.course_role = CourseRole.student db.session.commit() return lti_user_resource_link @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate resource link in consumer db.UniqueConstraint('lti_resource_link_id', 'lti_user_id', name='_unique_lti_resource_link_and_lti_user'), DefaultTableMixin.default_table_args )
class Comparison(DefaultTableMixin, UUIDMixin, WriteTrackingMixin): __tablename__ = 'comparison' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) answer1_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) answer2_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) winner = db.Column(EnumType(WinningAnswer, name="winner"), nullable=True) comparison_example_id = db.Column(db.Integer, db.ForeignKey('comparison_example.id', ondelete="SET NULL"), nullable=True) round_compared = db.Column(db.Integer, default=0, nullable=False) completed = db.Column(db.Boolean(name='completed'), default=False, nullable=False, index=True) pairing_algorithm = db.Column(EnumType(PairingAlgorithm, name="pairing_algorithm"), nullable=True, default=PairingAlgorithm.random) # relationships # assignment via Assignment Model # user via User Model # comparison_example via ComparisonExample Model comparison_criteria = db.relationship("ComparisonCriterion", backref="comparison", lazy='immediate') answer1 = db.relationship("Answer", foreign_keys=[answer1_id]) answer2 = db.relationship("Answer", foreign_keys=[answer2_id]) # hyprid and other functions course_id = association_proxy( 'assignment', 'course_id', creator=lambda course_id: import_module( 'compair.models.assignment').Assignment(course_id=course_id)) course_uuid = association_proxy('assignment', 'course_uuid') assignment_uuid = association_proxy('assignment', 'uuid') answer1_uuid = association_proxy('answer1', 'uuid') answer2_uuid = association_proxy('answer2', 'uuid') user_avatar = association_proxy('user', 'avatar') user_uuid = association_proxy('user', 'uuid') user_displayname = association_proxy('user', 'displayname') user_fullname = association_proxy('user', 'fullname') user_fullname_sortable = association_proxy('user', 'fullname_sortable') user_system_role = association_proxy('user', 'system_role') @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Comparison Unavailable" if not message: message = "Sorry, this comparison was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() def comparison_pair_winner(self): from . import WinningAnswer winner = None if self.winner == WinningAnswer.answer1: winner = ComparisonWinner.key1 elif self.winner == WinningAnswer.answer2: winner = ComparisonWinner.key2 elif self.winner == WinningAnswer.draw: winner = ComparisonWinner.draw return winner def convert_to_comparison_pair(self): return ComparisonPair(key1=self.answer1_id, key2=self.answer2_id, winner=self.comparison_pair_winner()) @classmethod def _get_new_comparison_pair(cls, course_id, assignment_id, user_id, pairing_algorithm, comparisons): from . import Assignment, UserCourse, CourseRole, Answer, AnswerScore, PairingAlgorithm # ineligible authors - eg. instructors, TAs, dropped student, current user non_students = UserCourse.query \ .filter(and_( UserCourse.course_id == course_id, UserCourse.course_role != CourseRole.student )) ineligible_user_ids = [non_student.user_id \ for non_student in non_students] ineligible_user_ids.append(user_id) answers_with_score = Answer.query \ .with_entities(Answer, AnswerScore.score ) \ .outerjoin(AnswerScore) \ .filter(and_( Answer.user_id.notin_(ineligible_user_ids), Answer.assignment_id == assignment_id, Answer.active == True, Answer.practice == False, Answer.draft == False )) \ .all() scored_objects = [] for answer_with_score in answers_with_score: scored_objects.append( ScoredObject(key=answer_with_score.Answer.id, score=answer_with_score.score, rounds=answer_with_score.Answer.round, variable1=None, variable2=None, wins=None, loses=None, opponents=None)) comparison_pairs = [ comparison.convert_to_comparison_pair() for comparison in comparisons ] comparison_pair = generate_pair(package_name=pairing_algorithm.value, scored_objects=scored_objects, comparison_pairs=comparison_pairs, log=current_app.logger) return comparison_pair @classmethod def create_new_comparison(cls, assignment_id, user_id, skip_comparison_examples): from . import Assignment, ComparisonExample, ComparisonCriterion # get all comparisons for the user comparisons = Comparison.query \ .filter_by( user_id=user_id, assignment_id=assignment_id ) \ .all() is_comparison_example_set = False answer1 = None answer2 = None comparison_example_id = None round_compared = 0 assignment = Assignment.query.get(assignment_id) pairing_algorithm = assignment.pairing_algorithm if pairing_algorithm == None: pairing_algorithm = PairingAlgorithm.random if not skip_comparison_examples: # check comparison examples first comparison_examples = ComparisonExample.query \ .filter_by( assignment_id=assignment_id, active=True ) \ .all() # check if user has not completed all comparison examples for comparison_example in comparison_examples: comparison = next( (c for c in comparisons if c.comparison_example_id == comparison_example.id), None) if comparison == None: is_comparison_example_set = True answer1 = comparison_example.answer1 answer2 = comparison_example.answer2 comparison_example_id = comparison_example.id break if not is_comparison_example_set: comparison_pair = Comparison._get_new_comparison_pair( assignment.course_id, assignment_id, user_id, pairing_algorithm, comparisons) answer1 = Answer.query.get(comparison_pair.key1) answer2 = Answer.query.get(comparison_pair.key2) round_compared = min(answer1.round + 1, answer2.round + 1) # update round counters answers = [answer1, answer2] for answer in answers: answer.round += 1 db.session.add(answer) comparison = Comparison(assignment_id=assignment_id, user_id=user_id, answer1_id=answer1.id, answer2_id=answer2.id, winner=None, round_compared=round_compared, comparison_example_id=comparison_example_id, pairing_algorithm=pairing_algorithm) db.session.add(comparison) for criterion in assignment.criteria: comparison_criterion = ComparisonCriterion( comparison=comparison, criterion_id=criterion.id, winner=None, content=None, ) db.session.commit() return comparison @classmethod def update_scores_1vs1(cls, comparison): from . import AnswerScore, AnswerCriterionScore, \ ComparisonCriterion, ScoringAlgorithm assignment_id = comparison.assignment_id answer1_id = comparison.answer1_id answer2_id = comparison.answer2_id # get all other comparisons for the answers not including the ones being calculated other_comparisons = Comparison.query \ .options(load_only('winner', 'answer1_id', 'answer2_id')) \ .filter(and_( Comparison.assignment_id == assignment_id, Comparison.id != comparison.id, or_( Comparison.answer1_id.in_([answer1_id, answer2_id]), Comparison.answer2_id.in_([answer1_id, answer2_id]) ) )) \ .all() scores = AnswerScore.query \ .filter( AnswerScore.answer_id.in_([answer1_id, answer2_id]) ) \ .all() # get all other criterion comparisons for the answers not including the ones being calculated other_criterion_comparisons = ComparisonCriterion.query \ .join("comparison") \ .filter(and_( Comparison.assignment_id == assignment_id, ~Comparison.id == comparison.id, or_( Comparison.answer1_id.in_([answer1_id, answer2_id]), Comparison.answer2_id.in_([answer1_id, answer2_id]) ) )) \ .all() criteria_scores = AnswerCriterionScore.query \ .filter( AnswerCriterionScore.answer_id.in_([answer1_id, answer2_id]) ) \ .all() #update answer criterion scores updated_criteria_scores = [] for comparison_criterion in comparison.comparison_criteria: criterion_id = comparison_criterion.criterion_id score1 = next((criterion_score for criterion_score in criteria_scores if criterion_score.answer_id == answer1_id and criterion_score.criterion_id == criterion_id), AnswerCriterionScore(assignment_id=assignment_id, answer_id=answer1_id, criterion_id=criterion_id)) updated_criteria_scores.append(score1) key1_scored_object = score1.convert_to_scored_object( ) if score1 != None else ScoredObject( key=answer1_id, score=None, variable1=None, variable2=None, rounds=0, wins=0, opponents=0, loses=0, ) score2 = next((criterion_score for criterion_score in criteria_scores if criterion_score.answer_id == answer2_id and criterion_score.criterion_id == criterion_id), AnswerCriterionScore(assignment_id=assignment_id, answer_id=answer2_id, criterion_id=criterion_id)) updated_criteria_scores.append(score2) key2_scored_object = score2.convert_to_scored_object( ) if score2 != None else ScoredObject( key=answer2_id, score=None, variable1=None, variable2=None, rounds=0, wins=0, opponents=0, loses=0, ) result_1, result_2 = calculate_score_1vs1( package_name=ScoringAlgorithm.elo.value, key1_scored_object=key1_scored_object, key2_scored_object=key2_scored_object, winner=comparison_criterion.comparison_pair_winner(), other_comparison_pairs=[ c.convert_to_comparison_pair() for c in other_criterion_comparisons if c.criterion_id == criterion_id ], log=current_app.logger) for score, result in [(score1, result_1), (score2, result_2)]: score.score = result.score score.variable1 = result.variable1 score.variable2 = result.variable2 score.rounds = result.rounds score.wins = result.wins score.loses = result.loses score.opponents = result.opponents updated_scores = [] score1 = next( (score for score in scores if score.answer_id == answer1_id), AnswerScore(assignment_id=assignment_id, answer_id=answer1_id)) updated_scores.append(score1) key1_scored_object = score1.convert_to_scored_object( ) if score1 != None else ScoredObject( key=answer1_id, score=None, variable1=None, variable2=None, rounds=0, wins=0, opponents=0, loses=0, ) score2 = next( (score for score in scores if score.answer_id == answer2_id), AnswerScore(assignment_id=assignment_id, answer_id=answer2_id)) updated_scores.append(score2) key2_scored_object = score2.convert_to_scored_object( ) if score2 != None else ScoredObject( key=answer2_id, score=None, variable1=None, variable2=None, rounds=0, wins=0, opponents=0, loses=0, ) result_1, result_2 = calculate_score_1vs1( package_name=ScoringAlgorithm.elo.value, key1_scored_object=key1_scored_object, key2_scored_object=key2_scored_object, winner=comparison.comparison_pair_winner(), other_comparison_pairs=[ c.convert_to_comparison_pair() for c in other_comparisons ], log=current_app.logger) for score, result in [(score1, result_1), (score2, result_2)]: score.score = result.score score.variable1 = result.variable1 score.variable2 = result.variable2 score.rounds = result.rounds score.wins = result.wins score.loses = result.loses score.opponents = result.opponents db.session.add_all(updated_criteria_scores) db.session.add_all(updated_scores) db.session.commit() return updated_scores @classmethod def calculate_scores(cls, assignment_id): from . import AnswerScore, AnswerCriterionScore, \ AssignmentCriterion, ScoringAlgorithm # get all comparisons for this assignment and only load the data we need comparisons = Comparison.query \ .filter(Comparison.assignment_id == assignment_id) \ .all() assignment_criteria = AssignmentCriterion.query \ .with_entities(AssignmentCriterion.criterion_id) \ .filter_by(assignment_id=assignment_id, active=True) \ .all() comparison_criteria = [] comparison_pairs = [] answer_ids = set() for comparison in comparisons: answer_ids.add(comparison.answer1_id) answer_ids.add(comparison.answer2_id) comparison_criteria.extend(comparison.comparison_criteria) comparison_pairs.append(comparison.convert_to_comparison_pair()) # calculate answer score comparison_results = calculate_score( package_name=ScoringAlgorithm.elo.value, comparison_pairs=comparison_pairs, log=current_app.logger) scores = AnswerScore.query \ .filter(AnswerScore.answer_id.in_(answer_ids)) \ .all() updated_answer_scores = update_answer_scores(scores, assignment_id, comparison_results) db.session.add_all(updated_answer_scores) # calculate answer criterion scores criterion_comparison_results = {} for assignment_criterion in assignment_criteria: comparison_pairs = [] for comparison_criterion in comparison_criteria: if comparison_criterion.criterion_id != assignment_criterion.criterion_id: continue comparison_pairs.append( comparison_criterion.convert_to_comparison_pair()) criterion_comparison_results[ assignment_criterion.criterion_id] = calculate_score( package_name=ScoringAlgorithm.elo.value, comparison_pairs=comparison_pairs, log=current_app.logger) scores = AnswerCriterionScore.query \ .filter(AnswerCriterionScore.answer_id.in_(answer_ids)) \ .all() updated_answer_criteria_scores = update_answer_criteria_scores( scores, assignment_id, criterion_comparison_results) db.session.add_all(updated_answer_criteria_scores) db.session.commit()
def modified_user_id(cls): return db.Column(db.Integer, db.ForeignKey('user.id', ondelete="SET NULL"), nullable=True)
class LTIUser(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'lti_user' # table columns lti_consumer_id = db.Column(db.Integer, db.ForeignKey("lti_consumer.id", ondelete="CASCADE"), nullable=False) user_id = db.Column(db.String(255), nullable=False) lis_person_name_given = db.Column(db.String(255), nullable=True) lis_person_name_family = db.Column(db.String(255), nullable=True) lis_person_name_full = db.Column(db.String(255), nullable=True) lis_person_contact_email_primary = db.Column(db.String(255), nullable=True) compair_user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=True) system_role = db.Column(EnumType(SystemRole, name="system_role"), nullable=False) # relationships # user via User Model # lti_consumer via LTIConsumer Model lti_memberships = db.relationship("LTIMembership", backref="lti_user", lazy="dynamic") lti_user_resource_links = db.relationship("LTIUserResourceLink", backref="lti_user", lazy="dynamic") # hyprid and other functions def is_linked_to_user(self): return self.compair_user_id != None @classmethod def get_by_lti_consumer_id_and_user_id(cls, lti_consumer_id, user_id): return LTIUser.query \ .filter_by( lti_consumer_id=lti_consumer_id, user_id=user_id ) \ .one_or_none() @classmethod def get_by_tool_provider(cls, lti_consumer, tool_provider): from . import SystemRole if tool_provider.user_id == None: return None lti_user = LTIUser.get_by_lti_consumer_id_and_user_id( lti_consumer.id, tool_provider.user_id) if not lti_user: lti_user = LTIUser( lti_consumer_id=lti_consumer.id, user_id=tool_provider.user_id, system_role=SystemRole.instructor \ if tool_provider.is_instructor() \ else SystemRole.student ) db.session.add(lti_user) lti_user.lis_person_name_given = tool_provider.lis_person_name_given lti_user.lis_person_name_family = tool_provider.lis_person_name_family lti_user.lis_person_name_full = tool_provider.lis_person_name_full lti_user.lis_person_contact_email_primary = tool_provider.lis_person_contact_email_primary db.session.commit() return lti_user def upgrade_system_role(self): # upgrade system role is needed if self.is_linked_to_user(): if self.compair_user.system_role == SystemRole.student and self.system_role in [ SystemRole.instructor, SystemRole.sys_admin ]: self.compair_user.system_role = self.system_role elif self.compair_user.system_role == SystemRole.instructor and self.system_role == SystemRole.sys_admin: self.compair_user.system_role = self.system_role db.session.commit() @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate resource link in consumer db.UniqueConstraint('lti_consumer_id', 'user_id', name='_unique_lti_consumer_and_lti_user'), DefaultTableMixin.default_table_args)
class AssignmentGrade(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'assignment_grade' # table columns user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) grade = db.Column(db.Float, default=0, nullable=False) # relationships # user via User Model # assignment via Course Model # hybrid and other functions @classmethod def get_assignment_grades(cls, assignment): return AssignmentGrade.query \ .filter_by(assignment_id=assignment.id) \ .all() @classmethod def get_user_assignment_grade(cls, assignment, user): return AssignmentGrade.query \ .filter_by( user_id=user.id, assignment_id=assignment.id ) \ .one_or_none() @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate user in course db.UniqueConstraint('assignment_id', 'user_id', name='_unique_user_and_assignment'), DefaultTableMixin.default_table_args) @classmethod def calculate_grade(cls, assignment, user): from . import Answer, Comparison, CourseRole, \ AnswerComment, AnswerCommentType, LTIOutcome user_is_student = False group_id = None for course_user in assignment.course.user_courses: if course_user.user_id != user.id: continue user_is_student = course_user.course_role == CourseRole.student group_id = course_user.group_id break if not user_is_student: return user_answer_count = Answer.query \ .filter_by( assignment_id=assignment.id, user_id=user.id, active=True, practice=False, draft=False ) \ .count() group_answer_counts = 0 if group_id: group_answer_counts = Answer.query \ .filter_by( assignment_id=assignment.id, active=True, practice=False, draft=False, group_id=group_id ) \ .count() comparison_count = Comparison.query \ .filter_by( user_id=user.id, assignment_id=assignment.id, completed=True ) \ .count() self_evaluation_count = AnswerComment.query \ .join("answer") \ .filter(and_( AnswerComment.user_id == user.id, AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False )) \ .count() answer_count = user_answer_count + group_answer_counts grade = _calculate_assignment_grade(assignment, answer_count, comparison_count, self_evaluation_count) assignment_grade = AssignmentGrade.get_user_assignment_grade( assignment, user) if assignment_grade == None: assignment_grade = AssignmentGrade(user_id=user.id, assignment_id=assignment.id) assignment_grade.grade = grade db.session.add(assignment_grade) db.session.commit() LTIOutcome.update_assignment_user_grade(assignment, user.id) @classmethod def calculate_group_grade(cls, assignment, group): from . import Answer, Comparison, CourseRole, \ AnswerComment, AnswerCommentType, LTIOutcome student_ids = [course_user.user_id for course_user in assignment.course.user_courses if course_user.course_role == CourseRole.student and \ course_user.group_id == group.id] # skip if there aren't any students if len(student_ids) == 0: return user_answer_counts = Answer.query \ .with_entities( Answer.user_id, func.count(Answer.user_id).label('answer_count') ) \ .filter_by( assignment_id=assignment.id, active=True, practice=False, draft=False ) \ .filter( Answer.user_id.in_(student_ids) ) \ .group_by(Answer.user_id) \ .all() group_answer_counts = Answer.query \ .with_entities( Answer.group_id, func.count(Answer.group_id).label('answer_count') ) \ .filter_by( assignment_id=assignment.id, active=True, practice=False, draft=False, group_id=group.id ) \ .group_by(Answer.group_id) \ .all() comparison_counts = Comparison.query \ .with_entities( Comparison.user_id, func.count(Comparison.user_id).label('comparison_count') ) \ .filter_by( assignment_id=assignment.id, completed=True ) \ .filter( Comparison.user_id.in_(student_ids) ) \ .group_by(Comparison.user_id) \ .all() self_evaluation_counts = AnswerComment.query \ .with_entities( AnswerComment.user_id, func.count(AnswerComment.user_id).label('self_evaluation_count') ) \ .join("answer") \ .filter(and_( AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False, AnswerComment.user_id.in_(student_ids) )) \ .group_by(AnswerComment.user_id) \ .all() assignment_grades = AssignmentGrade.get_assignment_grades(assignment) new_assignment_grades = [] for student_id in student_ids: user_answer_count = next((result.answer_count for result in user_answer_counts if result.user_id == student_id), 0) group_answer_count = next((result.answer_count for result in group_answer_counts if result.group_id == group.id), 0) answer_count = user_answer_count + group_answer_count comparison_count = next((result.comparison_count for result in comparison_counts if result.user_id == student_id), 0) self_evaluation_count = next((result.self_evaluation_count for result in self_evaluation_counts if result.user_id == student_id), 0) grade = _calculate_assignment_grade(assignment, answer_count, comparison_count, self_evaluation_count) assignment_grade = next( (assignment_grade for assignment_grade in assignment_grades if assignment_grade.user_id == student_id), None) if assignment_grade == None: assignment_grade = AssignmentGrade(user_id=student_id, assignment_id=assignment.id) new_assignment_grades.append(assignment_grade) assignment_grade.grade = grade db.session.add_all(assignment_grades + new_assignment_grades) db.session.commit() LTIOutcome.update_assignment_users_grades(assignment, student_ids) @classmethod def calculate_grades(cls, assignment): from . import Answer, CourseRole, Comparison, \ AnswerComment, AnswerCommentType, LTIOutcome student_ids = [] group_ids = set() user_groups = {} for course_user in assignment.course.user_courses: if course_user.course_role == CourseRole.student: student_ids.append(course_user.user_id) if course_user.group_id: group_ids.add(course_user.group_id) user_groups[course_user.user_id] = course_user.group_id group_ids = list(group_ids) # skip if there aren't any students if len(student_ids) == 0: AssignmentGrade.query \ .filter_by(assignment_id=assignment.id) \ .delete() LTIOutcome.update_assignment_grades(assignment) return user_answer_counts = Answer.query \ .with_entities( Answer.user_id, func.count(Answer.user_id).label('answer_count') ) \ .filter_by( assignment_id=assignment.id, active=True, practice=False, draft=False ) \ .filter( Answer.user_id.in_(student_ids) ) \ .group_by(Answer.user_id) \ .all() group_answer_counts = [] if len(group_ids) > 0: group_answer_counts = Answer.query \ .with_entities( Answer.group_id, func.count(Answer.group_id).label('answer_count') ) \ .filter_by( assignment_id=assignment.id, active=True, practice=False, draft=False ) \ .filter( Answer.group_id.in_(group_ids) ) \ .group_by(Answer.group_id) \ .all() comparison_counts = Comparison.query \ .with_entities( Comparison.user_id, func.count(Comparison.user_id).label('comparison_count') ) \ .filter_by( assignment_id=assignment.id, completed=True ) \ .filter( Comparison.user_id.in_(student_ids) ) \ .group_by(Comparison.user_id) \ .all() self_evaluation_counts = AnswerComment.query \ .with_entities( AnswerComment.user_id, func.count(AnswerComment.user_id).label('self_evaluation_count') ) \ .join("answer") \ .filter(and_( AnswerComment.active == True, AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.draft == False, Answer.assignment_id == assignment.id, Answer.active == True, Answer.practice == False, Answer.draft == False, AnswerComment.user_id.in_(student_ids) )) \ .group_by(AnswerComment.user_id) \ .all() assignment_grades = AssignmentGrade.get_assignment_grades(assignment) new_assignment_grades = [] for student_id in student_ids: user_answer_count = next((result.answer_count for result in user_answer_counts if result.user_id == student_id), 0) group_id = user_groups.get(student_id) group_answer_count = 0 if group_id: group_answer_count = next((result.answer_count for result in group_answer_counts if result.group_id == group_id), 0) answer_count = user_answer_count + group_answer_count comparison_count = next((result.comparison_count for result in comparison_counts if result.user_id == student_id), 0) self_evaluation_count = next((result.self_evaluation_count for result in self_evaluation_counts if result.user_id == student_id), 0) grade = _calculate_assignment_grade(assignment, answer_count, comparison_count, self_evaluation_count) assignment_grade = next( (assignment_grade for assignment_grade in assignment_grades if assignment_grade.user_id == student_id), None) if assignment_grade == None: assignment_grade = AssignmentGrade(user_id=student_id, assignment_id=assignment.id) new_assignment_grades.append(assignment_grade) assignment_grade.grade = grade db.session.add_all(assignment_grades + new_assignment_grades) db.session.commit() LTIOutcome.update_assignment_grades(assignment)
class Comparison(DefaultTableMixin, UUIDMixin, AttemptMixin, WriteTrackingMixin): __tablename__ = 'comparison' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) answer1_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) answer2_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) winner = db.Column(EnumType(WinningAnswer), nullable=True) comparison_example_id = db.Column(db.Integer, db.ForeignKey('comparison_example.id', ondelete="SET NULL"), nullable=True) round_compared = db.Column(db.Integer, default=0, nullable=False) completed = db.Column(db.Boolean(), default=False, nullable=False, index=True) pairing_algorithm = db.Column(EnumType(PairingAlgorithm), nullable=True, default=PairingAlgorithm.random) # relationships # assignment via Assignment Model # user via User Model # comparison_example via ComparisonExample Model comparison_criteria = db.relationship("ComparisonCriterion", backref="comparison", lazy='immediate') answer1 = db.relationship("Answer", foreign_keys=[answer1_id]) answer2 = db.relationship("Answer", foreign_keys=[answer2_id]) # hybrid and other functions course_id = association_proxy( 'assignment', 'course_id', creator=lambda course_id: import_module( 'compair.models.assignment').Assignment(course_id=course_id)) course_uuid = association_proxy('assignment', 'course_uuid') assignment_uuid = association_proxy('assignment', 'uuid') answer1_uuid = association_proxy('answer1', 'uuid') answer2_uuid = association_proxy('answer2', 'uuid') user_avatar = association_proxy('user', 'avatar') user_uuid = association_proxy('user', 'uuid') user_displayname = association_proxy('user', 'displayname') user_student_number = association_proxy('user', 'student_number') user_fullname = association_proxy('user', 'fullname') user_fullname_sortable = association_proxy('user', 'fullname_sortable') user_system_role = association_proxy('user', 'system_role') @hybrid_property def draft(self): return self.modified != self.created and not self.completed @draft.expression def draft(cls): return and_(cls.modified != cls.created, cls.completed == False) @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Comparison Unavailable" if not message: message = "Sorry, this comparison was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() def comparison_pair_winner(self): from . import WinningAnswer winner = None if self.winner == WinningAnswer.answer1: winner = ComparisonWinner.key1 elif self.winner == WinningAnswer.answer2: winner = ComparisonWinner.key2 elif self.winner == WinningAnswer.draw: winner = ComparisonWinner.draw return winner def convert_to_comparison_pair(self): return ComparisonPair(key1=self.answer1_id, key2=self.answer2_id, winner=self.comparison_pair_winner()) @classmethod def _get_new_comparison_pair(cls, course_id, assignment_id, user_id, group_id, pairing_algorithm, comparisons): from . import Assignment, UserCourse, CourseRole, Answer, AnswerScore, \ PairingAlgorithm, AnswerCriterionScore, AssignmentCriterion, Group # exclude current user and those without a proper role. # note that sys admin (not enrolled in the course and thus no course role) can create answers. # they are considered eligible ineligibles = UserCourse.query \ .with_entities(UserCourse.user_id) \ .filter(and_( UserCourse.course_id == course_id, UserCourse.course_role == CourseRole.dropped )) \ .all() ineligible_user_ids = [ ineligible.user_id for ineligible in ineligibles ] ineligible_user_ids.append(user_id) query = Answer.query \ .with_entities(Answer, AnswerScore.score) \ .outerjoin(AnswerScore, AnswerScore.answer_id == Answer.id) \ .filter(and_( or_( ~Answer.user_id.in_(ineligible_user_ids), Answer.user_id == None # don't filter out group answers ), Answer.assignment_id == assignment_id, Answer.active == True, Answer.practice == False, Answer.draft == False, Answer.comparable == True )) if group_id: query = query.filter( or_(Answer.group_id != group_id, Answer.group_id == None)) answers_with_score = query.all() scored_objects = [] for answer_with_score in answers_with_score: scored_objects.append( ScoredObject(key=answer_with_score.Answer.id, score=answer_with_score.score, rounds=answer_with_score.Answer.round, variable1=None, variable2=None, wins=None, loses=None, opponents=None)) comparison_pairs = [ comparison.convert_to_comparison_pair() for comparison in comparisons ] # adaptive min delta algo requires extra criterion specific parameters if pairing_algorithm == PairingAlgorithm.adaptive_min_delta: # retrieve extra criterion score data answer_criterion_scores = AnswerCriterionScore.query \ .with_entities(AnswerCriterionScore.answer_id, AnswerCriterionScore.criterion_id, AnswerCriterionScore.score) \ .join(Answer) \ .filter(and_( Answer.user_id.notin_(ineligible_user_ids), Answer.assignment_id == assignment_id, Answer.active == True, Answer.practice == False, Answer.draft == False )) \ .all() assignment_criterion_weights = AssignmentCriterion.query \ .with_entities(AssignmentCriterion.criterion_id, AssignmentCriterion.weight) \ .filter(and_( AssignmentCriterion.assignment_id == assignment_id, AssignmentCriterion.active == True )) \ .all() criterion_scores = {} for criterion_score in answer_criterion_scores: scores = criterion_scores.setdefault(criterion_score.answer_id, {}) scores[criterion_score.criterion_id] = criterion_score.score criterion_weights = {} for the_weight in assignment_criterion_weights: criterion_weights[the_weight.criterion_id] = \ the_weight.weight comparison_pair = generate_pair( package_name=pairing_algorithm.value, scored_objects=scored_objects, comparison_pairs=comparison_pairs, criterion_scores=criterion_scores, criterion_weights=criterion_weights, log=current_app.logger) else: comparison_pair = generate_pair( package_name=pairing_algorithm.value, scored_objects=scored_objects, comparison_pairs=comparison_pairs, log=current_app.logger) return comparison_pair @classmethod def create_new_comparison(cls, assignment_id, user_id, skip_comparison_examples): from . import Assignment, ComparisonExample, ComparisonCriterion, \ UserCourse, CourseRole # get all comparisons for the user comparisons = Comparison.query \ .filter_by( user_id=user_id, assignment_id=assignment_id ) \ .all() is_comparison_example_set = False answer1 = None answer2 = None comparison_example_id = None round_compared = 0 assignment = Assignment.query.get(assignment_id) pairing_algorithm = assignment.pairing_algorithm if pairing_algorithm == None: pairing_algorithm = PairingAlgorithm.random # set user group restriction if # - enable_group_answers, user part of a group, and user is student group_id = None if assignment.enable_group_answers: user_course = UserCourse.query \ .filter_by( user_id=user_id, course_id=assignment.course_id, course_role=CourseRole.student ) \ .first() if user_course and user_course.group_id: group_id = user_course.group_id if not skip_comparison_examples: # check comparison examples first comparison_examples = ComparisonExample.query \ .filter_by( assignment_id=assignment_id, active=True ) \ .all() # check if user has not completed all comparison examples for comparison_example in comparison_examples: comparison = next( (c for c in comparisons if c.comparison_example_id == comparison_example.id), None) if comparison == None: is_comparison_example_set = True answer1 = comparison_example.answer1 answer2 = comparison_example.answer2 comparison_example_id = comparison_example.id break if not is_comparison_example_set: comparison_pair = Comparison._get_new_comparison_pair( assignment.course_id, assignment_id, user_id, group_id, pairing_algorithm, comparisons) answer1 = Answer.query.get(comparison_pair.key1) answer2 = Answer.query.get(comparison_pair.key2) round_compared = min(answer1.round + 1, answer2.round + 1) # update round counters answers = [answer1, answer2] for answer in answers: answer._write_tracking_enabled = False answer.round += 1 db.session.add(answer) comparison = Comparison(assignment_id=assignment_id, user_id=user_id, answer1_id=answer1.id, answer2_id=answer2.id, winner=None, round_compared=round_compared, comparison_example_id=comparison_example_id, pairing_algorithm=pairing_algorithm) db.session.add(comparison) for criterion in assignment.criteria: comparison_criterion = ComparisonCriterion( comparison=comparison, criterion_id=criterion.id, winner=None, content=None, ) db.session.add(comparison) db.session.commit() return comparison @classmethod def update_scores_1vs1(cls, comparison): from . import AnswerScore, AnswerCriterionScore, \ ComparisonCriterion, ScoringAlgorithm assignment = comparison.assignment answer1_id = comparison.answer1_id answer2_id = comparison.answer2_id # get all other comparisons for the answers not including the ones being calculated other_comparisons = Comparison.query \ .options(load_only('winner', 'answer1_id', 'answer2_id')) \ .filter(and_( Comparison.assignment_id == assignment.id, Comparison.id != comparison.id, or_( Comparison.answer1_id.in_([answer1_id, answer2_id]), Comparison.answer2_id.in_([answer1_id, answer2_id]) ) )) \ .all() scores = AnswerScore.query \ .filter( AnswerScore.answer_id.in_([answer1_id, answer2_id]) ) \ .all() # get all other criterion comparisons for the answers not including the ones being calculated other_criterion_comparisons = ComparisonCriterion.query \ .join("comparison") \ .filter(and_( Comparison.assignment_id == assignment.id, Comparison.id != comparison.id, or_( Comparison.answer1_id.in_([answer1_id, answer2_id]), Comparison.answer2_id.in_([answer1_id, answer2_id]) ) )) \ .all() criteria_scores = AnswerCriterionScore.query \ .filter(and_( AnswerCriterionScore.assignment_id == assignment.id, AnswerCriterionScore.answer_id.in_([answer1_id, answer2_id]) )) \ .all() #update answer criterion scores updated_criteria_scores = [] for comparison_criterion in comparison.comparison_criteria: criterion_id = comparison_criterion.criterion_id criterion_score1 = next( (criterion_score for criterion_score in criteria_scores if criterion_score.answer_id == answer1_id and criterion_score.criterion_id == criterion_id), AnswerCriterionScore(assignment_id=assignment.id, answer_id=answer1_id, criterion_id=criterion_id)) updated_criteria_scores.append(criterion_score1) key1_scored_object = criterion_score1.convert_to_scored_object() criterion_score2 = next( (criterion_score for criterion_score in criteria_scores if criterion_score.answer_id == answer2_id and criterion_score.criterion_id == criterion_id), AnswerCriterionScore(assignment_id=assignment.id, answer_id=answer2_id, criterion_id=criterion_id)) updated_criteria_scores.append(criterion_score2) key2_scored_object = criterion_score2.convert_to_scored_object() criterion_result_1, criterion_result_2 = calculate_score_1vs1( package_name=assignment.scoring_algorithm.value, key1_scored_object=key1_scored_object, key2_scored_object=key2_scored_object, winner=comparison_criterion.comparison_pair_winner(), other_comparison_pairs=[ cc.convert_to_comparison_pair() for cc in other_criterion_comparisons if cc.criterion_id == criterion_id ], log=current_app.logger) for score, result in [(criterion_score1, criterion_result_1), (criterion_score2, criterion_result_2)]: score.score = result.score score.variable1 = result.variable1 score.variable2 = result.variable2 score.rounds = result.rounds score.wins = result.wins score.loses = result.loses score.opponents = result.opponents updated_scores = [] score1 = next( (score for score in scores if score.answer_id == answer1_id), AnswerScore(assignment_id=assignment.id, answer_id=answer1_id)) updated_scores.append(score1) key1_scored_object = score1.convert_to_scored_object() score2 = next( (score for score in scores if score.answer_id == answer2_id), AnswerScore(assignment_id=assignment.id, answer_id=answer2_id)) updated_scores.append(score2) key2_scored_object = score2.convert_to_scored_object() result_1, result_2 = calculate_score_1vs1( package_name=assignment.scoring_algorithm.value, key1_scored_object=key1_scored_object, key2_scored_object=key2_scored_object, winner=comparison.comparison_pair_winner(), other_comparison_pairs=[ c.convert_to_comparison_pair() for c in other_comparisons ], log=current_app.logger) for score, result in [(score1, result_1), (score2, result_2)]: score.score = result.score score.variable1 = result.variable1 score.variable2 = result.variable2 score.rounds = result.rounds score.wins = result.wins score.loses = result.loses score.opponents = result.opponents db.session.add_all(updated_criteria_scores) db.session.add_all(updated_scores) db.session.commit() return updated_scores @classmethod def calculate_scores(cls, assignment_id): from . import AnswerScore, AnswerCriterionScore, \ AssignmentCriterion, ScoringAlgorithm assignment = Assignment.get(assignment_id) # get all comparisons for this assignment and only load the data we need comparisons = Comparison.query \ .filter(Comparison.assignment_id == assignment_id) \ .all() assignment_criteria = AssignmentCriterion.query \ .with_entities(AssignmentCriterion.criterion_id) \ .filter_by(assignment_id=assignment_id, active=True) \ .all() comparison_criteria = [] comparison_pairs = [] answer_ids = set() for comparison in comparisons: answer_ids.add(comparison.answer1_id) answer_ids.add(comparison.answer2_id) comparison_criteria.extend(comparison.comparison_criteria) comparison_pairs.append(comparison.convert_to_comparison_pair()) # calculate answer score comparison_results = calculate_score( package_name=assignment.scoring_algorithm.value, comparison_pairs=comparison_pairs, log=current_app.logger) scores = AnswerScore.query \ .filter(AnswerScore.answer_id.in_(answer_ids)) \ .all() updated_answer_scores = update_answer_scores(scores, assignment_id, comparison_results) db.session.add_all(updated_answer_scores) # calculate answer criterion scores criterion_comparison_results = {} for assignment_criterion in assignment_criteria: comparison_pairs = [] for comparison_criterion in comparison_criteria: if comparison_criterion.criterion_id != assignment_criterion.criterion_id: continue comparison_pairs.append( comparison_criterion.convert_to_comparison_pair()) criterion_comparison_results[ assignment_criterion.criterion_id] = calculate_score( package_name=assignment.scoring_algorithm.value, comparison_pairs=comparison_pairs, log=current_app.logger) scores = AnswerCriterionScore.query \ .filter(AnswerCriterionScore.answer_id.in_(answer_ids)) \ .all() updated_answer_criteria_scores = update_answer_criteria_scores( scores, assignment_id, criterion_comparison_results) db.session.add_all(updated_answer_criteria_scores) db.session.commit()
class LTIContext(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'lti_context' # table columns lti_consumer_id = db.Column(db.Integer, db.ForeignKey("lti_consumer.id", ondelete="CASCADE"), nullable=False) context_id = db.Column(db.String(255), nullable=False) context_type = db.Column(db.String(255), nullable=True) context_title = db.Column(db.String(255), nullable=True) ext_ims_lis_memberships_id = db.Column(db.String(255), nullable=True) ext_ims_lis_memberships_url = db.Column(db.Text, nullable=True) custom_context_memberships_url = db.Column(db.Text, nullable=True) compair_course_id = db.Column(db.Integer, db.ForeignKey("course.id", ondelete="CASCADE"), nullable=True) # relationships # compair_course via Course Model # lti_consumer via LTIConsumer Model lti_memberships = db.relationship("LTIMembership", backref="lti_context", lazy="dynamic") lti_resource_links = db.relationship("LTIResourceLink", backref="lti_context") # hyprid and other functions compair_course_uuid = association_proxy('compair_course', 'uuid') @hybrid_property def membership_enabled(self): return self.membership_ext_enabled or self.membership_service_enabled @hybrid_property def membership_ext_enabled(self): return self.ext_ims_lis_memberships_url and self.ext_ims_lis_memberships_id @hybrid_property def membership_service_enabled(self): return self.custom_context_memberships_url def is_linked_to_course(self): return self.compair_course_id != None def update_enrolment(self, compair_user_id, course_role): from . import UserCourse if self.is_linked_to_course(): user_course = UserCourse.query \ .filter_by( user_id=compair_user_id, course_id=self.compair_course_id ) \ .one_or_none() if user_course is None: # create new enrollment new_user_course = UserCourse(user_id=compair_user_id, course_id=self.compair_course_id, course_role=course_role) db.session.add(new_user_course) else: user_course.course_role = course_role db.session.commit() @classmethod def get_by_lti_consumer_id_and_context_id(cls, lti_consumer_id, context_id): return LTIContext.query \ .filter_by( lti_consumer_id=lti_consumer_id, context_id=context_id ) \ .one_or_none() @classmethod def get_by_tool_provider(cls, lti_consumer, tool_provider): if tool_provider.context_id == None: return None lti_context = LTIContext.get_by_lti_consumer_id_and_context_id( lti_consumer.id, tool_provider.context_id) if lti_context == None: lti_context = LTIContext(lti_consumer_id=lti_consumer.id, context_id=tool_provider.context_id) db.session.add(lti_context) lti_context.context_type = tool_provider.context_type lti_context.context_title = tool_provider.context_title lti_context.ext_ims_lis_memberships_id = tool_provider.ext_ims_lis_memberships_id lti_context.ext_ims_lis_memberships_url = tool_provider.ext_ims_lis_memberships_url if tool_provider.custom_context_memberships_url: lti_context.custom_context_memberships_url = tool_provider.custom_context_memberships_url db.session.commit() return lti_context @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = (db.UniqueConstraint( 'lti_consumer_id', 'context_id', name='_unique_lti_consumer_and_lti_context'), DefaultTableMixin.default_table_args)
class LTIMembership(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'lti_membership' # table columns lti_context_id = db.Column(db.Integer, db.ForeignKey("lti_context.id", ondelete="CASCADE"), nullable=False) lti_user_id = db.Column(db.Integer, db.ForeignKey("lti_user.id", ondelete="CASCADE"), nullable=False) roles = db.Column(db.String(255), nullable=True) lis_result_sourcedid = db.Column(db.String(255), nullable=True) lis_result_sourcedids = db.Column(db.Text, nullable=True) course_role = db.Column(Enum(CourseRole, name="course_role"), nullable=False) compair_course_id = association_proxy('lti_context', 'compair_course_id') compair_user_id = association_proxy('lti_user', 'compair_user_id') # relationships # lti_conext via LTIContext Model # lti_user via LTIUser Model # hybrid and other functions context_id = association_proxy('lti_context', 'context_id') user_id = association_proxy('lti_user', 'user_id') @classmethod def update_membership_for_course(cls, course): from . import MembershipNoValidContextsException valid_membership_contexts = [ lti_context for lti_context in course.lti_contexts if lti_context.membership_enabled ] if len(valid_membership_contexts) == 0: raise MembershipNoValidContextsException lti_members = [] for lti_context in valid_membership_contexts: members = LTIMembership._get_membership(lti_context) lti_members += LTIMembership._update_membership_for_context( lti_context, members) LTIMembership._update_enrollment_for_course(course.id, lti_members) @classmethod def _update_membership_for_context(cls, lti_context, members): from compair.models import SystemRole, CourseRole, \ LTIUser, LTIUserResourceLink lti_resource_links = lti_context.lti_resource_links # remove old membership rows LTIMembership.query \ .filter_by( lti_context_id=lti_context.id ) \ .delete() # retrieve existing lti_user rows user_ids = [] for member in members: user_ids.append(member['user_id']) existing_lti_users = [] if len(user_ids) > 0: existing_lti_users = LTIUser.query \ .filter(and_( LTIUser.lti_consumer_id == lti_context.lti_consumer_id, LTIUser.user_id.in_(user_ids) )) \ .all() # get existing lti_user_resource_link if there there exists lti users and known resource links for context existing_lti_user_resource_links = [] if len(existing_lti_users) > 0 and len(lti_resource_links) > 0: lti_resource_link_ids = [ lti_resource_link.id for lti_resource_link in lti_resource_links ] existing_lti_user_ids = [ existing_lti_user.id for existing_lti_user in existing_lti_users ] existing_lti_user_resource_links = LTIUserResourceLink.query \ .filter(and_( LTIUserResourceLink.lti_resource_link_id.in_(lti_resource_link_ids), LTIUserResourceLink.lti_user_id.in_(existing_lti_user_ids) )) \ .all() new_lti_users = [] new_lti_user_resource_links = [] lti_memberships = [] for member in members: # get lti user if exists lti_user = next((lti_user for lti_user in existing_lti_users if lti_user.user_id == member.get('user_id')), None) roles = member.get('roles') has_instructor_role = any( role.lower().find("instructor") >= 0 or role.lower().find( "faculty") >= 0 or role.lower().find("staff") >= 0 for role in roles) has_ta_role = any(role.lower().find("teachingassistant") >= 0 for role in roles) # create lti user if doesn't exist if not lti_user: lti_user = LTIUser(lti_consumer_id=lti_context.lti_consumer_id, user_id=member.get('user_id')) new_lti_users.append(lti_user) # update/set fields if needed lti_user.system_role = SystemRole.instructor if has_instructor_role else SystemRole.student lti_user.lis_person_name_given = member.get('person_name_given') lti_user.lis_person_name_family = member.get('person_name_family') lti_user.lis_person_name_full = member.get('person_name_full') lti_user.handle_fullname_with_missing_first_and_last_name() lti_user.lis_person_contact_email_primary = member.get( 'person_contact_email_primary') lti_user.lis_person_sourcedid = member.get('lis_person_sourcedid') if member.get('global_unique_identifier'): lti_user.global_unique_identifier = member.get( 'global_unique_identifier') if member.get('student_number'): lti_user.student_number = member.get('student_number') if not lti_user.is_linked_to_user( ) and lti_user.global_unique_identifier: lti_user.generate_or_link_user_account() course_role = CourseRole.student if has_instructor_role: course_role = CourseRole.instructor elif has_ta_role: course_role = CourseRole.teaching_assistant # create new lti membership row lti_membership = LTIMembership( lti_user=lti_user, lti_context=lti_context, roles=text_type(roles), lis_result_sourcedid=member.get('lis_result_sourcedid'), lis_result_sourcedids=json.dumps( member.get('lis_result_sourcedids')) if member.get('lis_result_sourcedids') else None, course_role=course_role) lti_memberships.append(lti_membership) # if membership includes lis_result_sourcedids, create/update lti user resource links if member.get('lis_result_sourcedids'): for lis_result_sourcedid_set in member.get( 'lis_result_sourcedids'): lti_resource_link = next( lti_resource_link for lti_resource_link in lti_resource_links if lti_resource_link.resource_link_id == lis_result_sourcedid_set['resource_link_id']) if not lti_resource_link: continue lti_user_resource_link = None if len(existing_lti_user_resource_links ) > 0 and lti_user.id: # get lti user resource link if exists lti_user_resource_link = next( (lti_user_resource_link for lti_user_resource_link in existing_lti_user_resource_links if lti_user_resource_link.lti_user_id == lti_user. id and lti_user_resource_link.lti_resource_link_id == lti_resource_link.id), None) # create new lti user resource link if needed if not lti_user_resource_link: lti_user_resource_link = LTIUserResourceLink( lti_resource_link=lti_resource_link, lti_user=lti_user, roles=text_type(roles), course_role=course_role) new_lti_user_resource_links.append( lti_user_resource_link) # finally update the lis_result_sourcedid value for the user resource link lti_user_resource_link.lis_result_sourcedid = lis_result_sourcedid_set[ 'lis_result_sourcedid'] db.session.add_all(new_lti_users) db.session.add_all(existing_lti_users) db.session.add_all(lti_memberships) db.session.add_all(new_lti_user_resource_links) db.session.add_all(existing_lti_user_resource_links) # save new lti users db.session.commit() return lti_memberships @classmethod def _update_enrollment_for_course(cls, course_id, lti_members): from compair.models import UserCourse user_courses = UserCourse.query \ .filter_by(course_id=course_id) \ .all() new_user_courses = [] for lti_member in lti_members: if lti_member.compair_user_id != None: user_course = next( (user_course for user_course in user_courses if user_course.user_id == lti_member.compair_user_id), None) # add new user_course if doesn't exist if user_course == None: user_course = UserCourse( course_id=course_id, user_id=lti_member.compair_user_id, course_role=lti_member.course_role) new_user_courses.append(user_course) # update user_course role else: user_course.course_role = lti_member.course_role # update user profile if needed lti_member.lti_user.update_user_profile() db.session.add_all(new_user_courses) db.session.commit() # set user_course to dropped role if missing from membership results and not current user for user_course in user_courses: # never unenrol current_user if current_user and current_user.is_authenticated and user_course.user_id == current_user.id: continue lti_member = next( (lti_member for lti_member in lti_members if user_course.user_id == lti_member.compair_user_id), None) if lti_member == None: user_course.course_role = CourseRole.dropped db.session.commit() @classmethod def _get_membership(cls, lti_context): if lti_context.membership_ext_enabled: return LTIMembership._get_membership_ext(lti_context) elif lti_context.membership_service_enabled: return LTIMembership._get_membership_service(lti_context) return [] @classmethod def _get_membership_ext(cls, lti_context): lti_consumer = lti_context.lti_consumer memberships_id = lti_context.ext_ims_lis_memberships_id memberships_url = lti_context.ext_ims_lis_memberships_url params = { 'id': memberships_id, 'lti_message_type': 'basic-lis-readmembershipsforcontext', 'lti_version': 'LTI-1p0', 'oauth_callback': 'about:blank' } request = requests.Request('POST', memberships_url, data=params).prepare() sign = OAuth1(lti_consumer.oauth_consumer_key, lti_consumer.oauth_consumer_secret, signature_type=SIGNATURE_TYPE_BODY, signature_method=SIGNATURE_HMAC) signed_request = sign(request) params = parse_qs(signed_request.body.decode('utf-8')) data = LTIMembership._post_membership_request(memberships_url, params) root = ElementTree.fromstring(data.encode('utf-8')) codemajor = root.find('statusinfo/codemajor') if codemajor is not None and codemajor.text in [ 'Failure', 'Unsupported' ]: raise MembershipInvalidRequestException if root.find('memberships') == None or len( root.findall('memberships/member')) == 0: raise MembershipNoResultsException members = [] for record in root.findall('memberships/member'): roles_text = record.findtext('roles') member = { 'user_id': record.findtext('user_id'), 'roles': roles_text.split(",") if roles_text != None else [], 'global_unique_identifier': None, 'student_number': None, 'lis_result_sourcedid': record.findtext('lis_result_sourcedid'), 'person_contact_email_primary': record.findtext('person_contact_email_primary'), 'person_name_given': record.findtext('person_name_given'), 'person_name_family': record.findtext('person_name_family'), 'person_name_full': record.findtext('person_name_full') } # find global unique identifier if available if lti_consumer.global_unique_identifier_param and record.findtext( lti_consumer.global_unique_identifier_param): member['global_unique_identifier'] = record.findtext( lti_consumer.global_unique_identifier_param) if lti_consumer.custom_param_regex_sanitizer and lti_consumer.global_unique_identifier_param.startswith( 'custom_'): regex = re.compile( lti_consumer.custom_param_regex_sanitizer) member['global_unique_identifier'] = regex.sub( '', member['global_unique_identifier']) if member['global_unique_identifier'] == '': member['global_unique_identifier'] = None # find student number if available if lti_consumer.student_number_param and record.findtext( lti_consumer.student_number_param): member['student_number'] = record.findtext( lti_consumer.student_number_param) if lti_consumer.custom_param_regex_sanitizer and lti_consumer.student_number_param.startswith( 'custom_'): regex = re.compile( lti_consumer.custom_param_regex_sanitizer) member['student_number'] = regex.sub( '', member['student_number']) if member['student_number'] == '': member['student_number'] = None members.append(member) return members @classmethod def _get_membership_service(cls, lti_context): # possible parameters are role, lis_result_sourcedid, limit lti_consumer = lti_context.lti_consumer memberships_url = lti_context.custom_context_memberships_url lti_resource_links = lti_context.lti_resource_links members = [] while True: headers = { 'Accept': 'application/vnd.ims.lis.v2.membershipcontainer+json' } request = requests.Request('GET', memberships_url, headers=headers).prepare() # Note: need to use LTIMemerbshipServiceOauthClient since normal client will # not include oauth_body_hash if there is not content type or the body is None sign = OAuth1(lti_consumer.oauth_consumer_key, lti_consumer.oauth_consumer_secret, signature_type=SIGNATURE_TYPE_AUTH_HEADER, signature_method=SIGNATURE_HMAC, client_class=LTIMemerbshipServiceOauthClient) # sign = OAuth1(lti_consumer.oauth_consumer_key, lti_consumer.oauth_consumer_secret, # signature_type=SIGNATURE_TYPE_AUTH_HEADER, signature_method=SIGNATURE_HMAC) signed_request = sign(request) headers = signed_request.headers data = LTIMembership._get_membership_request( memberships_url, headers) if data == None: break membership = data['pageOf']['membershipSubject']['membership'] if len(membership) == 0: raise MembershipNoResultsException for record in membership: if record.get('status').find("Inactive") >= 0: continue member = { 'user_id': record['member'].get('userId'), 'roles': record.get('role'), 'lis_person_sourcedid': record['member'].get('sourcedId'), 'global_unique_identifier': None, 'student_number': None, 'person_contact_email_primary': record['member'].get('email'), 'person_name_given': record['member'].get('givenName'), 'person_name_family': record['member'].get('familyName'), 'person_name_full': record['member'].get('name') } if (lti_consumer.global_unique_identifier_param or lti_consumer.student_number_param ) and 'message' in record: for message in record['message']: if not message[ 'message_type'] == 'basic-lti-launch-request': continue # find global unique identifier if present in membership result if lti_consumer.global_unique_identifier_param: # check if global_unique_identifier_param is a basic lti parameter if lti_consumer.global_unique_identifier_param in message: member['global_unique_identifier'] = message[ lti_consumer. global_unique_identifier_param] # check if global_unique_identifier_param is an extension and present elif lti_consumer.global_unique_identifier_param.startswith( 'ext_'): ext_global_unique_identifier = lti_consumer.global_unique_identifier_param[ len('ext_'):] if ext_global_unique_identifier in message[ 'ext']: member[ 'global_unique_identifier'] = message[ 'ext'][ ext_global_unique_identifier] # check if global_unique_identifier_param is an custom attribute and present elif lti_consumer.global_unique_identifier_param.startswith( 'custom_'): custom_global_unique_identifier = lti_consumer.global_unique_identifier_param[ len('custom_'):] if custom_global_unique_identifier in message[ 'custom']: member[ 'global_unique_identifier'] = message[ 'custom'][ custom_global_unique_identifier] # get student number if present in membership result if lti_consumer.student_number_param: # check if student_number_param is a basic lti parameter if lti_consumer.student_number_param in message: member['student_number'] = message[ lti_consumer.student_number_param] # check if student_number_param is an extension and present elif lti_consumer.student_number_param.startswith( 'ext_'): ext_student_number = lti_consumer.student_number_param[ len('ext_'):] if ext_student_number in message['ext']: member['student_number'] = message['ext'][ ext_student_number] # check if student_number_param is an custom attribute and present elif lti_consumer.student_number_param.startswith( 'custom_'): custom_student_number = lti_consumer.student_number_param[ len('custom_'):] if custom_student_number in message['custom']: member['student_number'] = message[ 'custom'][custom_student_number] members.append(member) # check if another page or else finish memberships_url = data.get('nextPage') if not memberships_url: break # get lis_result_sourcedid for all resource links known to the system for lti_resource_link in lti_resource_links: memberships_url = lti_context.custom_context_memberships_url # add role t0 membership url query string memberships_url += "?" if memberships_url.find("?") == -1 else "&" memberships_url += "role=Learner" # add rlid to membership url query string memberships_url += "&rlid={}".format( lti_resource_link.resource_link_id) while True: headers = { 'Accept': 'application/vnd.ims.lis.v2.membershipcontainer+json' } request = requests.Request('GET', memberships_url, headers=headers).prepare() # Note: need to use LTIMemerbshipServiceOauthClient since normal client will # not include oauth_body_hash if there is not content type or the body is None sign = OAuth1(lti_consumer.oauth_consumer_key, lti_consumer.oauth_consumer_secret, signature_type=SIGNATURE_TYPE_AUTH_HEADER, signature_method=SIGNATURE_HMAC, client_class=LTIMemerbshipServiceOauthClient) # sign = OAuth1(lti_consumer.oauth_consumer_key, lti_consumer.oauth_consumer_secret, # signature_type=SIGNATURE_TYPE_AUTH_HEADER, signature_method=SIGNATURE_HMAC) signed_request = sign(request) headers = signed_request.headers data = LTIMembership._get_membership_request( memberships_url, headers) if data == None: break membership = data['pageOf']['membershipSubject']['membership'] if len(membership) == 0: continue for record in membership: if record.get('status').find("Inactive") >= 0: continue member = next( (member for member in members if member['user_id'] == record['member'].get('userId') ), None) if not member or not 'message' in record: continue for message in record['message']: if not message[ 'message_type'] == 'basic-lti-launch-request' or not 'lis_result_sourcedid' in message: continue lis_result_sourcedid_array = member.setdefault( 'lis_result_sourcedids', []) lis_result_sourcedid_array.append({ 'resource_link_id': lti_resource_link.resource_link_id, 'lis_result_sourcedid': message['lis_result_sourcedid'] }) # check if another page or else finish memberships_url = data.get('nextPage') if not memberships_url: break return members @classmethod def _post_membership_request(cls, memberships_url, params): verify = current_app.config.get('ENFORCE_SSL', True) return requests.post(memberships_url, data=params, verify=verify).text @classmethod def _get_membership_request(cls, memberships_url, headers=None): verify = current_app.config.get('ENFORCE_SSL', True) rv = requests.get(memberships_url, headers=headers, verify=verify) if rv.content: return rv.json() return None @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate resource link in consumer db.UniqueConstraint('lti_context_id', 'lti_user_id', name='_unique_lti_context_and_lti_user'), DefaultTableMixin.default_table_args)
class Answer(DefaultTableMixin, UUIDMixin, AttemptMixin, ActiveMixin, WriteTrackingMixin): __tablename__ = 'answer' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=True) group_id = db.Column(db.Integer, db.ForeignKey('group.id', ondelete="CASCADE"), nullable=True) file_id = db.Column(db.Integer, db.ForeignKey('file.id', ondelete="SET NULL"), nullable=True) content = db.Column(db.Text) round = db.Column(db.Integer, default=0, nullable=False) practice = db.Column(db.Boolean(), default=False, nullable=False, index=True) draft = db.Column(db.Boolean(), default=False, nullable=False, index=True) top_answer = db.Column(db.Boolean(), default=False, nullable=False, index=True) comparable = db.Column(db.Boolean(), default=True, nullable=False, index=True) submission_date = db.Column(db.DateTime(timezone=True), nullable=True) # relationships # assignment via Assignment Model # user via User Model # group via Group Model # file via File Model comments = db.relationship("AnswerComment", backref="answer") score = db.relationship("AnswerScore", uselist=False, backref="answer") criteria_scores = db.relationship("AnswerCriterionScore", backref="answer") # hybrid and other functions course_id = association_proxy( 'assignment', 'course_id', creator=lambda course_id: import_module( 'compair.models.assignment').Assignment(course_id=course_id)) course_uuid = association_proxy('assignment', 'course_uuid') assignment_uuid = association_proxy('assignment', 'uuid') user_avatar = association_proxy('user', 'avatar') user_uuid = association_proxy('user', 'uuid') user_displayname = association_proxy('user', 'displayname') user_student_number = association_proxy('user', 'student_number') user_fullname = association_proxy('user', 'fullname') user_fullname_sortable = association_proxy('user', 'fullname_sortable') user_system_role = association_proxy('user', 'system_role') group_uuid = association_proxy('group', 'uuid') group_avatar = association_proxy('group', 'avatar') group_name = association_proxy('group', 'name') @hybrid_property def private_comment_count(self): return self.comment_count - self.public_comment_count @hybrid_property def group_answer(self): return self.group_id != None @group_answer.expression def group_answer(cls): return cls.group_id != None @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Answer Unavailable" if not message: message = "Sorry, this answer was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Answer Unavailable" if not message: message = "Sorry, this answer was deleted or is no longer accessible." return super(cls, cls).get_active_by_uuid_or_404(model_uuid, joinedloads, title, message) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() cls.comment_count = column_property(select([ func.count(AnswerComment.id) ]).where( and_(AnswerComment.answer_id == cls.id, AnswerComment.active == True, AnswerComment.draft == False)), deferred=True, group='counts') cls.public_comment_count = column_property(select([ func.count(AnswerComment.id) ]).where( and_(AnswerComment.answer_id == cls.id, AnswerComment.active == True, AnswerComment.draft == False, AnswerComment.comment_type == AnswerCommentType.public)), deferred=True, group='counts') cls.self_evaluation_count = column_property(select( [func.count(AnswerComment.id)]).where( and_( AnswerComment.comment_type == AnswerCommentType.self_evaluation, AnswerComment.active == True, AnswerComment.draft == False, AnswerComment.answer_id == cls.id)), deferred=True, group='counts')
class AnswerCriterionScore(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'answer_criterion_score' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) answer_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), nullable=False) criterion_id = db.Column(db.Integer, db.ForeignKey('criterion.id', ondelete="CASCADE"), nullable=False) """ Comparative Judgement score = expected score / number of opponents score_variable1 = expected score score_variable2 = None Elo Rating score = Elo rating score_variable1 = Elo rating score_variable2 = None True Skill Rating score = Rating's Mu - (Default Mu / Default Sigma) * Rating's Sigma score_variable1 = Rating's Mu score_variable2 = Rating's Sigma """ scoring_algorithm = db.Column(EnumType(ScoringAlgorithm), nullable=True, default=ScoringAlgorithm.elo) score = db.Column(db.Float, default=0, nullable=False, index=True) variable1 = db.Column(db.Float, nullable=True) variable2 = db.Column(db.Float, nullable=True) rounds = db.Column(db.Integer, default=0, nullable=False) wins = db.Column(db.Integer, default=0, nullable=False) loses = db.Column(db.Integer, default=0, nullable=False) opponents = db.Column(db.Integer, default=0, nullable=False) # relationships # assignment via Assignment Model # answer via Answer Model # criterion via Criterion Model # hybrid and other functions criterion_uuid = association_proxy('criterion', 'uuid') def convert_to_scored_object(self): return ScoredObject(key=self.answer_id, score=self.score, variable1=self.variable1, variable2=self.variable2, rounds=self.rounds, wins=self.wins, loses=self.loses, opponents=self.opponents) @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() s_alias = cls.__table__.alias() cls.normalized_score = column_property( select([ (cls.score - func.min(s_alias.c.score)) / (func.max(s_alias.c.score) - func.min(s_alias.c.score)) * 100 ]).where( and_( s_alias.c.criterion_id == cls.criterion_id, s_alias.c.assignment_id == cls.assignment_id, )).scalar_subquery()) __table_args__ = (db.UniqueConstraint('answer_id', 'criterion_id', name='_unique_answer_and_criterion'), DefaultTableMixin.default_table_args)
class File(DefaultTableMixin, UUIDMixin, WriteTrackingMixin): __tablename__ = 'file' # table columns user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) kaltura_media_id = db.Column(db.Integer, db.ForeignKey('kaltura_media.id', ondelete="SET NULL"), nullable=True) name = db.Column(db.String(255), nullable=False) alias = db.Column(db.String(255), nullable=False) # relationships # user via User Model # kaltura_media via KalturaMedia Model assignments = db.relationship("Assignment", backref="file", lazy='dynamic') answers = db.relationship("Answer", backref="file", lazy='dynamic') # hyprid and other functions @hybrid_property def extension(self): return self.name.lower().rsplit('.', 1)[1] if '.' in self.name else None @hybrid_property def mimetype(self): mimetype, encoding = mimetypes.guess_type(self.name) return mimetype @hybrid_property def active(self): return self.assignment_count + self.answer_count > 0 @classmethod def get_active_or_404(cls, model_id, joinedloads=[], title=None, message=None): if not title: title = "Attachment Unavailable" if not message: message = "Sorry, this attachment was deleted or is no longer accessible." query = cls.query # load relationships if needed for load_string in joinedloads: query.options(joinedload(load_string)) model = query.get_or_404(model_id) if model is None or not model.active: abort(404, title=title, message=message) return model @classmethod def get_active_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Attachment Unavailable" if not message: message = "Sorry, this attachment was deleted or is no longer accessible." query = cls.query # load relationships if needed for load_string in joinedloads: query.options(joinedload(load_string)) model = query.filter_by(uuid=model_uuid).one_or_none() if model is None or not model.active: abort(404, title=title, message=message) return model @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() cls.assignment_count = column_property( select([func.count(Assignment.id)]). where(and_( Assignment.file_id == cls.id, Assignment.active == True )), deferred=True, group="counts" ) cls.answer_count = column_property( select([func.count(Answer.id)]). where(and_( Answer.file_id == cls.id, Answer.active == True )), deferred=True, group="counts" )
class CourseGrade(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'course_grade' # table columns user_id = db.Column(db.Integer, db.ForeignKey('user.id', ondelete="CASCADE"), nullable=False) course_id = db.Column(db.Integer, db.ForeignKey('course.id', ondelete="CASCADE"), nullable=False) grade = db.Column(db.Float, default=0, nullable=False) # relationships # user via User Model # course via Course Model # hybrid and other functions @classmethod def get_course_grades(cls, course): return CourseGrade.query \ .filter_by(course_id=course.id) \ .all() @classmethod def get_user_course_grade(cls, course, user): return CourseGrade.query \ .filter_by( user_id=user.id, course_id=course.id ) \ .one_or_none() @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate user in course db.UniqueConstraint('course_id', 'user_id', name='_unique_user_and_course'), DefaultTableMixin.default_table_args) @classmethod def calculate_grade(cls, course, user): from . import AssignmentGrade, LTIOutcome, CourseRole user_is_student = False for course_user in course.user_courses: if course_user.user_id != user.id: continue user_is_student = course_user.course_role == CourseRole.student break assignment_ids = [ assignment.id for assignment in course.assignments if assignment.active ] # skip if there aren't any assignments if len(assignment_ids) == 0: CourseGrade.query \ .filter_by(course_id=course.id) \ .filter(CourseGrade.user_id.in_(student_ids)) \ .delete() LTIOutcome.update_course_users_grade(course, student_ids) return elif not user_is_student: return # collect all of the students assignment grades student_assignment_grades = { # default grade of 0 in case assignment_grade record is missing assignment_id: 0.0 for assignment_id in assignment_ids } assignment_grades = AssignmentGrade.query \ .filter_by(user_id=user.id) \ .filter(AssignmentGrade.assignment_id.in_(assignment_ids)) \ .all() for assignment_grade in assignment_grades: student_assignment_grades[ assignment_grade.assignment_id] = assignment_grade.grade grade = _calculate_course_grade(course, student_assignment_grades) course_grade = CourseGrade.get_user_course_grade(course, user) if course_grade == None: course_grade = CourseGrade(user_id=user.id, course_id=course.id) course_grade.grade = grade db.session.add(course_grade) db.session.commit() LTIOutcome.update_course_user_grade(course, user.id) @classmethod def calculate_group_grade(cls, course, group): from . import CourseRole, AssignmentGrade, LTIOutcome student_ids = [course_user.user_id for course_user in course.user_courses if course_user.course_role == CourseRole.student and \ course_user.group_id == group.id] assignment_ids = [ assignment.id for assignment in course.assignments if assignment.active ] # skip if there aren't any assignments if len(student_ids) == 0: return if len(assignment_ids) == 0: CourseGrade.query \ .filter_by(course_id=course.id) \ .filter(CourseGrade.user_id.in_(student_ids)) \ .delete() LTIOutcome.update_course_users_grade(course, student_ids) return # collect all of the students assignment grades student_assignment_grades = {} for student_id in student_ids: student_assignment_grades[student_id] = {} for assignment_id in assignment_ids: student_assignment_grades[student_id][assignment_id] = 0.0 assignment_grades = AssignmentGrade.query \ .filter(AssignmentGrade.assignment_id.in_(assignment_ids)) \ .filter(AssignmentGrade.user_id.in_(student_ids)) \ .all() for assignment_grade in assignment_grades: student_assignment_grades[assignment_grade.user_id][ assignment_grade.assignment_id] = assignment_grade.grade course_grades = CourseGrade.query \ .filter(CourseGrade.user_id.in_(student_ids)) \ .all() new_course_grades = [] for student_id in student_ids: grade = _calculate_course_grade( course, student_assignment_grades[student_id]) course_grade = next((course_grade for course_grade in course_grades if course_grade.user_id == student_id), None) if course_grade == None: course_grade = CourseGrade(user_id=student_id, course_id=course.id) new_course_grades.append(course_grade) course_grade.grade = grade db.session.add_all(course_grades + new_course_grades) db.session.commit() LTIOutcome.update_course_users_grade(course, student_ids) @classmethod def calculate_grades(cls, course): from . import CourseRole, AssignmentGrade, LTIOutcome student_ids = [ course_user.user_id for course_user in course.user_courses if course_user.course_role == CourseRole.student ] assignment_ids = [ assignment.id for assignment in course.assignments if assignment.active ] # skip if there aren't any assignments if len(student_ids) == 0 or len(assignment_ids) == 0: CourseGrade.query \ .filter_by(course_id=course.id) \ .delete() LTIOutcome.update_course_grades(course) return # collect all of the students assignment grades student_assignment_grades = {} for student_id in student_ids: student_assignment_grades[student_id] = {} for assignment_id in assignment_ids: student_assignment_grades[student_id][assignment_id] = 0.0 assignment_grades = AssignmentGrade.query \ .filter(AssignmentGrade.assignment_id.in_(assignment_ids)) \ .filter(AssignmentGrade.user_id.in_(student_ids)) \ .all() for assignment_grade in assignment_grades: student_assignment_grades[assignment_grade.user_id][ assignment_grade.assignment_id] = assignment_grade.grade course_grades = CourseGrade.get_course_grades(course) new_course_grades = [] for student_id in student_ids: grade = _calculate_course_grade( course, student_assignment_grades[student_id]) course_grade = next((course_grade for course_grade in course_grades if course_grade.user_id == student_id), None) if course_grade == None: course_grade = CourseGrade(user_id=student_id, course_id=course.id) new_course_grades.append(course_grade) course_grade.grade = grade db.session.add_all(course_grades + new_course_grades) db.session.commit() LTIOutcome.update_course_grades(course)
class ThirdPartyUser(DefaultTableMixin, UUIDMixin, WriteTrackingMixin): __tablename__ = 'third_party_user' # table columns third_party_type = db.Column(EnumType(ThirdPartyType), nullable=False) unique_identifier = db.Column(db.String(191), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False) _params = db.Column(db.Text) # relationships # user via User Model user_uuid = association_proxy('user', 'uuid') # hybrid and other functions @property def params(self): return json.loads(self._params) if self._params else None @params.setter def params(self, params): self._params = json.dumps(params) if params else None @property def global_unique_identifier(self): if self.params: global_unique_identifier_attribute = None if self.third_party_type == ThirdPartyType.cas: global_unique_identifier_attribute = current_app.config.get('CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD') elif self.third_party_type == ThirdPartyType.saml: global_unique_identifier_attribute = current_app.config.get('SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD') if global_unique_identifier_attribute and global_unique_identifier_attribute in self.params: global_unique_identifier = self.params.get(global_unique_identifier_attribute) if isinstance(global_unique_identifier, list): global_unique_identifier = global_unique_identifier[0] if len(global_unique_identifier) > 0 else None return global_unique_identifier return None @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate user in course db.UniqueConstraint('third_party_type', 'unique_identifier', name='_unique_third_party_type_and_unique_identifier'), DefaultTableMixin.default_table_args ) @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "Third Party User Unavailable" if not message: message = "Sorry, this third party user was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) def generate_or_link_user_account(self): from . import SystemRole, User if not self.user: # check if global_unique_identifier user already exists if self.global_unique_identifier: self.user = User.query \ .filter_by(global_unique_identifier=self.global_unique_identifier) \ .one_or_none() if not self.user: self.user = User( username=None, password=None, system_role=self._get_system_role(), global_unique_identifier=self.global_unique_identifier ) self._sync_name() self._sync_email() if self.user.system_role == SystemRole.student: self._sync_student_number() # instructors can have their display names set to their full name by default if self.user.system_role != SystemRole.student and self.user.fullname != None: self.user.displayname = self.user.fullname else: self.user.displayname = display_name_generator(self.user.system_role.value) def update_user_profile(self): if self.user and self.user.system_role == SystemRole.student and self.params: # overwrite first/last name if student not allowed to change it if not current_app.config.get('ALLOW_STUDENT_CHANGE_NAME'): self._sync_name() # overwrite email if student not allowed to change it if not current_app.config.get('ALLOW_STUDENT_CHANGE_EMAIL'): self._sync_email() # overwrite student number if student not allowed to change it if not current_app.config.get('ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'): self._sync_student_number() def _sync_name(self): if self.params: firstname_attribute = lastname_attribute = None if self.third_party_type == ThirdPartyType.cas: firstname_attribute = current_app.config.get('CAS_ATTRIBUTE_FIRST_NAME') lastname_attribute = current_app.config.get('CAS_ATTRIBUTE_LAST_NAME') elif self.third_party_type == ThirdPartyType.saml: firstname_attribute = current_app.config.get('SAML_ATTRIBUTE_FIRST_NAME') lastname_attribute = current_app.config.get('SAML_ATTRIBUTE_LAST_NAME') if firstname_attribute and firstname_attribute in self.params: first_name = self.params.get(firstname_attribute) if isinstance(first_name, list): first_name = first_name[0] if len(first_name) > 0 else None self.user.firstname = first_name if lastname_attribute and lastname_attribute in self.params: last_name = self.params.get(lastname_attribute) if isinstance(last_name, list): last_name = last_name[0] if len(last_name) > 0 else None self.user.lastname = last_name def _sync_email(self): if self.params: email_attribute = None if self.third_party_type == ThirdPartyType.cas: email_attribute = current_app.config.get('CAS_ATTRIBUTE_EMAIL') elif self.third_party_type == ThirdPartyType.saml: email_attribute = current_app.config.get('SAML_ATTRIBUTE_EMAIL') if email_attribute and email_attribute in self.params: email = self.params.get(email_attribute) if isinstance(email, list): email = email[0] if len(email) > 0 else None self.user.email = email def _sync_student_number(self): if self.params: student_number_attribute = None if self.third_party_type == ThirdPartyType.cas: student_number_attribute = current_app.config.get('CAS_ATTRIBUTE_STUDENT_NUMBER') elif self.third_party_type == ThirdPartyType.saml: student_number_attribute = current_app.config.get('SAML_ATTRIBUTE_STUDENT_NUMBER') if student_number_attribute and student_number_attribute in self.params: student_number = self.params.get(student_number_attribute) if isinstance(student_number, list): student_number = student_number[0] if len(student_number) > 0 else None self.user.student_number = student_number def _get_system_role(self): from . import SystemRole if self.params: user_roles_attribute = instructor_role_values = None if self.third_party_type == ThirdPartyType.cas: user_roles_attribute = current_app.config.get('CAS_ATTRIBUTE_USER_ROLE') instructor_role_values = list(current_app.config.get('CAS_INSTRUCTOR_ROLE_VALUES')) if self.third_party_type == ThirdPartyType.saml: user_roles_attribute = current_app.config.get('SAML_ATTRIBUTE_USER_ROLE') instructor_role_values = list(current_app.config.get('SAML_INSTRUCTOR_ROLE_VALUES')) if user_roles_attribute and instructor_role_values and user_roles_attribute in self.params: user_roles = self.params.get(user_roles_attribute) if not isinstance(user_roles, list): user_roles = [user_roles] for user_role in user_roles: if user_role in instructor_role_values: return SystemRole.instructor return SystemRole.student def upgrade_system_role(self): # upgrade system role is needed if self.user and self.params and self._get_system_role(): system_role = self._get_system_role() if self.user.system_role == SystemRole.student and system_role == SystemRole.instructor: self.user.system_role = system_role db.session.commit()
class LTIUser(DefaultTableMixin, UUIDMixin, WriteTrackingMixin): __tablename__ = 'lti_user' # table columns lti_consumer_id = db.Column(db.Integer, db.ForeignKey("lti_consumer.id", ondelete="CASCADE"), nullable=False) user_id = db.Column(db.String(191), nullable=False) lis_person_name_given = db.Column(db.String(255), nullable=True) lis_person_name_family = db.Column(db.String(255), nullable=True) lis_person_name_full = db.Column(db.String(255), nullable=True) lis_person_contact_email_primary = db.Column(db.String(255), nullable=True) global_unique_identifier = db.Column(db.String(255), nullable=True) compair_user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=True) system_role = db.Column(EnumType(SystemRole), nullable=False) student_number = db.Column(db.String(255), nullable=True) lis_person_sourcedid = db.Column(db.String(255), nullable=True) # relationships # compair_user via User Model # lti_consumer via LTIConsumer Model lti_memberships = db.relationship("LTIMembership", backref="lti_user", lazy="dynamic") lti_user_resource_links = db.relationship("LTIUserResourceLink", backref="lti_user", lazy="dynamic") # hybrid and other functions lti_consumer_uuid = association_proxy('lti_consumer', 'uuid') oauth_consumer_key = association_proxy('lti_consumer', 'oauth_consumer_key') compair_user_uuid = association_proxy('compair_user', 'uuid') def is_linked_to_user(self): return self.compair_user_id != None def generate_or_link_user_account(self): from . import SystemRole, User if self.compair_user_id == None and self.global_unique_identifier: self.compair_user = User.query \ .filter_by(global_unique_identifier=self.global_unique_identifier) \ .one_or_none() if not self.compair_user: self.compair_user = User( username=None, password=None, system_role=self.system_role, firstname=self.lis_person_name_given, lastname=self.lis_person_name_family, email=self.lis_person_contact_email_primary, global_unique_identifier=self.global_unique_identifier ) if self.compair_user.system_role == SystemRole.student: self.compair_user.student_number = self.student_number # instructors can have their display names set to their full name by default if self.compair_user.system_role != SystemRole.student and self.compair_user.fullname != None: self.compair_user.displayname = self.compair_user.fullname else: self.compair_user.displayname = display_name_generator(self.compair_user.system_role.value) db.session.commit() @classmethod def get_by_lti_consumer_id_and_user_id(cls, lti_consumer_id, user_id): return LTIUser.query \ .filter_by( lti_consumer_id=lti_consumer_id, user_id=user_id ) \ .one_or_none() @classmethod def get_by_tool_provider(cls, lti_consumer, tool_provider): from . import SystemRole if tool_provider.user_id == None: return None lti_user = LTIUser.get_by_lti_consumer_id_and_user_id( lti_consumer.id, tool_provider.user_id) if not lti_user: lti_user = LTIUser( lti_consumer_id=lti_consumer.id, user_id=tool_provider.user_id, system_role=SystemRole.instructor \ if tool_provider.roles and any( role.lower().find("instructor") >= 0 or role.lower().find("faculty") >= 0 or role.lower().find("staff") >= 0 for role in tool_provider.roles ) \ else SystemRole.student ) db.session.add(lti_user) lti_user.lis_person_name_given = tool_provider.lis_person_name_given lti_user.lis_person_name_family = tool_provider.lis_person_name_family lti_user.lis_person_name_full = tool_provider.lis_person_name_full lti_user.handle_fullname_with_missing_first_and_last_name() lti_user.lis_person_contact_email_primary = tool_provider.lis_person_contact_email_primary lti_user.lis_person_sourcedid = tool_provider.lis_person_sourcedid if lti_consumer.global_unique_identifier_param and lti_consumer.global_unique_identifier_param in tool_provider.launch_params: lti_user.global_unique_identifier = tool_provider.launch_params[lti_consumer.global_unique_identifier_param] if lti_consumer.custom_param_regex_sanitizer and lti_consumer.global_unique_identifier_param.startswith('custom_'): regex = re.compile(lti_consumer.custom_param_regex_sanitizer) lti_user.global_unique_identifier = regex.sub('', lti_user.global_unique_identifier) if lti_user.global_unique_identifier == '': lti_user.global_unique_identifier = None else: lti_user.global_unique_identifier = None if lti_consumer.student_number_param and lti_consumer.student_number_param in tool_provider.launch_params: lti_user.student_number = tool_provider.launch_params[lti_consumer.student_number_param] if lti_consumer.custom_param_regex_sanitizer and lti_consumer.student_number_param.startswith('custom_'): regex = re.compile(lti_consumer.custom_param_regex_sanitizer) lti_user.student_number = regex.sub('', lti_user.student_number) if lti_user.student_number == '': lti_user.student_number = None else: lti_user.student_number = None if not lti_user.is_linked_to_user() and lti_user.global_unique_identifier: lti_user.generate_or_link_user_account() db.session.commit() return lti_user @classmethod def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None): if not title: title = "LTI User Unavailable" if not message: message = "Sorry, this LTI user was deleted or is no longer accessible." return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message) # relationships def update_user_profile(self): if self.compair_user and self.compair_user.system_role == SystemRole.student: # overwrite first/last name if student not allowed to change it if not current_app.config.get('ALLOW_STUDENT_CHANGE_NAME'): self.compair_user.firstname = self.lis_person_name_given self.compair_user.lastname = self.lis_person_name_family # overwrite email if student not allowed to change it if not current_app.config.get('ALLOW_STUDENT_CHANGE_EMAIL'): self.compair_user.email = self.lis_person_contact_email_primary # overwrite student number if student not allowed to change it and lti_consumer has a student_number_param if not current_app.config.get('ALLOW_STUDENT_CHANGE_STUDENT_NUMBER') and self.lti_consumer.student_number_param: self.compair_user.student_number = self.student_number def upgrade_system_role(self): # upgrade system role is needed if self.compair_user: if self.compair_user.system_role == SystemRole.student and self.system_role in [SystemRole.instructor, SystemRole.sys_admin]: self.compair_user.system_role = self.system_role elif self.compair_user.system_role == SystemRole.instructor and self.system_role == SystemRole.sys_admin: self.compair_user.system_role = self.system_role db.session.commit() def handle_fullname_with_missing_first_and_last_name(self): if self.lis_person_name_full and (not self.lis_person_name_given or not self.lis_person_name_family): full_name_parts = self.lis_person_name_full.split(" ") if len(full_name_parts) >= 2: # assume lis_person_name_given is all but last part self.lis_person_name_given = " ".join(full_name_parts[:-1]) self.lis_person_name_family = full_name_parts[-1] else: # not sure what is first or last name, just assignment both to full name self.lis_person_name_given = self.lis_person_name_full self.lis_person_name_family = self.lis_person_name_full @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() __table_args__ = ( # prevent duplicate resource link in consumer db.UniqueConstraint('lti_consumer_id', 'user_id', name='_unique_lti_consumer_and_lti_user'), DefaultTableMixin.default_table_args )
class AnswerScore(DefaultTableMixin, WriteTrackingMixin): __tablename__ = 'answer_score' # table columns assignment_id = db.Column(db.Integer, db.ForeignKey('assignment.id', ondelete="CASCADE"), nullable=False) answer_id = db.Column(db.Integer, db.ForeignKey('answer.id', ondelete="CASCADE"), unique=True, nullable=False) """ Comparative Judgement score = expected score / number of opponents score_variable1 = expected score score_variable2 = None Elo Rating score = Elo rating score_variable1 = Elo rating score_variable2 = None True Skill Rating score = Rating's Mu - (Default Mu / Default Sigma) * Rating's Sigma score_variable1 = Rating's Mu score_variable2 = Rating's Sigma """ scoring_algorithm = db.Column(EnumType(ScoringAlgorithm, name="scoring_algorithm"), nullable=True, default=ScoringAlgorithm.elo) score = db.Column(db.Float, default=0, nullable=False, index=True) variable1 = db.Column(db.Float, nullable=True) variable2 = db.Column(db.Float, nullable=True) rounds = db.Column(db.Integer, default=0, nullable=False) wins = db.Column(db.Integer, default=0, nullable=False) loses = db.Column(db.Integer, default=0, nullable=False) opponents = db.Column(db.Integer, default=0, nullable=False) # relationships # assignment via Assignment Model # answer via Answer Model # hyprid and other functions def convert_to_scored_object(self): return ScoredObject( key=self.answer_id, score=self.score, variable1=self.variable1, variable2=self.variable2, rounds=self.rounds, wins=self.wins, loses=self.loses, opponents=self.opponents ) @hybrid_property def rank(self): scores = AnswerScore.get_assignment_scores(self.assignment_id) for index, score in enumerate(scores): if score.score == self.score: return index + 1 return None # TODO: this should be cached @classmethod def get_assignment_scores(cls, assignment_id): return AnswerScore.query \ .with_entities(AnswerScore.score) \ .join("answer") \ .filter(and_( Answer.active == True, AnswerScore.assignment_id == assignment_id )) \ .order_by(AnswerScore.score.desc()) \ .all() @classmethod def get_score_for_rank(cls, assignment_id, rank): scores = AnswerScore.get_assignment_scores(assignment_id) if len(scores) < rank: return None else: return scores[rank-1].score @classmethod def __declare_last__(cls): super(cls, cls).__declare_last__() s_alias = cls.__table__.alias() cls.normalized_score = column_property( select([ (cls.score - func.min(s_alias.c.score)) / (func.max(s_alias.c.score) - func.min(s_alias.c.score)) * 100 ]). select_from(join(Answer, s_alias, s_alias.c.answer_id == Answer.id)). where(and_( Answer.active == True, s_alias.c.assignment_id == cls.assignment_id, )) ) __table_args__ = ( DefaultTableMixin.default_table_args )