Beispiel #1
0
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
    )
Beispiel #2
0
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"
        )
Beispiel #3
0
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__()
Beispiel #4
0
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__()
Beispiel #5
0
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__()
Beispiel #6
0
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')
Beispiel #7
0
class Course(DefaultTableMixin, UUIDMixin, ActiveMixin, WriteTrackingMixin):
    __tablename__ = 'course'

    # table columns
    name = db.Column(db.String(255), nullable=False)
    year = db.Column(db.Integer, nullable=False)
    term = db.Column(db.String(255), nullable=False)
    sandbox = db.Column(db.Boolean(), nullable=False, default=False, index=True)
    start_date = db.Column(db.DateTime(timezone=True), nullable=True)
    end_date = db.Column(db.DateTime(timezone=True), nullable=True)
    # relationships

    # user many-to-many course with association user_course
    user_courses = db.relationship("UserCourse", back_populates="course", lazy="dynamic")
    assignments = db.relationship("Assignment", backref="course", lazy="dynamic")
    grades = db.relationship("CourseGrade", backref="course", lazy='dynamic')
    groups = db.relationship("Group", backref="course", lazy='dynamic')

    # lti
    lti_contexts = db.relationship("LTIContext", backref="compair_course", lazy='dynamic')

    # hybrid and other functions
    @hybrid_property
    def lti_linked(self):
        return self.lti_context_count > 0

    @hybrid_property
    def lti_has_sis_data(self):
        return self.lti_context_sis_count > 0

    @hybrid_property
    def lti_sis_data(self):
        sis_data = {}
        for lti_context in self.lti_contexts.all():
            sis_course_id = lti_context.lis_course_offering_sourcedid
            sis_section_id = lti_context.lis_course_section_sourcedid
            if not sis_course_id or not sis_section_id:
                continue
            sis_data.setdefault(sis_course_id, []).append(sis_section_id)
        return sis_data

    @hybrid_property
    def available(self):
        now = dateutil.parser.parse(datetime.datetime.utcnow().replace(tzinfo=pytz.utc).isoformat())

        # must be after start date if set
        if self.start_date and self.start_date.replace(tzinfo=pytz.utc) > now:
            return False

        # must be before end date if set
        if self.end_date and now >= self.end_date.replace(tzinfo=pytz.utc):
            return False

        return True

    @hybrid_property
    def start_date_order(self):
        if self.start_date:
            return self.start_date
        elif self.min_assignment_answer_start:
            return self.min_assignment_answer_start
        else:
            return self.created

    @start_date_order.expression
    def start_date_order(cls):
        return case([
            (cls.start_date != None, cls.start_date),
            (cls.min_assignment_answer_start != None, cls.min_assignment_answer_start)
        ], else_ = cls.created)

    def calculate_grade(self, user):
        from . import CourseGrade
        CourseGrade.calculate_grade(self, user)

    def calculate_group_grade(self, group):
        from . import CourseGrade
        CourseGrade.calculate_group_grade(self, group)

    def calculate_grades(self):
        from . import CourseGrade
        CourseGrade.calculate_grades(self)

    def clear_lti_links(self):
        for lti_context in self.lti_contexts.all():
            lti_context.compair_course_id = None
        for assignment in self.assignments.all():
            assignment.clear_lti_links()

    @classmethod
    def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None):
        if not title:
            title = "Course Unavailable"
        if not message:
            message = "Sorry, this course 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 = "Course Unavailable"
        if not message:
            message = "Sorry, this course 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 .lti_models import LTIContext
        from . import Assignment, UserCourse, CourseRole
        super(cls, cls).__declare_last__()

        cls.groups_locked = column_property(
            exists([1]).
            where(and_(
                Assignment.course_id == cls.id,
                Assignment.active == True,
                Assignment.enable_group_answers == True,
                or_(
                    and_(Assignment.compare_start == None, Assignment.answer_end <= sql_utcnow()),
                    and_(Assignment.compare_start != None, Assignment.compare_start <= sql_utcnow())
                )
            )),
            deferred=True,
            group="group_associates"
        )

        cls.min_assignment_answer_start = column_property(
            select([func.min(Assignment.answer_start)]).
            where(and_(
                Assignment.course_id == cls.id,
                Assignment.active == True
            )).
            scalar_subquery(),
            deferred=True,
            group="min_associates"
        )

        cls.lti_context_count = column_property(
            select([func.count(LTIContext.id)]).
            where(LTIContext.compair_course_id == cls.id).
            scalar_subquery(),
            deferred=True,
            group="counts"
        )

        cls.lti_context_sis_count = column_property(
            select([func.count(LTIContext.id)]).
            where(and_(
                LTIContext.compair_course_id == cls.id,
                LTIContext.lis_course_offering_sourcedid != None,
                LTIContext.lis_course_section_sourcedid != None,
            )).
            scalar_subquery(),
            deferred=True,
            group="counts"
        )

        cls.assignment_count = column_property(
            select([func.count(Assignment.id)]).
            where(and_(
                Assignment.course_id == cls.id,
                Assignment.active == True
            )).
            scalar_subquery(),
            deferred=True,
            group="counts"
        )

        cls.student_assignment_count = column_property(
            select([func.count(Assignment.id)]).
            where(and_(
                Assignment.course_id == cls.id,
                Assignment.active == True,
                Assignment.answer_start <= sql_utcnow()
            )).
            scalar_subquery(),
            deferred=True,
            group="counts"
        )

        cls.student_count = column_property(
            select([func.count(UserCourse.id)]).
            where(and_(
                UserCourse.course_id == cls.id,
                UserCourse.course_role == CourseRole.student
            )).
            scalar_subquery(),
            deferred=True,
            group="counts"
        )
Beispiel #8
0
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")
Beispiel #9
0
class User(DefaultTableMixin, UUIDMixin, WriteTrackingMixin, UserMixin):
    __tablename__ = 'user'

    # table columns
    global_unique_identifier = db.Column(
        db.String(191),
        nullable=True)  #should be treated as write once and only once
    username = db.Column(db.String(191), unique=True, nullable=True)
    _password = db.Column(db.String(255), unique=False, nullable=True)
    system_role = db.Column(EnumType(SystemRole), nullable=False, index=True)
    displayname = db.Column(db.String(255), nullable=False)
    email = db.Column(db.String(254),
                      nullable=True)  # email addresses are max 254 characters
    firstname = db.Column(db.String(255), nullable=True)
    lastname = db.Column(db.String(255), nullable=True)
    student_number = db.Column(db.String(50), unique=True, nullable=True)
    last_online = db.Column(db.DateTime)
    email_notification_method = db.Column(
        EnumType(EmailNotificationMethod),
        nullable=False,
        default=EmailNotificationMethod.enable,
        index=True)

    # relationships

    # user many-to-many course with association user_course
    user_courses = db.relationship("UserCourse",
                                   foreign_keys='UserCourse.user_id',
                                   back_populates="user")
    course_grades = db.relationship("CourseGrade",
                                    foreign_keys='CourseGrade.user_id',
                                    backref="user",
                                    lazy='dynamic')
    assignments = db.relationship("Assignment",
                                  foreign_keys='Assignment.user_id',
                                  backref="user",
                                  lazy='dynamic')
    assignment_grades = db.relationship("AssignmentGrade",
                                        foreign_keys='AssignmentGrade.user_id',
                                        backref="user",
                                        lazy='dynamic')
    answers = db.relationship("Answer",
                              foreign_keys='Answer.user_id',
                              backref="user",
                              lazy='dynamic')
    answer_comments = db.relationship("AnswerComment",
                                      foreign_keys='AnswerComment.user_id',
                                      backref="user",
                                      lazy='dynamic')
    criteria = db.relationship("Criterion",
                               foreign_keys='Criterion.user_id',
                               backref="user",
                               lazy='dynamic')
    files = db.relationship("File",
                            foreign_keys='File.user_id',
                            backref="user",
                            lazy='dynamic')
    kaltura_files = db.relationship("KalturaMedia",
                                    foreign_keys='KalturaMedia.user_id',
                                    backref="user",
                                    lazy='dynamic')
    comparisons = db.relationship("Comparison",
                                  foreign_keys='Comparison.user_id',
                                  backref="user",
                                  lazy='dynamic')
    # third party authentification
    third_party_auths = db.relationship("ThirdPartyUser",
                                        foreign_keys='ThirdPartyUser.user_id',
                                        backref="user",
                                        lazy='dynamic')
    # lti authentification
    lti_user_links = db.relationship("LTIUser",
                                     foreign_keys='LTIUser.compair_user_id',
                                     backref="compair_user",
                                     lazy='dynamic')

    # hybrid and other functions
    @property
    def password(self):
        return self._password

    @password.setter
    def password(self, password):
        self._password = hash_password(password) if password != None else None

    @hybrid_property
    def fullname(self):
        if self.firstname and self.lastname:
            return '%s %s' % (self.firstname, self.lastname)
        elif self.firstname:  # only first name provided
            return self.firstname
        elif self.lastname:  # only last name provided
            return self.lastname
        elif self.displayname:
            return self.displayname
        else:
            return None

    @hybrid_property
    def fullname_sortable(self):
        if self.firstname and self.lastname and self.system_role == SystemRole.student and self.student_number:
            return '%s, %s (%s)' % (self.lastname, self.firstname,
                                    self.student_number)
        elif self.firstname and self.lastname:
            return '%s, %s' % (self.lastname, self.firstname)
        elif self.firstname:  # only first name provided
            return self.firstname
        elif self.lastname:  # only last name provided
            return self.lastname
        elif self.displayname:
            return self.displayname
        else:
            return None

    @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 = None
        if self.system_role != SystemRole.student and self.email:
            hash_input = self.email
        elif self.uuid:
            hash_input = self.uuid + "@compair"

        m = hashlib.md5()
        m.update(hash_input.strip().lower().encode('utf-8'))
        return m.hexdigest()

    @hybrid_property
    def uses_compair_login(self):
        # third party auth users may have their username not set
        return self.username != None and current_app.config['APP_LOGIN_ENABLED']

    @hybrid_property
    def lti_linked(self):
        return self.lti_user_link_count > 0

    @hybrid_property
    def has_third_party_auth(self):
        return self.third_party_auth_count > 0

    def verify_password(self, password):
        if self.password == None or not current_app.config['APP_LOGIN_ENABLED']:
            return False
        pwd_context = getattr(security, current_app.config['PASSLIB_CONTEXT'])
        return pwd_context.verify(password, self.password)

    def update_last_online(self):
        self.last_online = datetime.utcnow()
        db.session.add(self)
        db.session.commit()

    def generate_session_token(self):
        """
        Generate a session token that identifies the user login session. Since the flask
        wll generate the same session _id for the same IP and browser agent combination,
        it is hard to distinguish the users by session from the activity log
        """
        key = str(self.id) + '-' + str(time.time())
        return hashlib.md5(key.encode('UTF-8')).hexdigest()

    # This could be used for token based authentication
    # def generate_auth_token(self, expiration=60):
    #     s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
    #     return s.dumps({'id': self.id})

    @classmethod
    def get_user_course_role(cls, user_id, course_id):
        from . import UserCourse
        user_course = UserCourse.query \
            .filter_by(
                course_id=course_id,
                user_id=user_id
            ) \
            .one_or_none()
        return user_course.course_role if user_course else None

    def get_course_role(self, course_id):
        """ Return user's course role by course id """

        for user_course in self.user_courses:
            if user_course.course_id == course_id:
                return user_course.course_role

        return None

    @classmethod
    def get_user_course_group(cls, user_id, course_id):
        from . import UserCourse
        user_course = UserCourse.query \
            .options(joinedload('group')) \
            .filter_by(
                course_id=course_id,
                user_id=user_id
            ) \
            .one_or_none()

        return user_course.group if user_course else None

    def get_course_group(self, course_id):
        """ Return user's course group by course id """

        for user_course in self.user_courses:
            if user_course.course_id == course_id:
                return user_course.group

        return None

    @classmethod
    def get_by_uuid_or_404(cls,
                           model_uuid,
                           joinedloads=[],
                           title=None,
                           message=None):
        if not title:
            title = "User Unavailable"
        if not message:
            message = "Sorry, this user 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 = "User Unavailable"
        if not message:
            message = "Sorry, this user 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 .lti_models import LTIUser
        from . import ThirdPartyUser
        super(cls, cls).__declare_last__()

        cls.third_party_auth_count = column_property(select([
            func.count(ThirdPartyUser.id)
        ]).where(ThirdPartyUser.user_id == cls.id).scalar_subquery(),
                                                     deferred=True,
                                                     group="counts")

        cls.lti_user_link_count = column_property(select([
            func.count(LTIUser.id)
        ]).where(LTIUser.compair_user_id == cls.id).scalar_subquery(),
                                                  deferred=True,
                                                  group="counts")

    __table_args__ = (
        # prevent duplicate user in course
        db.UniqueConstraint('global_unique_identifier',
                            name='_unique_global_unique_identifier'),
        DefaultTableMixin.default_table_args)
Beispiel #10
0
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()
Beispiel #11
0
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)
Beispiel #12
0
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)
Beispiel #13
0
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)
Beispiel #14
0
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()
Beispiel #15
0
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"
        )
Beispiel #16
0
class LTIConsumer(DefaultTableMixin, UUIDMixin, ActiveMixin,
                  WriteTrackingMixin):
    __tablename__ = 'lti_consumer'

    # table columns
    oauth_consumer_key = db.Column(db.String(255), unique=True, nullable=False)
    oauth_consumer_secret = db.Column(db.String(255), nullable=False)
    lti_version = db.Column(db.String(20), nullable=True)
    tool_consumer_instance_guid = db.Column(db.String(255),
                                            unique=True,
                                            nullable=True)
    tool_consumer_instance_name = db.Column(db.String(255), nullable=True)
    tool_consumer_instance_url = db.Column(db.Text, nullable=True)
    lis_outcome_service_url = db.Column(db.Text, nullable=True)
    user_id_override = db.Column(db.String(255), nullable=True)

    # relationships
    lti_nonces = db.relationship("LTINonce",
                                 backref="lti_consumer",
                                 lazy="dynamic")
    lti_contexts = db.relationship("LTIContext",
                                   backref="lti_consumer",
                                   lazy="dynamic")
    lti_resource_links = db.relationship("LTIResourceLink",
                                         backref="lti_consumer",
                                         lazy="dynamic")
    lti_users = db.relationship("LTIUser",
                                backref="lti_consumer",
                                lazy="dynamic")

    # hyprid and other functions
    @classmethod
    def get_by_consumer_key(cls, consumer_key):
        return LTIConsumer.query \
            .filter_by(
                active=True,
                oauth_consumer_key=consumer_key
            ) \
            .one_or_none()

    @classmethod
    def get_by_tool_provider(cls, tool_provider):
        lti_consumer = LTIConsumer.get_by_consumer_key(
            tool_provider.consumer_key)

        if lti_consumer == None:
            return None

        lti_consumer.lti_version = tool_provider.lti_version
        lti_consumer.tool_consumer_instance_guid = tool_provider.tool_consumer_instance_guid
        lti_consumer.tool_consumer_instance_name = tool_provider.tool_consumer_instance_name
        lti_consumer.tool_consumer_instance_url = tool_provider.tool_consumer_instance_url

        # do no overwrite lis_outcome_service_url if value is None
        # some LTI consumers do not always send the lis_outcome_service_url
        # ex: Canvas when linking from module instead of an assignment
        if tool_provider.lis_outcome_service_url:
            lti_consumer.lis_outcome_service_url = tool_provider.lis_outcome_service_url

        db.session.commit()

        return lti_consumer

    @classmethod
    def get_by_uuid_or_404(cls,
                           model_uuid,
                           joinedloads=[],
                           title=None,
                           message=None):
        if not title:
            title = "LTI Consumer Unavailable"
        if not message:
            message = "Sorry, this LTI consumer 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 = "LTI Consumer Unavailable"
        if not message:
            message = "Sorry, this LTI consumer 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__()
Beispiel #17
0
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)
Beispiel #18
0
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
    )
Beispiel #19
0
class User(DefaultTableMixin, UUIDMixin, WriteTrackingMixin, UserMixin):
    __tablename__ = 'user'

    # table columns
    username = db.Column(db.String(255), unique=True, nullable=True)
    _password = db.Column(db.String(255), unique=False, nullable=True)
    system_role = db.Column(EnumType(SystemRole, name="system_role"),
                            nullable=False,
                            index=True)
    displayname = db.Column(db.String(255), nullable=False)
    email = db.Column(db.String(254))  # email addresses are max 254 characters
    firstname = db.Column(db.String(255))
    lastname = db.Column(db.String(255))
    student_number = db.Column(db.String(50), unique=True, nullable=True)
    last_online = db.Column(db.DateTime)
    email_notification_method = db.Column(
        EnumType(EmailNotificationMethod, name="email_notification_method"),
        nullable=False,
        default=EmailNotificationMethod.enable,
        index=True)

    # relationships

    # user many-to-many course with association user_course
    user_courses = db.relationship("UserCourse",
                                   foreign_keys='UserCourse.user_id',
                                   back_populates="user")
    course_grades = db.relationship("CourseGrade",
                                    foreign_keys='CourseGrade.user_id',
                                    backref="user",
                                    lazy='dynamic')
    assignments = db.relationship("Assignment",
                                  foreign_keys='Assignment.user_id',
                                  backref="user",
                                  lazy='dynamic')
    assignment_grades = db.relationship("AssignmentGrade",
                                        foreign_keys='AssignmentGrade.user_id',
                                        backref="user",
                                        lazy='dynamic')
    assignment_comments = db.relationship(
        "AssignmentComment",
        foreign_keys='AssignmentComment.user_id',
        backref="user",
        lazy='dynamic')
    answers = db.relationship("Answer",
                              foreign_keys='Answer.user_id',
                              backref="user",
                              lazy='dynamic')
    answer_comments = db.relationship("AnswerComment",
                                      foreign_keys='AnswerComment.user_id',
                                      backref="user",
                                      lazy='dynamic')
    criteria = db.relationship("Criterion",
                               foreign_keys='Criterion.user_id',
                               backref="user",
                               lazy='dynamic')
    files = db.relationship("File",
                            foreign_keys='File.user_id',
                            backref="user",
                            lazy='dynamic')
    kaltura_files = db.relationship("KalturaMedia",
                                    foreign_keys='KalturaMedia.user_id',
                                    backref="user",
                                    lazy='dynamic')
    comparisons = db.relationship("Comparison",
                                  foreign_keys='Comparison.user_id',
                                  backref="user",
                                  lazy='dynamic')
    # third party authentification
    third_party_auths = db.relationship("ThirdPartyUser",
                                        foreign_keys='ThirdPartyUser.user_id',
                                        backref="user",
                                        lazy='dynamic')
    # lti authentification
    lti_user_links = db.relationship("LTIUser",
                                     foreign_keys='LTIUser.compair_user_id',
                                     backref="compair_user",
                                     lazy='dynamic')

    # hyprid and other functions

    def _get_password(self):
        return self._password

    def _set_password(self, password):
        self._password = hash_password(password) if password != None else None

    password = property(_get_password, _set_password)
    password = synonym('_password', descriptor=password)

    @hybrid_property
    def fullname(self):
        if self.firstname and self.lastname:
            return '%s %s' % (self.firstname, self.lastname)
        elif self.firstname:  # only first name provided
            return self.firstname
        elif self.lastname:  # only last name provided
            return self.lastname
        else:
            return None

    @hybrid_property
    def fullname_sortable(self):
        if self.firstname and self.lastname:
            return '%s, %s' % (self.lastname, self.firstname)
        elif self.firstname:  # only first name provided
            return self.firstname
        elif self.lastname:  # only last name provided
            return self.lastname
        else:
            return None

    @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
        Defaults to a hash of the user's username if no email is available
        """
        hash_input = None
        if self.system_role != SystemRole.student and self.email:
            hash_input = self.email
        elif self.uuid:
            hash_input = self.uuid + "@compair"

        m = hashlib.md5()
        m.update(hash_input.strip().lower().encode('utf-8'))
        return m.hexdigest()

    @hybrid_property
    def uses_compair_login(self):
        # third party auth users may have their username not set
        return self.username != None and current_app.config['APP_LOGIN_ENABLED']

    def verify_password(self, password):
        if self.password == None or not current_app.config['APP_LOGIN_ENABLED']:
            return False
        pwd_context = getattr(security, current_app.config['PASSLIB_CONTEXT'])
        return pwd_context.verify(password, self.password)

    def update_last_online(self):
        self.last_online = datetime.utcnow()
        db.session.add(self)
        db.session.commit()

    def generate_session_token(self):
        """
        Generate a session token that identifies the user login session. Since the flask
        wll generate the same session _id for the same IP and browser agent combination,
        it is hard to distinguish the users by session from the activity log
        """
        key = str(self.id) + str(time.time())
        return hashlib.md5(key.encode('UTF-8')).hexdigest()

    # This could be used for token based authentication
    # def generate_auth_token(self, expiration=60):
    #     s = Serializer(current_app.config['SECRET_KEY'], expires_in=expiration)
    #     return s.dumps({'id': self.id})

    def get_course_role(self, course_id):
        """ Return user's course role by course id """

        for user_course in self.user_courses:
            if user_course.course_id == course_id:
                return user_course.course_role

        return None

    @classmethod
    def get_by_uuid_or_404(cls,
                           model_uuid,
                           joinedloads=[],
                           title=None,
                           message=None):
        if not title:
            title = "User Unavailable"
        if not message:
            message = "Sorry, this user 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 = "User Unavailable"
        if not message:
            message = "Sorry, this user 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__()


# This could be used for token based authentication
# def verify_auth_token(token):
#     s = Serializer(current_app.config['SECRET_KEY'])
#     try:
#         data = s.loads(token)
#     except SignatureExpired:
#         return None  # valid token, but expired
#     except BadSignature:
#         return None  # invalid token
#
#     if 'id' not in data:
#         return None
#
#     return data['id']