class LanguageEnrollment(db.Model): """ Represents a user's enrollment in a language """ __tablename__ = 'language_enrollments' id = db.Column(db.Integer, primary_key=True, autoincrement=True) language_id = db.Column(db.Integer, db.ForeignKey('languages.id', ondelete="CASCADE")) language = db.relationship("Language") user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete="CASCADE")) user = db.relationship("User", backref="languageEnrollments") default = db.Column(db.Boolean, default=False)
class Symbol(db.Model): """ Represents a symbol used in a language """ __tablename__ = 'symbols' id = db.Column(db.Integer, primary_key=True) text = db.Column(db.UnicodeText()) concept_id = db.Column(db.Integer, db.ForeignKey('concepts.id', ondelete="CASCADE")) language_id = db.Column(db.Integer, db.ForeignKey('languages.id', ondelete="CASCADE")) ambiguity_group_id = db.Column( db.Integer, db.ForeignKey(AmbiguityGroup.id, ondelete="SET NULL")) clarification = db.Column(db.UnicodeText()) concept = db.relationship("Concept") language = db.relationship("Language") ambiguity_group = db.relationship(AmbiguityGroup, backref="symbols") @hybrid_method def ratingFor(self, masteryCache): """ Return the rating for the given user """ return masteryCache[self.id].rating @ratingFor.expression def ratingFor(self, user): """ Return the expression to use when querying for a word's rating """ return Mastery.rating @property def needsClarification(self): """ Return if this Symbol needs Clarification """ return self.ambiguity_group_id is not None def ambiguousWith(self, other): """ Return if this Symbol is ambiguous with the other Symbol """ return self.text == other.text or self.matchingAmbiguityGroup(other) def matchingAmbiguityGroup(self, other): """ Return whether this Symbol matches the other Symbol's Ambiguity Group """ if self.ambiguity_group_id is not None: return self.ambiguity_group_id == other.ambiguity_group_id else: return False def __unicode__(self): """ Return the string representation of the Word """ return unicode(self.text) def __repr__(self): """ Return the String representation of the Symbol """ return "<Symbol({0}, {1})>".format(self.id, self.language.name)
class ConceptList(db.Model): """ Represents a list of concepts """ __tablename__ = 'concept_lists' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.Unicode()) isWords = db.Column(db.Boolean) concepts = db.relationship("Concept", secondary=concept_list_concepts) def getConceptPairs(self, conceptManager): """ Return the concept pairs """ return conceptManager.getConceptPairs( [concept.id for concept in self.concepts]) @hybrid_method def averageRatingFor(self, language, conceptFormCache, masteryCache): """ Return the rating for the given user """ ratings = [] for concept in self.concepts: form = conceptFormCache.get(conceptId=concept.id, languageId=language.id) ratings.append(masteryCache[form.id].rating) return round(sum(ratings, 0.0) / len(ratings), 1) @averageRatingFor.expression def averageRatingFor(self, user, language): """ Return the rating for the given user """ return func.avg( self.entry_model.ratingFor(user)).over(partition_by=self.id)
class User(db.Model): """ Represents a user """ __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) email = db.Column(db.Text(), nullable=False, unique=True, index=True) password = db.Column(db.String(128), nullable=False) is_admin = db.Column(db.Boolean, default=False) givenName = db.Column(db.UnicodeText()) lastName = db.Column(db.UnicodeText()) native_language_id = db.Column(db.Integer, db.ForeignKey('languages.id')) nativeLanguage = db.relationship("Language") learnedSymbols = db.relationship("Symbol", secondary=learned_symbols, lazy='dynamic') learnedWords = db.relationship("Word", secondary=learned_words, lazy='dynamic') def getLearnedFor(self, formInfo, language): """ Return the learned forms for the given Form Info """ helper = self.helperFor(formInfo) return helper.formsFor(language) def tryToLearn(self, form, formInfo, learnedCache): """ Learn the form unless it has already been learned """ helper = self.helperFor(formInfo) helper.tryToLearn(form, learnedCache) def save(self): """ Save the Underlying User Data Object """ db.session.add(self) db.session.commit() def helperFor(self, formInfo): """ Return the Learn Helper for the given Form Info """ return self.learnedSymbolsHelper if formInfo is SymbolInfo else self.learnedWordsHelper @lazy_property def learnedSymbolsHelper(self): """ Helper to manage Learned Symbols """ return LearnHelper(self, SymbolInfo) @lazy_property def learnedWordsHelper(self): """ Helper to manage Learned Words """ return LearnHelper(self, WordInfo)
class StalenessPeriod(db.Model): """ Represents a staleness period """ __tablename__ = 'staleness_periods' id = db.Column(db.Integer, primary_key=True) days = db.Column(db.Integer) first = db.Column(db.Boolean, default=False) next_id = db.Column(db.Integer, db.ForeignKey('staleness_periods.id')) next = db.relationship("StalenessPeriod", remote_side=[id]) @classmethod def getFirstStalenessPeriod(cls): """ Return the first staleness period """ return cls.query.filter_by(first=True).first()
class AmbiguityGroup(db.Model): """ Represents a group of words or symbols that are ambiguous with each other """ __tablename__ = 'ambiguity_groups' id = db.Column(db.Integer, primary_key=True) language_id = db.Column(db.Integer, db.ForeignKey(Language.id), nullable=False) language = db.relationship(Language, backref="ambiguity_groups") def __repr__(self): """ Return the String Representation of the Ambiguity Group """ return "<AmbiguityGroup(id={})>".format(self.id)
class Word(db.Model): """ Represents a word from a particular language """ __tablename__ = 'words' id = db.Column(db.Integer, primary_key=True) text = db.Column(db.UnicodeText()) concept_id = db.Column(db.Integer, db.ForeignKey('concepts.id', ondelete="CASCADE")) concept = db.relationship("Concept") language_id = db.Column(db.Integer, db.ForeignKey('languages.id', ondelete="CASCADE")) language = db.relationship("Language") @hybrid_method def ratingFor(self, masteryCache): """ Return the rating for the given user """ return masteryCache[self.id].rating @ratingFor.expression def ratingFor(self, user): """ Return the expression to use when querying for a word's rating """ return Mastery.rating @property def needsClarification(self): """ Return if this Symbol needs Clarification """ return False def ambiguousWith(self, other): """ Return if this Symbol is ambiguous with the other Symbol """ return self.text == other.text def __unicode__(self): """ Return the string representation of the Word """ return unicode(self.text) def __repr__(self): """ Return the string representation of the Word """ return repr(self.text)
class Mastery(db.Model): """ Represents the mastery of some skill """ __tablename__ = 'masteries' MAX_RATING = 5 CORRECT_CHANGE = 1 WRONG_CHANGE = -1 id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('users.id')) user = db.relationship("User") word_id = db.Column(db.Integer, db.ForeignKey('words.id', ondelete="CASCADE")) word = db.relationship("Word") symbol_id = db.Column(db.Integer, db.ForeignKey('symbols.id', ondelete="CASCADE")) symbol = db.relationship("Symbol") answerRating = db.Column(db.Integer, server_default=text('0'), nullable=False) lastCorrectAnswer = db.Column(db.DateTime) staleness_period_id = db.Column(db.Integer, db.ForeignKey('staleness_periods.id')) stalenessPeriod = db.relationship("StalenessPeriod", lazy='subquery') def __init__(self, *args, **kwargs): """ Initialize the mastery """ if 'user' in kwargs and hasattr(kwargs['user'], 'user'): kwargs['user'] = kwargs['user'].user if 'stalenessPeriod' not in kwargs: kwargs[ 'stalenessPeriod'] = StalenessPeriod.getFirstStalenessPeriod() db.Model.__init__(self, *args, **kwargs) def addAnswer(self, correct): """ Add an answer to this mastery """ self.updateStalenessPeriod(correct) self.updateAnswerDate(correct) self.updateRating(correct) db.session.add(self) db.session.commit() def updateRating(self, correct): """ Update the answer rating """ ratingChange = self.CORRECT_CHANGE if correct else self.WRONG_CHANGE newRating = self.answerRating + ratingChange newRating = min(newRating, self.MAX_RATING) newRating = max(newRating, 0) self.answerRating = newRating def updateAnswerDate(self, correct): """ Update the answer date """ if correct: self.lastCorrectAnswer = datetime.now() def updateStalenessPeriod(self, correct): """ Update the staleness period based on whether the answer is correct """ if correct and self.answerRating == self.MAX_RATING and self.isStale: self.moveToNextStalenessPeriod() elif not correct: self.revertToFirstStalenessPeriod() def moveToNextStalenessPeriod(self): """ Move the mastery to the next staleness period """ if self.stalenessPeriod.next: self.stalenessPeriod = self.stalenessPeriod.next def revertToFirstStalenessPeriod(self): """ Revert the staleness period to the first staleness period """ self.stalenessPeriod = StalenessPeriod.getFirstStalenessPeriod() @property def form(self): """ Return the Concept Form associated with the Mastery """ return self.word if self.word_id is not None else self.symbol @hybrid_property def rating(self): """ Return the rating of the mastery """ return max(0, self.answerRating - self.stalenessRating) @rating.expression def rating(self): """ Return the Queryable rating of the mastery """ return func.greatest(0, self.answerRating - self.stalenessRating) @hybrid_property def stalenessRating(self): """ Return the staleness rating of the mastery """ mostRecentCorrectAnswer = self.lastCorrectAnswer if mostRecentCorrectAnswer is None: return 0 else: return int((datetime.now() - mostRecentCorrectAnswer).days / self.stalenessPeriod.days) @stalenessRating.expression def stalenessRating(self): """ Return the Queryable staleness rating of the mastery """ return func.coalesce( cast( func.floor( func.extract('epoch', func.now() - self.lastCorrectAnswer) / 86400), db.Integer) / StalenessPeriod.days, 0) @hybrid_property def isStale(self): """ Return if the mastery has outlived the staleness period """ return self.stalenessRating > 0