Пример #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
    )
Пример #2
0
class LTINonce(DefaultTableMixin, WriteTrackingMixin):
    __tablename__ = 'lti_nonce'

    # table columns
    lti_consumer_id = db.Column(db.Integer,
                                db.ForeignKey("lti_consumer.id",
                                              ondelete="CASCADE"),
                                nullable=False)
    oauth_nonce = db.Column(db.String(191), nullable=False)
    oauth_timestamp = db.Column(db.TIMESTAMP, nullable=False)

    # relationships
    # lti_consumer via LTIConsumer Model

    # hybrid and other functions
    @classmethod
    def is_valid_nonce(cls, oauth_consumer_key, oauth_nonce, oauth_timestamp):
        from . import LTIConsumer
        lti_consumer = LTIConsumer.get_by_consumer_key(oauth_consumer_key)

        if lti_consumer == None:
            return False

        try:
            # is valid if it is unique on consumer, nonce, and timestamp
            # validate based on insert passing the unique check or not
            lti_nonce = LTINonce(lti_consumer_id=lti_consumer.id,
                                 oauth_nonce=oauth_nonce,
                                 oauth_timestamp=datetime.fromtimestamp(
                                     float(oauth_timestamp)))
            db.session.add(lti_nonce)
            db.session.commit()
        except exc.IntegrityError:
            db.session.rollback()
            return False

        return True

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

    __table_args__ = (
        # prevent duplicate user in course
        db.UniqueConstraint('lti_consumer_id',
                            'oauth_nonce',
                            'oauth_timestamp',
                            name='_unique_lti_consumer_nonce_and_timestamp'),
        DefaultTableMixin.default_table_args)
Пример #3
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__()
Пример #4
0
class ThirdPartyUser(DefaultTableMixin, WriteTrackingMixin):
    __tablename__ = 'third_party_user'

    # table columns
    third_party_type = db.Column(EnumType(ThirdPartyType,
                                          name="third_party_type"),
                                 nullable=False)
    unique_identifier = db.Column(db.String(255), nullable=False)
    user_id = db.Column(db.Integer,
                        db.ForeignKey("user.id", ondelete="CASCADE"),
                        nullable=False)
    _params = db.Column(db.Text)

    # relationships
    # user via User Model

    # hyprid and other functions

    @property
    def params(self):
        return json.loads(self._params) if self._params else None

    @params.setter
    def params(self, params):
        self._params = json.dumps(params) if params else None

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

    __table_args__ = (
        # prevent duplicate user in course
        db.UniqueConstraint(
            'third_party_type',
            'unique_identifier',
            name='_unique_third_party_type_and_unique_identifier'),
        DefaultTableMixin.default_table_args)
Пример #5
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
    )
Пример #6
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)
Пример #7
0
class CourseGrade(DefaultTableMixin, WriteTrackingMixin):
    __tablename__ = 'course_grade'

    # table columns
    user_id = db.Column(db.Integer,
                        db.ForeignKey('user.id', ondelete="CASCADE"),
                        nullable=False)
    course_id = db.Column(db.Integer,
                          db.ForeignKey('course.id', ondelete="CASCADE"),
                          nullable=False)
    grade = db.Column(db.Float, default=0, nullable=False)

    # relationships
    # user via User Model
    # course via Course Model

    # hybrid and other functions
    @classmethod
    def get_course_grades(cls, course):
        return CourseGrade.query \
            .filter_by(course_id=course.id) \
            .all()

    @classmethod
    def get_user_course_grade(cls, course, user):
        return CourseGrade.query \
            .filter_by(
                user_id=user.id,
                course_id=course.id
            ) \
            .one_or_none()

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

    __table_args__ = (
        # prevent duplicate user in course
        db.UniqueConstraint('course_id',
                            'user_id',
                            name='_unique_user_and_course'),
        DefaultTableMixin.default_table_args)

    @classmethod
    def calculate_grade(cls, course, user):
        from . import AssignmentGrade, LTIOutcome, CourseRole

        user_is_student = False

        for course_user in course.user_courses:
            if course_user.user_id != user.id:
                continue
            user_is_student = course_user.course_role == CourseRole.student
            break

        assignment_ids = [
            assignment.id for assignment in course.assignments
            if assignment.active
        ]

        # skip if there aren't any assignments
        if len(assignment_ids) == 0:
            CourseGrade.query \
                .filter_by(course_id=course.id) \
                .filter(CourseGrade.user_id.in_(student_ids)) \
                .delete()
            LTIOutcome.update_course_users_grade(course, student_ids)
            return

        elif not user_is_student:
            return

        # collect all of the students assignment grades
        student_assignment_grades = {
            # default grade of 0 in case assignment_grade record is missing
            assignment_id: 0.0
            for assignment_id in assignment_ids
        }

        assignment_grades = AssignmentGrade.query \
            .filter_by(user_id=user.id) \
            .filter(AssignmentGrade.assignment_id.in_(assignment_ids)) \
            .all()
        for assignment_grade in assignment_grades:
            student_assignment_grades[
                assignment_grade.assignment_id] = assignment_grade.grade

        grade = _calculate_course_grade(course, student_assignment_grades)

        course_grade = CourseGrade.get_user_course_grade(course, user)
        if course_grade == None:
            course_grade = CourseGrade(user_id=user.id, course_id=course.id)

        course_grade.grade = grade

        db.session.add(course_grade)
        db.session.commit()

        LTIOutcome.update_course_user_grade(course, user.id)

    @classmethod
    def calculate_group_grade(cls, course, group):
        from . import CourseRole, AssignmentGrade, LTIOutcome

        student_ids = [course_user.user_id
            for course_user in course.user_courses
            if course_user.course_role == CourseRole.student and \
            course_user.group_id == group.id]

        assignment_ids = [
            assignment.id for assignment in course.assignments
            if assignment.active
        ]

        # skip if there aren't any assignments
        if len(student_ids) == 0:
            return

        if len(assignment_ids) == 0:
            CourseGrade.query \
                .filter_by(course_id=course.id) \
                .filter(CourseGrade.user_id.in_(student_ids)) \
                .delete()
            LTIOutcome.update_course_users_grade(course, student_ids)
            return

        # collect all of the students assignment grades
        student_assignment_grades = {}
        for student_id in student_ids:
            student_assignment_grades[student_id] = {}
            for assignment_id in assignment_ids:
                student_assignment_grades[student_id][assignment_id] = 0.0

        assignment_grades = AssignmentGrade.query \
            .filter(AssignmentGrade.assignment_id.in_(assignment_ids)) \
            .filter(AssignmentGrade.user_id.in_(student_ids)) \
            .all()
        for assignment_grade in assignment_grades:
            student_assignment_grades[assignment_grade.user_id][
                assignment_grade.assignment_id] = assignment_grade.grade

        course_grades = CourseGrade.query \
            .filter(CourseGrade.user_id.in_(student_ids)) \
            .all()
        new_course_grades = []
        for student_id in student_ids:
            grade = _calculate_course_grade(
                course, student_assignment_grades[student_id])

            course_grade = next((course_grade for course_grade in course_grades
                                 if course_grade.user_id == student_id), None)

            if course_grade == None:
                course_grade = CourseGrade(user_id=student_id,
                                           course_id=course.id)
                new_course_grades.append(course_grade)

            course_grade.grade = grade

        db.session.add_all(course_grades + new_course_grades)
        db.session.commit()

        LTIOutcome.update_course_users_grade(course, student_ids)

    @classmethod
    def calculate_grades(cls, course):
        from . import CourseRole, AssignmentGrade, LTIOutcome

        student_ids = [
            course_user.user_id for course_user in course.user_courses
            if course_user.course_role == CourseRole.student
        ]

        assignment_ids = [
            assignment.id for assignment in course.assignments
            if assignment.active
        ]

        # skip if there aren't any assignments
        if len(student_ids) == 0 or len(assignment_ids) == 0:
            CourseGrade.query \
                .filter_by(course_id=course.id) \
                .delete()
            LTIOutcome.update_course_grades(course)
            return

        # collect all of the students assignment grades
        student_assignment_grades = {}
        for student_id in student_ids:
            student_assignment_grades[student_id] = {}
            for assignment_id in assignment_ids:
                student_assignment_grades[student_id][assignment_id] = 0.0

        assignment_grades = AssignmentGrade.query \
            .filter(AssignmentGrade.assignment_id.in_(assignment_ids)) \
            .filter(AssignmentGrade.user_id.in_(student_ids)) \
            .all()
        for assignment_grade in assignment_grades:
            student_assignment_grades[assignment_grade.user_id][
                assignment_grade.assignment_id] = assignment_grade.grade

        course_grades = CourseGrade.get_course_grades(course)
        new_course_grades = []
        for student_id in student_ids:
            grade = _calculate_course_grade(
                course, student_assignment_grades[student_id])

            course_grade = next((course_grade for course_grade in course_grades
                                 if course_grade.user_id == student_id), None)

            if course_grade == None:
                course_grade = CourseGrade(user_id=student_id,
                                           course_id=course.id)
                new_course_grades.append(course_grade)

            course_grade.grade = grade

        db.session.add_all(course_grades + new_course_grades)
        db.session.commit()

        LTIOutcome.update_course_grades(course)
Пример #8
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)
Пример #9
0
class AnswerCriterionScore(DefaultTableMixin, WriteTrackingMixin):
    __tablename__ = 'answer_criterion_score'

    # table columns
    assignment_id = db.Column(db.Integer,
                              db.ForeignKey('assignment.id',
                                            ondelete="CASCADE"),
                              nullable=False)
    answer_id = db.Column(db.Integer,
                          db.ForeignKey('answer.id', ondelete="CASCADE"),
                          nullable=False)
    criterion_id = db.Column(db.Integer,
                             db.ForeignKey('criterion.id', ondelete="CASCADE"),
                             nullable=False)
    """
    Comparative Judgement
    score = expected score / number of opponents
    score_variable1 = expected score
    score_variable2 = None

    Elo Rating
    score = Elo rating
    score_variable1 = Elo rating
    score_variable2 = None

    True Skill Rating
    score = Rating's Mu - (Default Mu / Default Sigma) * Rating's Sigma
    score_variable1 = Rating's Mu
    score_variable2 = Rating's Sigma
    """

    scoring_algorithm = db.Column(EnumType(ScoringAlgorithm),
                                  nullable=True,
                                  default=ScoringAlgorithm.elo)
    score = db.Column(db.Float, default=0, nullable=False, index=True)
    variable1 = db.Column(db.Float, nullable=True)
    variable2 = db.Column(db.Float, nullable=True)
    rounds = db.Column(db.Integer, default=0, nullable=False)
    wins = db.Column(db.Integer, default=0, nullable=False)
    loses = db.Column(db.Integer, default=0, nullable=False)
    opponents = db.Column(db.Integer, default=0, nullable=False)

    # relationships
    # assignment via Assignment Model
    # answer via Answer Model
    # criterion via Criterion Model

    # hybrid and other functions
    criterion_uuid = association_proxy('criterion', 'uuid')

    def convert_to_scored_object(self):
        return ScoredObject(key=self.answer_id,
                            score=self.score,
                            variable1=self.variable1,
                            variable2=self.variable2,
                            rounds=self.rounds,
                            wins=self.wins,
                            loses=self.loses,
                            opponents=self.opponents)

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

        s_alias = cls.__table__.alias()
        cls.normalized_score = column_property(
            select([
                (cls.score - func.min(s_alias.c.score)) /
                (func.max(s_alias.c.score) - func.min(s_alias.c.score)) * 100
            ]).where(
                and_(
                    s_alias.c.criterion_id == cls.criterion_id,
                    s_alias.c.assignment_id == cls.assignment_id,
                )).scalar_subquery())

    __table_args__ = (db.UniqueConstraint('answer_id',
                                          'criterion_id',
                                          name='_unique_answer_and_criterion'),
                      DefaultTableMixin.default_table_args)
Пример #10
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)
Пример #11
0
class LTIMembership(DefaultTableMixin, WriteTrackingMixin):
    __tablename__ = 'lti_membership'

    # table columns
    lti_context_id = db.Column(db.Integer,
                               db.ForeignKey("lti_context.id",
                                             ondelete="CASCADE"),
                               nullable=False)
    lti_user_id = db.Column(db.Integer,
                            db.ForeignKey("lti_user.id", ondelete="CASCADE"),
                            nullable=False)
    roles = db.Column(db.String(255), nullable=True)
    lis_result_sourcedid = db.Column(db.String(255), nullable=True)
    lis_result_sourcedids = db.Column(db.Text, nullable=True)
    course_role = db.Column(Enum(CourseRole, name="course_role"),
                            nullable=False)

    compair_course_id = association_proxy('lti_context', 'compair_course_id')
    compair_user_id = association_proxy('lti_user', 'compair_user_id')

    # relationships
    # lti_conext via LTIContext Model
    # lti_user via LTIUser Model

    # hybrid and other functions
    context_id = association_proxy('lti_context', 'context_id')
    user_id = association_proxy('lti_user', 'user_id')

    @classmethod
    def update_membership_for_course(cls, course):
        from . import MembershipNoValidContextsException

        valid_membership_contexts = [
            lti_context for lti_context in course.lti_contexts
            if lti_context.membership_enabled
        ]

        if len(valid_membership_contexts) == 0:
            raise MembershipNoValidContextsException

        lti_members = []
        for lti_context in valid_membership_contexts:
            members = LTIMembership._get_membership(lti_context)
            lti_members += LTIMembership._update_membership_for_context(
                lti_context, members)

        LTIMembership._update_enrollment_for_course(course.id, lti_members)

    @classmethod
    def _update_membership_for_context(cls, lti_context, members):
        from compair.models import SystemRole, CourseRole, \
            LTIUser, LTIUserResourceLink

        lti_resource_links = lti_context.lti_resource_links

        # remove old membership rows
        LTIMembership.query \
            .filter_by(
                lti_context_id=lti_context.id
            ) \
            .delete()

        # retrieve existing lti_user rows
        user_ids = []
        for member in members:
            user_ids.append(member['user_id'])

        existing_lti_users = []
        if len(user_ids) > 0:
            existing_lti_users = LTIUser.query \
                .filter(and_(
                    LTIUser.lti_consumer_id == lti_context.lti_consumer_id,
                    LTIUser.user_id.in_(user_ids)
                )) \
                .all()

        # get existing lti_user_resource_link if there there exists lti users and known resource links for context
        existing_lti_user_resource_links = []
        if len(existing_lti_users) > 0 and len(lti_resource_links) > 0:
            lti_resource_link_ids = [
                lti_resource_link.id
                for lti_resource_link in lti_resource_links
            ]
            existing_lti_user_ids = [
                existing_lti_user.id
                for existing_lti_user in existing_lti_users
            ]
            existing_lti_user_resource_links = LTIUserResourceLink.query \
                .filter(and_(
                    LTIUserResourceLink.lti_resource_link_id.in_(lti_resource_link_ids),
                    LTIUserResourceLink.lti_user_id.in_(existing_lti_user_ids)
                )) \
                .all()

        new_lti_users = []
        new_lti_user_resource_links = []
        lti_memberships = []
        for member in members:
            # get lti user if exists
            lti_user = next((lti_user for lti_user in existing_lti_users
                             if lti_user.user_id == member.get('user_id')),
                            None)
            roles = member.get('roles')
            has_instructor_role = any(
                role.lower().find("instructor") >= 0 or role.lower().find(
                    "faculty") >= 0 or role.lower().find("staff") >= 0
                for role in roles)
            has_ta_role = any(role.lower().find("teachingassistant") >= 0
                              for role in roles)

            # create lti user if doesn't exist
            if not lti_user:
                lti_user = LTIUser(lti_consumer_id=lti_context.lti_consumer_id,
                                   user_id=member.get('user_id'))
                new_lti_users.append(lti_user)

            # update/set fields if needed
            lti_user.system_role = SystemRole.instructor if has_instructor_role else SystemRole.student
            lti_user.lis_person_name_given = member.get('person_name_given')
            lti_user.lis_person_name_family = member.get('person_name_family')
            lti_user.lis_person_name_full = member.get('person_name_full')
            lti_user.handle_fullname_with_missing_first_and_last_name()

            lti_user.lis_person_contact_email_primary = member.get(
                'person_contact_email_primary')
            lti_user.lis_person_sourcedid = member.get('lis_person_sourcedid')

            if member.get('global_unique_identifier'):
                lti_user.global_unique_identifier = member.get(
                    'global_unique_identifier')

            if member.get('student_number'):
                lti_user.student_number = member.get('student_number')

            if not lti_user.is_linked_to_user(
            ) and lti_user.global_unique_identifier:
                lti_user.generate_or_link_user_account()

            course_role = CourseRole.student
            if has_instructor_role:
                course_role = CourseRole.instructor
            elif has_ta_role:
                course_role = CourseRole.teaching_assistant

            # create new lti membership row
            lti_membership = LTIMembership(
                lti_user=lti_user,
                lti_context=lti_context,
                roles=text_type(roles),
                lis_result_sourcedid=member.get('lis_result_sourcedid'),
                lis_result_sourcedids=json.dumps(
                    member.get('lis_result_sourcedids'))
                if member.get('lis_result_sourcedids') else None,
                course_role=course_role)
            lti_memberships.append(lti_membership)

            # if membership includes lis_result_sourcedids, create/update lti user resource links
            if member.get('lis_result_sourcedids'):
                for lis_result_sourcedid_set in member.get(
                        'lis_result_sourcedids'):
                    lti_resource_link = next(
                        lti_resource_link
                        for lti_resource_link in lti_resource_links
                        if lti_resource_link.resource_link_id ==
                        lis_result_sourcedid_set['resource_link_id'])

                    if not lti_resource_link:
                        continue

                    lti_user_resource_link = None
                    if len(existing_lti_user_resource_links
                           ) > 0 and lti_user.id:
                        # get lti user resource link if exists
                        lti_user_resource_link = next(
                            (lti_user_resource_link for lti_user_resource_link
                             in existing_lti_user_resource_links
                             if lti_user_resource_link.lti_user_id == lti_user.
                             id and lti_user_resource_link.lti_resource_link_id
                             == lti_resource_link.id), None)

                    # create new lti user resource link if needed
                    if not lti_user_resource_link:
                        lti_user_resource_link = LTIUserResourceLink(
                            lti_resource_link=lti_resource_link,
                            lti_user=lti_user,
                            roles=text_type(roles),
                            course_role=course_role)
                        new_lti_user_resource_links.append(
                            lti_user_resource_link)

                    # finally update the lis_result_sourcedid value for the user resource link
                    lti_user_resource_link.lis_result_sourcedid = lis_result_sourcedid_set[
                        'lis_result_sourcedid']

        db.session.add_all(new_lti_users)
        db.session.add_all(existing_lti_users)
        db.session.add_all(lti_memberships)
        db.session.add_all(new_lti_user_resource_links)
        db.session.add_all(existing_lti_user_resource_links)

        # save new lti users
        db.session.commit()

        return lti_memberships

    @classmethod
    def _update_enrollment_for_course(cls, course_id, lti_members):
        from compair.models import UserCourse

        user_courses = UserCourse.query \
            .filter_by(course_id=course_id) \
            .all()
        new_user_courses = []

        for lti_member in lti_members:
            if lti_member.compair_user_id != None:
                user_course = next(
                    (user_course for user_course in user_courses
                     if user_course.user_id == lti_member.compair_user_id),
                    None)
                # add new user_course if doesn't exist
                if user_course == None:
                    user_course = UserCourse(
                        course_id=course_id,
                        user_id=lti_member.compair_user_id,
                        course_role=lti_member.course_role)
                    new_user_courses.append(user_course)

                # update user_course role
                else:
                    user_course.course_role = lti_member.course_role

                # update user profile if needed
                lti_member.lti_user.update_user_profile()

        db.session.add_all(new_user_courses)
        db.session.commit()

        # set user_course to dropped role if missing from membership results and not current user
        for user_course in user_courses:
            # never unenrol current_user
            if current_user and current_user.is_authenticated and user_course.user_id == current_user.id:
                continue

            lti_member = next(
                (lti_member for lti_member in lti_members
                 if user_course.user_id == lti_member.compair_user_id), None)
            if lti_member == None:
                user_course.course_role = CourseRole.dropped

        db.session.commit()

    @classmethod
    def _get_membership(cls, lti_context):
        if lti_context.membership_ext_enabled:
            return LTIMembership._get_membership_ext(lti_context)
        elif lti_context.membership_service_enabled:
            return LTIMembership._get_membership_service(lti_context)
        return []

    @classmethod
    def _get_membership_ext(cls, lti_context):
        lti_consumer = lti_context.lti_consumer
        memberships_id = lti_context.ext_ims_lis_memberships_id
        memberships_url = lti_context.ext_ims_lis_memberships_url
        params = {
            'id': memberships_id,
            'lti_message_type': 'basic-lis-readmembershipsforcontext',
            'lti_version': 'LTI-1p0',
            'oauth_callback': 'about:blank'
        }
        request = requests.Request('POST', memberships_url,
                                   data=params).prepare()
        sign = OAuth1(lti_consumer.oauth_consumer_key,
                      lti_consumer.oauth_consumer_secret,
                      signature_type=SIGNATURE_TYPE_BODY,
                      signature_method=SIGNATURE_HMAC)
        signed_request = sign(request)
        params = parse_qs(signed_request.body.decode('utf-8'))

        data = LTIMembership._post_membership_request(memberships_url, params)
        root = ElementTree.fromstring(data.encode('utf-8'))

        codemajor = root.find('statusinfo/codemajor')
        if codemajor is not None and codemajor.text in [
                'Failure', 'Unsupported'
        ]:
            raise MembershipInvalidRequestException

        if root.find('memberships') == None or len(
                root.findall('memberships/member')) == 0:
            raise MembershipNoResultsException

        members = []
        for record in root.findall('memberships/member'):
            roles_text = record.findtext('roles')

            member = {
                'user_id':
                record.findtext('user_id'),
                'roles':
                roles_text.split(",") if roles_text != None else [],
                'global_unique_identifier':
                None,
                'student_number':
                None,
                'lis_result_sourcedid':
                record.findtext('lis_result_sourcedid'),
                'person_contact_email_primary':
                record.findtext('person_contact_email_primary'),
                'person_name_given':
                record.findtext('person_name_given'),
                'person_name_family':
                record.findtext('person_name_family'),
                'person_name_full':
                record.findtext('person_name_full')
            }

            # find global unique identifier if available
            if lti_consumer.global_unique_identifier_param and record.findtext(
                    lti_consumer.global_unique_identifier_param):
                member['global_unique_identifier'] = record.findtext(
                    lti_consumer.global_unique_identifier_param)
                if lti_consumer.custom_param_regex_sanitizer and lti_consumer.global_unique_identifier_param.startswith(
                        'custom_'):
                    regex = re.compile(
                        lti_consumer.custom_param_regex_sanitizer)
                    member['global_unique_identifier'] = regex.sub(
                        '', member['global_unique_identifier'])
                    if member['global_unique_identifier'] == '':
                        member['global_unique_identifier'] = None

            # find student number if available
            if lti_consumer.student_number_param and record.findtext(
                    lti_consumer.student_number_param):
                member['student_number'] = record.findtext(
                    lti_consumer.student_number_param)
                if lti_consumer.custom_param_regex_sanitizer and lti_consumer.student_number_param.startswith(
                        'custom_'):
                    regex = re.compile(
                        lti_consumer.custom_param_regex_sanitizer)
                    member['student_number'] = regex.sub(
                        '', member['student_number'])
                    if member['student_number'] == '':
                        member['student_number'] = None

            members.append(member)
        return members

    @classmethod
    def _get_membership_service(cls, lti_context):
        # possible parameters are role, lis_result_sourcedid, limit
        lti_consumer = lti_context.lti_consumer
        memberships_url = lti_context.custom_context_memberships_url
        lti_resource_links = lti_context.lti_resource_links

        members = []

        while True:
            headers = {
                'Accept': 'application/vnd.ims.lis.v2.membershipcontainer+json'
            }
            request = requests.Request('GET', memberships_url,
                                       headers=headers).prepare()
            # Note: need to use LTIMemerbshipServiceOauthClient since normal client will
            #       not include oauth_body_hash if there is not content type or the body is None
            sign = OAuth1(lti_consumer.oauth_consumer_key,
                          lti_consumer.oauth_consumer_secret,
                          signature_type=SIGNATURE_TYPE_AUTH_HEADER,
                          signature_method=SIGNATURE_HMAC,
                          client_class=LTIMemerbshipServiceOauthClient)
            # sign = OAuth1(lti_consumer.oauth_consumer_key, lti_consumer.oauth_consumer_secret,
            #     signature_type=SIGNATURE_TYPE_AUTH_HEADER, signature_method=SIGNATURE_HMAC)
            signed_request = sign(request)
            headers = signed_request.headers
            data = LTIMembership._get_membership_request(
                memberships_url, headers)

            if data == None:
                break

            membership = data['pageOf']['membershipSubject']['membership']

            if len(membership) == 0:
                raise MembershipNoResultsException

            for record in membership:
                if record.get('status').find("Inactive") >= 0:
                    continue
                member = {
                    'user_id': record['member'].get('userId'),
                    'roles': record.get('role'),
                    'lis_person_sourcedid': record['member'].get('sourcedId'),
                    'global_unique_identifier': None,
                    'student_number': None,
                    'person_contact_email_primary':
                    record['member'].get('email'),
                    'person_name_given': record['member'].get('givenName'),
                    'person_name_family': record['member'].get('familyName'),
                    'person_name_full': record['member'].get('name')
                }

                if (lti_consumer.global_unique_identifier_param
                        or lti_consumer.student_number_param
                    ) and 'message' in record:
                    for message in record['message']:
                        if not message[
                                'message_type'] == 'basic-lti-launch-request':
                            continue

                        # find global unique identifier if present in membership result
                        if lti_consumer.global_unique_identifier_param:
                            # check if global_unique_identifier_param is a basic lti parameter
                            if lti_consumer.global_unique_identifier_param in message:
                                member['global_unique_identifier'] = message[
                                    lti_consumer.
                                    global_unique_identifier_param]
                            # check if global_unique_identifier_param is an extension and present
                            elif lti_consumer.global_unique_identifier_param.startswith(
                                    'ext_'):
                                ext_global_unique_identifier = lti_consumer.global_unique_identifier_param[
                                    len('ext_'):]
                                if ext_global_unique_identifier in message[
                                        'ext']:
                                    member[
                                        'global_unique_identifier'] = message[
                                            'ext'][
                                                ext_global_unique_identifier]
                            # check if global_unique_identifier_param is an custom attribute and present
                            elif lti_consumer.global_unique_identifier_param.startswith(
                                    'custom_'):
                                custom_global_unique_identifier = lti_consumer.global_unique_identifier_param[
                                    len('custom_'):]
                                if custom_global_unique_identifier in message[
                                        'custom']:
                                    member[
                                        'global_unique_identifier'] = message[
                                            'custom'][
                                                custom_global_unique_identifier]

                        # get student number if present in membership result
                        if lti_consumer.student_number_param:
                            # check if student_number_param is a basic lti parameter
                            if lti_consumer.student_number_param in message:
                                member['student_number'] = message[
                                    lti_consumer.student_number_param]
                            # check if student_number_param is an extension and present
                            elif lti_consumer.student_number_param.startswith(
                                    'ext_'):
                                ext_student_number = lti_consumer.student_number_param[
                                    len('ext_'):]
                                if ext_student_number in message['ext']:
                                    member['student_number'] = message['ext'][
                                        ext_student_number]
                            # check if student_number_param is an custom attribute and present
                            elif lti_consumer.student_number_param.startswith(
                                    'custom_'):
                                custom_student_number = lti_consumer.student_number_param[
                                    len('custom_'):]
                                if custom_student_number in message['custom']:
                                    member['student_number'] = message[
                                        'custom'][custom_student_number]

                members.append(member)
            # check if another page or else finish
            memberships_url = data.get('nextPage')
            if not memberships_url:
                break

        # get lis_result_sourcedid for all resource links known to the system
        for lti_resource_link in lti_resource_links:
            memberships_url = lti_context.custom_context_memberships_url
            # add role t0 membership url query string
            memberships_url += "?" if memberships_url.find("?") == -1 else "&"
            memberships_url += "role=Learner"
            # add rlid to membership url query string
            memberships_url += "&rlid={}".format(
                lti_resource_link.resource_link_id)

            while True:
                headers = {
                    'Accept':
                    'application/vnd.ims.lis.v2.membershipcontainer+json'
                }
                request = requests.Request('GET',
                                           memberships_url,
                                           headers=headers).prepare()
                # Note: need to use LTIMemerbshipServiceOauthClient since normal client will
                #       not include oauth_body_hash if there is not content type or the body is None
                sign = OAuth1(lti_consumer.oauth_consumer_key,
                              lti_consumer.oauth_consumer_secret,
                              signature_type=SIGNATURE_TYPE_AUTH_HEADER,
                              signature_method=SIGNATURE_HMAC,
                              client_class=LTIMemerbshipServiceOauthClient)
                # sign = OAuth1(lti_consumer.oauth_consumer_key, lti_consumer.oauth_consumer_secret,
                #     signature_type=SIGNATURE_TYPE_AUTH_HEADER, signature_method=SIGNATURE_HMAC)
                signed_request = sign(request)
                headers = signed_request.headers
                data = LTIMembership._get_membership_request(
                    memberships_url, headers)

                if data == None:
                    break

                membership = data['pageOf']['membershipSubject']['membership']

                if len(membership) == 0:
                    continue

                for record in membership:
                    if record.get('status').find("Inactive") >= 0:
                        continue

                    member = next(
                        (member for member in members
                         if member['user_id'] == record['member'].get('userId')
                         ), None)

                    if not member or not 'message' in record:
                        continue

                    for message in record['message']:
                        if not message[
                                'message_type'] == 'basic-lti-launch-request' or not 'lis_result_sourcedid' in message:
                            continue

                        lis_result_sourcedid_array = member.setdefault(
                            'lis_result_sourcedids', [])
                        lis_result_sourcedid_array.append({
                            'resource_link_id':
                            lti_resource_link.resource_link_id,
                            'lis_result_sourcedid':
                            message['lis_result_sourcedid']
                        })

                # check if another page or else finish
                memberships_url = data.get('nextPage')
                if not memberships_url:
                    break

        return members

    @classmethod
    def _post_membership_request(cls, memberships_url, params):
        verify = current_app.config.get('ENFORCE_SSL', True)
        return requests.post(memberships_url, data=params, verify=verify).text

    @classmethod
    def _get_membership_request(cls, memberships_url, headers=None):
        verify = current_app.config.get('ENFORCE_SSL', True)
        rv = requests.get(memberships_url, headers=headers, verify=verify)
        if rv.content:
            return rv.json()
        return None

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

    __table_args__ = (
        # prevent duplicate resource link in consumer
        db.UniqueConstraint('lti_context_id',
                            'lti_user_id',
                            name='_unique_lti_context_and_lti_user'),
        DefaultTableMixin.default_table_args)
Пример #12
0
class LTIUserResourceLink(DefaultTableMixin, WriteTrackingMixin):
    __tablename__ = 'lti_user_resource_link'

    # table columns
    lti_resource_link_id = db.Column(db.Integer, db.ForeignKey("lti_resource_link.id", ondelete="CASCADE"),
        nullable=False)
    lti_user_id = db.Column(db.Integer, db.ForeignKey("lti_user.id", ondelete="CASCADE"),
        nullable=False)
    roles = db.Column(db.String(255), nullable=True)
    lis_result_sourcedid = db.Column(db.String(255), nullable=True)
    course_role = db.Column(EnumType(CourseRole),
        nullable=False)

    # relationships
    # lti_user via LTIUser Model
    # lti_resource_link via LTIResourceLink Model

    # hybrid and other functions
    context_id = association_proxy('lti_resource_link', 'context_id')
    resource_link_id = association_proxy('lti_resource_link', 'resource_link_id')
    user_id = association_proxy('lti_user', 'user_id')
    compair_user_id = association_proxy('lti_user', 'compair_user_id')

    @classmethod
    def get_by_lti_resource_link_id_and_lti_user_id(cls, lti_resource_link_id, lti_user_id):
        return LTIUserResourceLink.query \
            .filter_by(
                lti_resource_link_id=lti_resource_link_id,
                lti_user_id=lti_user_id
            ) \
            .one_or_none()

    @classmethod
    def get_by_tool_provider(cls, lti_resource_link, lti_user, tool_provider):
        from . import CourseRole

        lti_user_resource_link = LTIUserResourceLink.get_by_lti_resource_link_id_and_lti_user_id(
            lti_resource_link.id, lti_user.id)

        if lti_user_resource_link == None:
            lti_user_resource_link = LTIUserResourceLink(
                lti_resource_link_id=lti_resource_link.id,
                lti_user_id=lti_user.id
            )
            db.session.add(lti_user_resource_link)

        lti_user_resource_link.roles = text_type(tool_provider.roles)
        lti_user_resource_link.lis_result_sourcedid = tool_provider.lis_result_sourcedid

        # set course role every time
        if tool_provider.roles and any(
                    role.lower().find("instructor") >= 0 or
                    role.lower().find("faculty") >= 0 or
                    role.lower().find("staff") >= 0
                    for role in tool_provider.roles
                ):
            lti_user_resource_link.course_role = CourseRole.instructor
        elif tool_provider.roles and any(role.lower().find("teachingassistant") >= 0 for role in tool_provider.roles):
            lti_user_resource_link.course_role = CourseRole.teaching_assistant
        else:
            lti_user_resource_link.course_role = CourseRole.student

        db.session.commit()

        return lti_user_resource_link

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

    __table_args__ = (
        # prevent duplicate resource link in consumer
        db.UniqueConstraint('lti_resource_link_id', 'lti_user_id', name='_unique_lti_resource_link_and_lti_user'),
        DefaultTableMixin.default_table_args
    )
Пример #13
0
class ThirdPartyUser(DefaultTableMixin, UUIDMixin, WriteTrackingMixin):
    __tablename__ = 'third_party_user'

    # table columns
    third_party_type = db.Column(EnumType(ThirdPartyType), nullable=False)
    unique_identifier = db.Column(db.String(191), nullable=False)
    user_id = db.Column(db.Integer, db.ForeignKey("user.id", ondelete="CASCADE"), nullable=False)
    _params = db.Column(db.Text)

    # relationships
    # user via User Model
    user_uuid = association_proxy('user', 'uuid')

    # hybrid and other functions

    @property
    def params(self):
        return json.loads(self._params) if self._params else None

    @params.setter
    def params(self, params):
        self._params = json.dumps(params) if params else None

    @property
    def global_unique_identifier(self):
        if self.params:
            global_unique_identifier_attribute = None
            if self.third_party_type == ThirdPartyType.cas:
                global_unique_identifier_attribute = current_app.config.get('CAS_GLOBAL_UNIQUE_IDENTIFIER_FIELD')
            elif self.third_party_type == ThirdPartyType.saml:
                global_unique_identifier_attribute = current_app.config.get('SAML_GLOBAL_UNIQUE_IDENTIFIER_FIELD')

            if global_unique_identifier_attribute and global_unique_identifier_attribute in self.params:
                global_unique_identifier = self.params.get(global_unique_identifier_attribute)
                if isinstance(global_unique_identifier, list):
                    global_unique_identifier = global_unique_identifier[0] if len(global_unique_identifier) > 0 else None
                return global_unique_identifier

        return None

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

    __table_args__ = (
        # prevent duplicate user in course
        db.UniqueConstraint('third_party_type', 'unique_identifier', name='_unique_third_party_type_and_unique_identifier'),
        DefaultTableMixin.default_table_args
    )

    @classmethod
    def get_by_uuid_or_404(cls, model_uuid, joinedloads=[], title=None, message=None):
        if not title:
            title = "Third Party User Unavailable"
        if not message:
            message = "Sorry, this third party user was deleted or is no longer accessible."
        return super(cls, cls).get_by_uuid_or_404(model_uuid, joinedloads, title, message)

    def generate_or_link_user_account(self):
        from . import SystemRole, User

        if not self.user:
            # check if global_unique_identifier user already exists
            if self.global_unique_identifier:
                self.user = User.query \
                    .filter_by(global_unique_identifier=self.global_unique_identifier) \
                    .one_or_none()

            if not self.user:
                self.user = User(
                    username=None,
                    password=None,
                    system_role=self._get_system_role(),
                    global_unique_identifier=self.global_unique_identifier
                )
                self._sync_name()
                self._sync_email()
                if self.user.system_role == SystemRole.student:
                    self._sync_student_number()

                # instructors can have their display names set to their full name by default
                if self.user.system_role != SystemRole.student and self.user.fullname != None:
                    self.user.displayname = self.user.fullname
                else:
                    self.user.displayname = display_name_generator(self.user.system_role.value)

    def update_user_profile(self):
        if self.user and self.user.system_role == SystemRole.student and self.params:
            # overwrite first/last name if student not allowed to change it
            if not current_app.config.get('ALLOW_STUDENT_CHANGE_NAME'):
                self._sync_name()

            # overwrite email if student not allowed to change it
            if not current_app.config.get('ALLOW_STUDENT_CHANGE_EMAIL'):
                self._sync_email()

            # overwrite student number if student not allowed to change it
            if not current_app.config.get('ALLOW_STUDENT_CHANGE_STUDENT_NUMBER'):
                self._sync_student_number()

    def _sync_name(self):
        if self.params:
            firstname_attribute = lastname_attribute = None
            if self.third_party_type == ThirdPartyType.cas:
                firstname_attribute = current_app.config.get('CAS_ATTRIBUTE_FIRST_NAME')
                lastname_attribute = current_app.config.get('CAS_ATTRIBUTE_LAST_NAME')
            elif self.third_party_type == ThirdPartyType.saml:
                firstname_attribute = current_app.config.get('SAML_ATTRIBUTE_FIRST_NAME')
                lastname_attribute = current_app.config.get('SAML_ATTRIBUTE_LAST_NAME')

            if firstname_attribute and firstname_attribute in self.params:
                first_name = self.params.get(firstname_attribute)
                if isinstance(first_name, list):
                    first_name = first_name[0] if len(first_name) > 0 else None
                self.user.firstname = first_name

            if lastname_attribute and lastname_attribute in self.params:
                last_name = self.params.get(lastname_attribute)
                if isinstance(last_name, list):
                    last_name = last_name[0] if len(last_name) > 0 else None
                self.user.lastname = last_name

    def _sync_email(self):
        if self.params:
            email_attribute = None
            if self.third_party_type == ThirdPartyType.cas:
                email_attribute = current_app.config.get('CAS_ATTRIBUTE_EMAIL')
            elif self.third_party_type == ThirdPartyType.saml:
                email_attribute = current_app.config.get('SAML_ATTRIBUTE_EMAIL')

            if email_attribute and email_attribute in self.params:
                email = self.params.get(email_attribute)
                if isinstance(email, list):
                    email = email[0] if len(email) > 0 else None
                self.user.email = email

    def _sync_student_number(self):
        if self.params:
            student_number_attribute = None
            if self.third_party_type == ThirdPartyType.cas:
                student_number_attribute = current_app.config.get('CAS_ATTRIBUTE_STUDENT_NUMBER')
            elif self.third_party_type == ThirdPartyType.saml:
                student_number_attribute = current_app.config.get('SAML_ATTRIBUTE_STUDENT_NUMBER')

            if student_number_attribute and student_number_attribute in self.params:
                student_number = self.params.get(student_number_attribute)
                if isinstance(student_number, list):
                    student_number = student_number[0] if len(student_number) > 0 else None
                self.user.student_number = student_number

    def _get_system_role(self):
        from . import SystemRole

        if self.params:
            user_roles_attribute = instructor_role_values = None
            if self.third_party_type == ThirdPartyType.cas:
                user_roles_attribute = current_app.config.get('CAS_ATTRIBUTE_USER_ROLE')
                instructor_role_values = list(current_app.config.get('CAS_INSTRUCTOR_ROLE_VALUES'))
            if self.third_party_type == ThirdPartyType.saml:
                user_roles_attribute = current_app.config.get('SAML_ATTRIBUTE_USER_ROLE')
                instructor_role_values = list(current_app.config.get('SAML_INSTRUCTOR_ROLE_VALUES'))

            if user_roles_attribute and instructor_role_values and user_roles_attribute in self.params:
                user_roles = self.params.get(user_roles_attribute)
                if not isinstance(user_roles, list):
                    user_roles = [user_roles]

                for user_role in user_roles:
                    if user_role in instructor_role_values:
                        return SystemRole.instructor

        return SystemRole.student

    def upgrade_system_role(self):
        # upgrade system role is needed
        if self.user and self.params and self._get_system_role():
            system_role = self._get_system_role()
            if self.user.system_role == SystemRole.student and system_role == SystemRole.instructor:
                self.user.system_role = system_role

            db.session.commit()
Пример #14
0
class AssignmentGrade(DefaultTableMixin, WriteTrackingMixin):
    __tablename__ = 'assignment_grade'

    # table columns
    user_id = db.Column(db.Integer,
                        db.ForeignKey('user.id', ondelete="CASCADE"),
                        nullable=False)
    assignment_id = db.Column(db.Integer,
                              db.ForeignKey('assignment.id',
                                            ondelete="CASCADE"),
                              nullable=False)
    grade = db.Column(db.Float, default=0, nullable=False)

    # relationships
    # user via User Model
    # assignment via Course Model

    # hybrid and other functions
    @classmethod
    def get_assignment_grades(cls, assignment):
        return AssignmentGrade.query \
            .filter_by(assignment_id=assignment.id) \
            .all()

    @classmethod
    def get_user_assignment_grade(cls, assignment, user):
        return AssignmentGrade.query \
            .filter_by(
                user_id=user.id,
                assignment_id=assignment.id
            ) \
            .one_or_none()

    @classmethod
    def __declare_last__(cls):
        super(cls, cls).__declare_last__()

    __table_args__ = (
        # prevent duplicate user in course
        db.UniqueConstraint('assignment_id',
                            'user_id',
                            name='_unique_user_and_assignment'),
        DefaultTableMixin.default_table_args)

    @classmethod
    def calculate_grade(cls, assignment, user):
        from . import Answer, Comparison, CourseRole, \
            AnswerComment, AnswerCommentType, LTIOutcome

        user_is_student = False
        group_id = None

        for course_user in assignment.course.user_courses:
            if course_user.user_id != user.id:
                continue
            user_is_student = course_user.course_role == CourseRole.student
            group_id = course_user.group_id
            break

        if not user_is_student:
            return

        user_answer_count = Answer.query \
            .filter_by(
                assignment_id=assignment.id,
                user_id=user.id,
                active=True,
                practice=False,
                draft=False
            ) \
            .count()

        group_answer_counts = 0
        if group_id:
            group_answer_counts = Answer.query \
                .filter_by(
                    assignment_id=assignment.id,
                    active=True,
                    practice=False,
                    draft=False,
                    group_id=group_id
                ) \
                .count()

        comparison_count = Comparison.query \
            .filter_by(
                user_id=user.id,
                assignment_id=assignment.id,
                completed=True
            ) \
            .count()

        self_evaluation_count = AnswerComment.query \
            .join("answer") \
            .filter(and_(
                AnswerComment.user_id == user.id,
                AnswerComment.active == True,
                AnswerComment.comment_type == AnswerCommentType.self_evaluation,
                AnswerComment.draft == False,
                Answer.assignment_id == assignment.id,
                Answer.active == True,
                Answer.practice == False,
                Answer.draft == False
            )) \
            .count()

        answer_count = user_answer_count + group_answer_counts

        grade = _calculate_assignment_grade(assignment, answer_count,
                                            comparison_count,
                                            self_evaluation_count)

        assignment_grade = AssignmentGrade.get_user_assignment_grade(
            assignment, user)
        if assignment_grade == None:
            assignment_grade = AssignmentGrade(user_id=user.id,
                                               assignment_id=assignment.id)

        assignment_grade.grade = grade
        db.session.add(assignment_grade)
        db.session.commit()

        LTIOutcome.update_assignment_user_grade(assignment, user.id)

    @classmethod
    def calculate_group_grade(cls, assignment, group):
        from . import Answer, Comparison, CourseRole, \
            AnswerComment, AnswerCommentType, LTIOutcome

        student_ids = [course_user.user_id
            for course_user in assignment.course.user_courses
            if course_user.course_role == CourseRole.student and \
            course_user.group_id == group.id]

        # skip if there aren't any students
        if len(student_ids) == 0:
            return

        user_answer_counts = Answer.query \
            .with_entities(
                Answer.user_id,
                func.count(Answer.user_id).label('answer_count')
            ) \
            .filter_by(
                assignment_id=assignment.id,
                active=True,
                practice=False,
                draft=False
            ) \
            .filter(
                Answer.user_id.in_(student_ids)
            ) \
            .group_by(Answer.user_id) \
            .all()

        group_answer_counts = Answer.query \
            .with_entities(
                Answer.group_id,
                func.count(Answer.group_id).label('answer_count')
            ) \
            .filter_by(
                assignment_id=assignment.id,
                active=True,
                practice=False,
                draft=False,
                group_id=group.id
            ) \
            .group_by(Answer.group_id) \
            .all()

        comparison_counts = Comparison.query \
            .with_entities(
                Comparison.user_id,
                func.count(Comparison.user_id).label('comparison_count')
            ) \
            .filter_by(
                assignment_id=assignment.id,
                completed=True
            ) \
            .filter(
                Comparison.user_id.in_(student_ids)
            ) \
            .group_by(Comparison.user_id) \
            .all()

        self_evaluation_counts = AnswerComment.query \
            .with_entities(
                AnswerComment.user_id,
                func.count(AnswerComment.user_id).label('self_evaluation_count')
            ) \
            .join("answer") \
            .filter(and_(
                AnswerComment.active == True,
                AnswerComment.comment_type == AnswerCommentType.self_evaluation,
                AnswerComment.draft == False,
                Answer.assignment_id == assignment.id,
                Answer.active == True,
                Answer.practice == False,
                Answer.draft == False,
                AnswerComment.user_id.in_(student_ids)
            )) \
            .group_by(AnswerComment.user_id) \
            .all()

        assignment_grades = AssignmentGrade.get_assignment_grades(assignment)
        new_assignment_grades = []
        for student_id in student_ids:
            user_answer_count = next((result.answer_count
                                      for result in user_answer_counts
                                      if result.user_id == student_id), 0)

            group_answer_count = next((result.answer_count
                                       for result in group_answer_counts
                                       if result.group_id == group.id), 0)

            answer_count = user_answer_count + group_answer_count

            comparison_count = next((result.comparison_count
                                     for result in comparison_counts
                                     if result.user_id == student_id), 0)

            self_evaluation_count = next((result.self_evaluation_count
                                          for result in self_evaluation_counts
                                          if result.user_id == student_id), 0)

            grade = _calculate_assignment_grade(assignment, answer_count,
                                                comparison_count,
                                                self_evaluation_count)

            assignment_grade = next(
                (assignment_grade for assignment_grade in assignment_grades
                 if assignment_grade.user_id == student_id), None)

            if assignment_grade == None:
                assignment_grade = AssignmentGrade(user_id=student_id,
                                                   assignment_id=assignment.id)
                new_assignment_grades.append(assignment_grade)

            assignment_grade.grade = grade

        db.session.add_all(assignment_grades + new_assignment_grades)
        db.session.commit()

        LTIOutcome.update_assignment_users_grades(assignment, student_ids)

    @classmethod
    def calculate_grades(cls, assignment):
        from . import Answer, CourseRole, Comparison, \
            AnswerComment, AnswerCommentType, LTIOutcome

        student_ids = []
        group_ids = set()

        user_groups = {}
        for course_user in assignment.course.user_courses:
            if course_user.course_role == CourseRole.student:
                student_ids.append(course_user.user_id)
                if course_user.group_id:
                    group_ids.add(course_user.group_id)
                    user_groups[course_user.user_id] = course_user.group_id
        group_ids = list(group_ids)

        # skip if there aren't any students
        if len(student_ids) == 0:
            AssignmentGrade.query \
                .filter_by(assignment_id=assignment.id) \
                .delete()
            LTIOutcome.update_assignment_grades(assignment)
            return

        user_answer_counts = Answer.query \
            .with_entities(
                Answer.user_id,
                func.count(Answer.user_id).label('answer_count')
            ) \
            .filter_by(
                assignment_id=assignment.id,
                active=True,
                practice=False,
                draft=False
            ) \
            .filter(
                Answer.user_id.in_(student_ids)
            ) \
            .group_by(Answer.user_id) \
            .all()

        group_answer_counts = []
        if len(group_ids) > 0:
            group_answer_counts = Answer.query \
                .with_entities(
                    Answer.group_id,
                    func.count(Answer.group_id).label('answer_count')
                ) \
                .filter_by(
                    assignment_id=assignment.id,
                    active=True,
                    practice=False,
                    draft=False
                ) \
                .filter(
                    Answer.group_id.in_(group_ids)
                ) \
                .group_by(Answer.group_id) \
                .all()

        comparison_counts = Comparison.query \
            .with_entities(
                Comparison.user_id,
                func.count(Comparison.user_id).label('comparison_count')
            ) \
            .filter_by(
                assignment_id=assignment.id,
                completed=True
            ) \
            .filter(
                Comparison.user_id.in_(student_ids)
            ) \
            .group_by(Comparison.user_id) \
            .all()

        self_evaluation_counts = AnswerComment.query \
            .with_entities(
                AnswerComment.user_id,
                func.count(AnswerComment.user_id).label('self_evaluation_count')
            ) \
            .join("answer") \
            .filter(and_(
                AnswerComment.active == True,
                AnswerComment.comment_type == AnswerCommentType.self_evaluation,
                AnswerComment.draft == False,
                Answer.assignment_id == assignment.id,
                Answer.active == True,
                Answer.practice == False,
                Answer.draft == False,
                AnswerComment.user_id.in_(student_ids)
            )) \
            .group_by(AnswerComment.user_id) \
            .all()

        assignment_grades = AssignmentGrade.get_assignment_grades(assignment)
        new_assignment_grades = []
        for student_id in student_ids:
            user_answer_count = next((result.answer_count
                                      for result in user_answer_counts
                                      if result.user_id == student_id), 0)

            group_id = user_groups.get(student_id)
            group_answer_count = 0
            if group_id:
                group_answer_count = next((result.answer_count
                                           for result in group_answer_counts
                                           if result.group_id == group_id), 0)

            answer_count = user_answer_count + group_answer_count

            comparison_count = next((result.comparison_count
                                     for result in comparison_counts
                                     if result.user_id == student_id), 0)

            self_evaluation_count = next((result.self_evaluation_count
                                          for result in self_evaluation_counts
                                          if result.user_id == student_id), 0)

            grade = _calculate_assignment_grade(assignment, answer_count,
                                                comparison_count,
                                                self_evaluation_count)

            assignment_grade = next(
                (assignment_grade for assignment_grade in assignment_grades
                 if assignment_grade.user_id == student_id), None)

            if assignment_grade == None:
                assignment_grade = AssignmentGrade(user_id=student_id,
                                                   assignment_id=assignment.id)
                new_assignment_grades.append(assignment_grade)

            assignment_grade.grade = grade

        db.session.add_all(assignment_grades + new_assignment_grades)
        db.session.commit()

        LTIOutcome.update_assignment_grades(assignment)
Пример #15
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)
Пример #16
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)