class Note(BaseModel): model = pv.ForeignKeyField(Model, backref='notes') data = sqlite_ext.JSONField() # format = dict() constraint = ConstraintField(unique=True) # format = list() _tags = pv.ManyToManyField(Tag, backref='notes') created = pv.DateTimeField(constraints=[pv.SQL('DEFAULT CURRENT_TIMESTAMP')]) modified = pv.DateTimeField(constraints=[pv.SQL('DEFAULT CURRENT_TIMESTAMP')]) info = sqlite_ext.JSONField(default=dict) def to_dict(self): return super(Note, self).to_dict(manytomany=False, backrefs=False, exclude=['_tags'], extra_attrs=['tags']) @property def tags(self): return [t.name for t in self._tags] def mark(self, tag='marked'): Tag.get_or_create(name=tag)[0].notes.add(self) add_tag = mark def unmark(self, tag='marked'): Tag.get_or_create(name=tag)[0].notes.remove(self) remove_tag = unmark
class Result(Model): station = ForeignKeyField(WeatherStation) dc_output = sqlite_ext.JSONField() temperature = sqlite_ext.JSONField() wind_speed = sqlite_ext.JSONField() class Meta: database = db
class Settings(BaseModel): srs = SrsField(default=DEFAULT['srs']) type_ = pv.TextField(default=DEFAULT['type_']) info = sqlite_ext.JSONField(default=DEFAULT['info']) def to_dict(self): return model_to_dict(self)
class Media(BaseModel): data = pv.BlobField() h = pv.TextField() info = sqlite_ext.JSONField(default=dict) class Meta: indexes = [ # pv.SQL('CREATE UNIQUE INDEX media_data_hash ON media (MD5(data))'), ]
class Deck(BaseModel): name = pv.TextField(unique=True, collation='NOCASE') info = sqlite_ext.JSONField(default=dict) def __repr__(self): return f'<Deck: "{self.name}">' def __str__(self): return self.name
class Notes(BaseModel): """ -- Notes contain the raw information that is formatted into a number of cards -- according to the models CREATE TABLE notes ( id integer primary key, -- epoch seconds of when the note was created guid text not null, -- globally unique id, almost certainly used for syncing mid integer not null, -- model id mod integer not null, -- modification timestamp, epoch seconds usn integer not null, -- update sequence number: for finding diffs when syncing. -- See the description in the cards table for more info tags text not null, -- space-separated string of tags. -- includes space at the beginning and end, for LIKE "% tag %" queries flds text not null, -- the values of the fields in this note. separated by 0x1f (31) character. sfld text not null, -- sort field: used for quick sorting and duplicate check csum integer not null, -- field checksum used for duplicate check. -- integer representation of first 8 digits of sha1 hash of the first field flags integer not null, -- unused data text not null -- unused ); """ id = pv.AutoField(primary_key=True) # Use auto-increment instead of time in Epoch seconds to ensure uniqueness guid = pv.TextField(unique=True, default=shortuuid.uuid) # Use short UUID instead of default generator model = pv.ForeignKeyField(Models, column_name="mid", null=True) mod = pv.IntegerField() # autogenerated usn = pv.IntegerField(default=-1) tags = ArrayFieldPadded(default=list) flds = X1fField() sfld = pv.TextField() # autogenerated csum = pv.IntegerField() # autogenerated flags = pv.IntegerField(default=0) data = sqlite_ext.JSONField(default=dict) # shouldn't that be a TextField? class Meta: indexes = [ pv.SQL('CREATE INDEX ix_notes_usn on notes (usn)'), pv.SQL('CREATE INDEX ix_notes_csum on notes (csum)') ] @classmethod def create(cls, fields=None, **query): inst = cls(**query) inst.fields = fields inst.save(force_insert=True) return inst
def upgrade(): settings = db.Settings.get() version = settings.info['version'] migrator = SqliteMigrator(db.database) if version < '0.2': migrate( migrator.add_column('deck', 'info', sqlite_ext.JSONField(default=dict)), migrator.add_column('media', 'info', sqlite_ext.JSONField(default=dict)), migrator.add_column('model', 'info', sqlite_ext.JSONField(default=dict)), migrator.add_column('template', 'info', sqlite_ext.JSONField(default=dict)), migrator.add_column('note', 'info', sqlite_ext.JSONField(default=dict)), migrator.add_column('card', 'info', sqlite_ext.JSONField(default=dict)), migrator.add_column('card', 'last_review', pv.TimestampField()), ) settings.info['version'] = '0.2' settings.save() if version < '0.2.1': migrate( migrator.drop_column('card', 'last_review'), migrator.add_column('card', 'last_review', pv.DateTimeField(default=datetime.now)), migrator.drop_column('note', 'modified'), migrator.add_column('note', 'modified', pv.DateTimeField(default=datetime.now))) settings.info['version'] = '0.2.1' settings.save()
class Template(BaseModel): model = pv.ForeignKeyField(Model, backref='templates') name = pv.TextField() front = pv.TextField() back = pv.TextField(null=True) info = sqlite_ext.JSONField(default=dict) def test_front(self, d): text = self.front for k, v in d.items(): text = text.replace('{{%s}}' % k, str(v)) return text class Meta: indexes = [ (('model_id', 'name'), True), (('model_id', 'front'), True), ]
class Model(BaseModel): name = pv.TextField(unique=True) key_fields = sqlite_ext.JSONField(default=list) css = pv.TextField(null=True) js = pv.TextField(null=True) info = sqlite_ext.JSONField(default=dict)
class Settings(BaseModel): srs = SrsField(default=DEFAULT['srs']) info = sqlite_ext.JSONField(default=DEFAULT['info'])
class Card(BaseModel): template = pv.ForeignKeyField(Template, backref='cards') note = pv.ForeignKeyField(Note, backref='cards') _front = pv.TextField(unique=True) srs_level = pv.IntegerField(null=True) next_review = pv.DateTimeField(null=True) _decks = pv.ManyToManyField(Deck, backref='cards') last_review = pv.DateTimeField(constraints=[pv.SQL('DEFAULT CURRENT_TIMESTAMP')]) info = sqlite_ext.JSONField(default=dict) backup = None def to_dict(self, max_depth=2, **kwargs): d = super(Card, self).to_dict(manytomany=False, backrefs=False, exclude=['_decks', '_front', 'note'], extra_attrs=['decks', 'front', 'back']) d['note'] = self.note.to_dict() return d @property def decks(self): return [d.name for d in self._decks] @property def front(self): text = self.template.front for k, v in self.note.data.items(): text = text.replace('{{%s}}' % k, str(v)) return text @property def back(self): text = self.template.back if not text: return '\n'.join(' ' * 4 + line for line in json.dumps( self.note.data, indent=2, ensure_ascii=False ).split('\n')) for k, v in self.note.data.items(): text = text.replace('{{%s}}' % k, str(v)) return text def __repr__(self): return self.front @property def data(self): return self.note.data def add_deck(self, deck_name): Deck.get_or_create(name=deck_name)[0].cards.add(self) def remove_deck(self, deck_name): Deck.get_or_create(name=deck_name)[0].cards.remove(self) def mark(self, tag='marked'): return self.note.mark(tag) def unmark(self, tag='marked'): return self.note.unmark(tag) def right(self, step=1): self.undo() if not self.backup: self.backup = model_to_dict(self) print(self.srs_level) if self.srs_level is None: self.srs_level = 0 else: self.srs_level = self.srs_level + step srs = Settings.get().srs try: self.next_review = datetime.now() + srs[self.srs_level] except IndexError: self.next_review = None assert isinstance(self.info, dict) self.info['lapse'] = 0 self.info['streak'] = self.info.get('streak', 0) + 1 self.info['total_right'] = self.info.get('total_right', 0) + 1 self.save() correct = next_srs = right def easy(self, max_srs_level_enabled=3): if self.srs_level < max_srs_level_enabled: return self.right(step=2) else: raise ValueError def wrong(self, next_review=timedelta(minutes=10)): self.undo() if not self.backup: self.backup = model_to_dict(self) if self.srs_level is not None and self.srs_level > 0: self.srs_level = self.srs_level - 1 assert isinstance(self.info, dict) self.info['streak'] = 0 self.info['lapse'] = self.info.get('lapse', 0) + 1 self.info['total_wrong'] = self.info.get('total_wrong', 0) + 1 self.bury(next_review) incorrect = previous_srs = wrong def bury(self, next_review=timedelta(hours=4)): if not self.backup: self.backup = model_to_dict(self) if isinstance(next_review, timedelta): self.next_review = datetime.now() + next_review else: self.next_review = next_review self.save() def reset(self): self.srs_level = None self.next_review = None self.save() def undo(self): if self.backup: dict_to_model(Card, self.backup).save() @classmethod def iter_quiz(cls, **kwargs): db_cards = list(cls.search(**kwargs)) random.shuffle(db_cards) return iter(db_cards) @classmethod def iter_due(cls, **kwargs): return cls.iter_quiz(due=True, **kwargs) @classmethod def search(cls, q_str='', deck=None, tags=None, due=None, offset=0, limit=None): """ :param q_str: :param deck: :param tags: :param bool|None|timedelta|datetime due: :param offset: :param limit: :return: """ query = cls.select() due_is_set = False note_keys = None result = parse_query(q_str) if result: for seg in result: if len(seg) == 1: if note_keys is None: note_keys = set() for srs_note in Note.select(Note.data): note_keys.update(srs_note.data.keys()) note_keys = tuple(note_keys) q_note = Note.data[note_keys[0]].contains(seg[0]) for k in note_keys[1:]: q_note |= Note.data[k].contains(seg[0]) query = query.switch(cls).join(Note).where(q_note) else: if seg[0] == 'due': due_is_set = True if seg[2].lower() == 'true': query = query.switch(cls).where(cls.next_review < datetime.now()) elif seg[2].lower() == 'false': query = query.switch(cls).where(cls.next_review.is_null(True)) else: dur_sec = pytimeparse.parse(seg[2]) if dur_sec: _due = datetime.now() + timedelta(seconds=dur_sec) else: _due = dateutil.parser.parse(seg[2]) query = query.switch(cls).where(cls.next_review < _due) elif seg[0] == 'deck': deck_q = (Deck.name == seg[2]) if seg[1] != '=': deck_q = (deck_q | Deck.name.startswith(seg[2] + '::')) query = query.switch(cls).join(CardDeck).join(Deck).where(deck_q) elif seg[0] == 'tag': if seg[1] == '=': query = query.switch(cls).join(Note).join(NoteTag).join(Tag)\ .where(Tag.name == seg[2]) else: query = query.switch(cls).join(Note).join(NoteTag).join(Tag)\ .where(Tag.name.contains(seg[2])) else: if seg[1] == '=': query = query.switch(cls).join(Note).where(Note.data[seg[0]] == seg[2]) elif seg[1] == '>': query = query.switch(cls).join(Note).where(Note.data[seg[0]] > seg[2]) elif seg[1] == '<': query = query.switch(cls).join(Note).where(Note.data[seg[0]] < seg[2]) else: query = query.switch(cls).join(Note).where(Note.data[seg[0]].contains(seg[2])) if due is True: query = query.switch(cls).where(cls.next_review < datetime.now()) elif due is False: query = query.switch(cls).where(cls.next_review.is_null(True)) elif isinstance(due, timedelta): query = query.switch(cls).where(cls.next_review < datetime.now() + due) elif isinstance(due, datetime): query = query.switch(cls).where(cls.next_review < due) else: if not due_is_set: query = query.where((cls.next_review < datetime.now()) | cls.next_review.is_null(True)) if deck: query = query.switch(cls).join(CardDeck).join(Deck).where(Deck.name.startswith(deck + '::') | (Deck.name == deck)) if tags: for tag in tags: query = query.switch(cls).join(Note).join(NoteTag).join(Tag).where(Tag.name.contains(tag)) query = query.order_by(cls.next_review.desc()) if offset: query = query.offset(offset) if limit: query = query.limit(limit) return query
class Note(BaseModel): data = sqlite_ext.JSONField() model = pv.ForeignKeyField(Model, backref='notes') media = pv.ManyToManyField(Media, backref='notes', on_delete='cascade') tags = pv.ManyToManyField(Tag, backref='notes', on_delete='cascade') h = pv.TextField(unique=True) def mark(self, tag): Tag.get_or_create(name=tag)[0].notes.add(self) def unmark(self, tag): Tag.get_or_create(name=tag)[0].notes.remove(self) def rename_field(self, old_name, new_name): for db_note in Note.select(Note.data, Note.model_id).where(model_id=self.model_id): if old_name in db_note.data.keys(): db_note.data[new_name] = db_note.data.pop(old_name) db_note.save() @classmethod def add(cls, data, model, card_to_decks: dict, media: dict=None, tags: list=None): if media is None: media = dict() if tags is None: tags = list() with database.atomic(): if isinstance(model, int) or (isinstance(model, str) and model.isdigit()): db_model = Model.get(id=int(model)) elif isinstance(model, Model): db_model = model else: db_model = Model.get(name=model) db_note = cls.create( data=data, model_id=db_model.id ) for template, deck in card_to_decks.items(): if isinstance(deck, int) or (isinstance(deck, str) and deck.isdigit()): db_deck = Deck.get(id=int(deck)) elif isinstance(deck, Deck): db_deck = deck else: db_deck = Deck.get_or_create(name=deck)[0] if isinstance(template, int) or (isinstance(template, str) and template.isdigit()): db_template = Template.get(id=int(template)) elif isinstance(template, Template): db_template = template else: db_template = Template.get(model_id=db_model.id, name=template) Card.create( note_id=db_note.id, deck_id=db_deck.id, template_id=db_template.id ) for media_name, media_path in media.items(): type_ = { 'audio': MediaType.audio, 'font': MediaType.font }.get(magic.from_file(media_path, mime=True).split('/')[0], MediaType.image) with open(media_path, 'rb') as f: Media.create( name=media_name, data=f.read(), type_=type_ ) for tag_name in tags: Tag.get_or_create(name=tag_name)[0].notes.add(db_note) return db_note @classmethod def search(cls, model_name=None, deck_name=None, tags=None, data=None, **kwargs): db_query = cls.select() if deck_name: db_query = db_query \ .join(Card).join(Deck) \ .where(Deck.name.contains(deck_name)) db_query = cls._build_query(db_query, model_name=model_name, tags=tags, data=data, **kwargs) return db_query @classmethod def _build_query(cls, db_query, model_name=None, tags=None, data=None, **kwargs): if data is None: data = dict() data.update(kwargs) db_query = db_query.switch(cls) if data: for k, v in data.items(): db_query = db_query.where(cls.data[k].contains(v)) if model_name: db_query = db_query \ .join(Model) \ .where(Model.name.contains(model_name)) if tags: db_query = db_query\ .join(NoteTag).join(Tag)\ .where(~Tag.name.not_in(tags)) return db_query def to_viewer(self): d = model_to_dict(self) d['cards'] = '<br/>'.join(c.html for c in self.cards) d['tags'] = [t.name for t in self.tags] return d viewer_config = { 'renderer': { 'cards': 'html' }, 'colWidth': { 'cards': 400 } }
class Card(BaseModel): item = pv.TextField(unique=True) info = sqlite_ext.JSONField(null=True) srs_level = pv.IntegerField(null=True) next_review = pv.DateTimeField(null=True) tags = pv.ManyToManyField(Tag, backref='cards', on_delete='cascade') backup = None def __repr__(self): return self.item def _repr_markdown_(self): type_ = Settings.get().type_ if type_ == 'markdown': return self.item def _repr_html_(self): type_ = Settings.get().type_ if type_ == 'html': return self.item def mark(self, tag='marked'): Tag.get_or_create(name=tag)[0].notes.add(self) def unmark(self, tag='marked'): Tag.get_or_create(name=tag)[0].notes.remove(self) def right(self): if not self.backup: self.backup = model_to_dict(self) if not self.srs_level: self.srs_level = 0 else: self.srs_level = self.srs_level + 1 srs = Settings.get()['srs'] try: self.next_review = datetime.now() + srs[self.srs_level] except IndexError: self.next_review = None self.save() correct = next_srs = right def wrong(self, next_review=timedelta(minutes=10)): if not self.backup: self.backup = model_to_dict(self) if self.srs_level and self.srs_level > 0: self.srs_level = self.srs_level - 1 self.bury(next_review) incorrect = previous_srs = wrong def bury(self, next_review=timedelta(hours=4)): if not self.backup: self.backup = model_to_dict(self) if isinstance(next_review, timedelta): self.next_review = datetime.now() + next_review else: self.next_review = next_review self.save() def undo(self): if self.backup: dict_to_model(Card, self.backup).save() @classmethod def iter_quiz(cls, **kwargs): db_cards = list(cls.search(**kwargs)) random.shuffle(db_cards) return iter(db_cards) @classmethod def iter_due(cls, **kwargs): return cls.iter_quiz(due=True, **kwargs) @classmethod def search(cls, tags=None, due=Any, offset=0, limit=None): query = cls.select() if due is True: query = query.where(Card.next_review < datetime.now()) elif due is False: query = query.where(Card.next_review >= datetime.now()) elif due is None: query = query.where(Card.next_review.is_null(True)) elif isinstance(due, timedelta): query = query.where(Card.next_review < datetime.now() + due) elif isinstance(due, datetime): query = query.where(Card.next_review < due) if tags: for tag in tags: query = query.join(CardTag).join(Tag).where(Tag.name.contains(tag)) query = query.order_by(cls.next_review.desc()) if offset: query = query.offset(offset) if limit: query = query.limit(limit) return query def to_dict(self): return model_to_dict(self, manytomany=True) @classmethod def add(cls, item, tags, **kwargs): with database.atomic(): db_card = cls.create(item=item, info=kwargs) for tag in tags: Tag.get_or_create(name=tag)[0].cards.add(db_card) return db_card
class Word(BaseModel): word = sqlite.CharField(index=True, constraints=[peewee.SQL("COLLATE NOCASE")]) vector = sqlite.JSONField() stoplisted = sqlite.BooleanField(index=True, default=False)
class Gene(BaseModel): gene = peewee.CharField(index=True, constraints=[peewee.SQL("COLLATE NOCASE")]) vector = sqlite.JSONField()