class Panorama(BaseMeta): id = Column(Integer, primary_key=True) type = Column(Choice(['article', 'comment', 'tag', 'group']), nullable=False, index=True) event = Column(Choice([ 'add', 'modify', 'move', 'remove', 'survey_start', 'survey_end', 'article', 'group', 'user', 'file', 'add_rel', 'remove_rel', 'add_menu', 'remove_menu', 'add_board', 'remove_board', 'new' ]), nullable=False, index=True) from_group_id = Column(Integer, ForeignKey('Group.uid', ondelete='set null'), nullable=True) from_board_id = Column(Integer, ForeignKey('Board.uid', ondelete='set null'), nullable=True) user_id = Column(Integer, ForeignKey( 'User.uid', ondelete='set null', ), nullable=True) target_id = Column(Integer, ForeignKey('Base.uid', ondelete='set null'), nullable=True) content = Column(UnicodeText, nullable=False, default=u'') created_at = Column(DateTime, nullable=False, default=func.now(), index=True) from_group = relationship('Group', primaryjoin='Panorama.from_group_id == \ Group.uid') from_board = relationship('Board', primaryjoin='Panorama.from_board_id == \ Board.uid') user = relationship('User', primaryjoin='Panorama.user_id == User.uid') target = relationship('Base', primaryjoin='Panorama.target_id == Base.uid')
class GroupMenu(BaseMeta): GROUP_MENU_TYPES = (u'board', u'link', u'text', u'indent', u'outdent', ) __table_args__ = ( UniqueConstraint('group_id', 'position'), ) id = Column(Integer, primary_key=True, autoincrement=True) group_id = Column(Integer, ForeignKey('Group.uid', ondelete='cascade'), nullable=True) menu_type = Column(Choice(GROUP_MENU_TYPES), nullable=False) board_uid = Column(Integer, ForeignKey('Board.uid', ondelete='cascade'), nullable=True) url = Column(UnicodeText, nullable=False, default=u'') name = Column(Unicode(256), nullable=False, default=u'') position = Column(Integer, nullable=False, default=0, index=True) group = relationship('Group', backref=backref( 'menus', order_by='GroupMenu.position')) board = relationship('Board')
class Mention(BaseMeta): u''' 멘션은 글 또는 댓글에서만 할 수 있다. [[uid]]로 할 수 있고, 당연히 uid가 달린 것들만 멘션할 수 있다. 글 제목에서는 멘션할 수 없다. 멘션하면 멘션 당한 유저에게 알림이 가야 한다. 알림은 "A 유저가 B 유저를 멘션했습니다" 또는 "A 유저가 B유저의 글/댓글 C를 멘션했습니다" 정도가 될 것이다. ''' __table_args__ = (UniqueConstraint('mentioning_id', 'target_id'), ) id = Column(Integer, primary_key=True, autoincrement=True) mentioning_user_id = Column(Integer, ForeignKey('User.uid', ondelete='cascade'), nullable=True, index=True) mentioning_id = Column(Integer, ForeignKey('Base.uid', ondelete='cascade'), nullable=False, index=True) target_id = Column(Integer, ForeignKey('Base.uid', ondelete='cascade'), nullable=False, index=True) target_user_id = Column(Integer, ForeignKey('User.uid', ondelete='cascade'), nullable=True, index=True) target_type = Column(Choice(['User', 'Article', 'Comment']), nullable=False, index=True) created_at = Column(DateTime, default=func.now(), nullable=False) mentioning_user = relationship('User', primaryjoin='User.uid == \ Mention.mentioning_user_id') mentioning = relationship('Base', primaryjoin='Base.uid == \ Mention.mentioning_id', backref=backref('mentions', passive_deletes=True)) target = relationship('Base', primaryjoin='Base.uid == Mention.target_id') target_user = relationship('User', primaryjoin='User.uid == \ Mention.target_user_id')
class Board(UIDMixin, Base, ArticleContainer): types = ( 'normal', 'album', 'forum', ) types_str = ( ('normal', u'일반게시판'), ('album', u'사진게시판'), ('forum', u'포럼게시판'), ) name = Column(Unicode(256), nullable=False) board_type = Column('type', Choice(types), nullable=False, default='normal') last_updated_at = Column(DateTime, nullable=False, default=func.now()) admin_id = Column(Integer, ForeignKey('User.uid', ondelete='set null'), nullable=True, default=None) _admin = relationship('User', backref='managable_boards', primaryjoin='Board.admin_id == User.uid') article_count = Column(Integer, nullable=False, default=0) articles = relationship('Article', secondary='BoardAndArticle', primaryjoin='Board.uid == \ BoardAndArticle.c.board_id', order_by='Article.uid.desc()') group = relationship('Group', secondary=GroupMenu.__table__, primaryjoin='Board.uid == GroupMenu.board_uid', uselist=False) @property def has_new_article(self): return datetime.now() - self.last_updated_at < timedelta(hours=24) @property def type_str(self): return dict(Board.types_str)[self.board_type] def get_group(self, session=None): from group import GroupMenu if session is None: session = db menu = session.query(GroupMenu).filter_by(board=self).first() if menu: return menu.group return None @hybrid_property def admin(self): if self._admin: return self._admin if self.group and self.group.admin: return self.group.admin return None @admin.setter def set_admin(self, value): self._admin = value def is_admin(self, user): return self.admin == user or self.group.admin == user @classmethod def from_group(cls, groups): if isinstance(groups, (list, tuple)) and not \ any([not isinstance(e, Group) for e in groups]): return db.query(cls).filter( cls.group.has(Group.uid.in_([group.uid for group in groups]))) elif isinstance(groups, Group): return groups.boards return []
class NotificationLog(BaseMeta): u''' 유저에게 알림을 날린 로그. 타입은 다음과 같은 것들이 있으며, 추가될 수 있다. comment: 내가 쓴 글이나 댓글, 그리고 내 프로필 페이지에 댓글이 달림 tag: 내가 쓴 글이나 업로드한 사진, 또는 내 프로필 페이지에 태그가 달림 recommend: 내가 쓴 글이나 댓글에 추천이 달림 article: 내가 즐겨찾기 추가한 게시판이나 모임에 새 글이 올라옴 mention: 글이나 댓글로 나, 혹은 내가 쓴 글/댓글을 언급함 ''' id = Column(Integer, primary_key=True, autoincrement=True) type = Column(Choice([ 'comment', 'tag', 'recommend', 'article', 'mention', ]), nullable=False, index=True) sub_type = Column(Choice([ 'article', 'comment', 'commented_article', 'favorite', 'user', ]), nullable=False, index=True) receiver_id = Column(Integer, ForeignKey('User.uid', ondelete='cascade'), nullable=False) target_id = Column(Integer, ForeignKey('Base.uid', ondelete='cascade'), nullable=False) ''' senders 구성: list: list of dict: sender_id: number sender_realname: string content: string content_id: number. optional. ''' senders = Column(JSONEncodedDict, nullable=False, default={}) created_at = Column(DateTime, nullable=False, default=func.now()) last_updated_at = Column(DateTime, nullable=False, default=func.now(), onupdate=func.now(), index=True) receiver = relationship('User', primaryjoin='NotificationLog.receiver_id \ == User.uid', backref='noties') target = relationship('Base', primaryjoin='NotificationLog.target_id == \ Base.uid') @property def url(self): try: return url_for('main.go_uid', uid=self.senders['list'][0]['content_id']) except KeyError: return url_for('main.go_uid', uid=self.target_id) @property def target_name(self): target = db.query(Base).get(self.target_id) return target.title if isinstance(target, Article) else \ target.content if isinstance(target, Comment) else \ target.realname if isinstance(target, User) else \ target.group.name + ' - ' + target.name if isinstance(target, Board) else \ target.name @property def jsonify(self): u''' 알림을 redis로 바로 뿌릴 수 있는 형태의 json으로 리턴 ''' result = { 'target_id': self.target_id, 'target_name': self.target_name, 'receiver_id': self.receiver_id, 'url': self.url, 'senders': [{ 'uid': s['sender_id'], 'realname': s['sender_realname'], } for s in self.senders['list']], 'content': self.senders['list'][0]['content'], 'type': self.type, 'sub_type': self.sub_type, } return json.dumps(result)
class User(UIDMixin, Base): CLASSES = OrderedDict([ ('bachelor', u'학사 재학'), ('bechelor_degree', u'학사 졸업'), ('master', u'석사 재학'), ('master_degree', u'석사 졸업'), ('phd', u'박사 재학'), ('phd_degree', u'박사 졸업'), ('major', u'복수전공'), ('minor', u'부전공'), ('professor', u'교수'), ('staff', u'직원'), ('others', u'기타'), ('exchange', u'교환학생'), ]) PHONES = OrderedDict([ ('home', u'집'), ('cell', u'휴대전화'), ('office', u'사무실'), ('lab', u'연구실'), ('prof.room', u'교수실'), ]) username = Column(Unicode(255), unique=True) _password = Column('password', Unicode(512), default=u'') salt = Column(Unicode(256), nullable=False, default=u'') realname = Column(Unicode(255), nullable=False, default=u'', index=True) email = Column(Unicode(254), nullable=False, default=u'') phone = Column(Unicode(20), nullable=False, default=u'') birthday = Column(Date, nullable=True, default=None, index=True) bs_year = Column(Integer, nullable=True, default=None, index=True) ms_year = Column(Integer, nullable=True, default=None, index=True) phd_year = Column(Integer, nullable=True, default=None, index=True) info = Column(JSONEncodedDict, nullable=False, default={}) classes = Column(Unicode(255), nullable=False, default=u'bachelor', index=True) state = Column(Choice([u'pending', u'normal', u'admin']), nullable=False, default=u'pending') photo_id = Column(Integer, ForeignKey('File.uid', ondelete='set null', use_alter=True, name='User_photo_id_fkey'), nullable=True) photo = relationship('File', primaryjoin='User.photo_id == File.uid', post_update=True) accessible_groups = relationship('Group', secondary=AccessibleGroup.__table__, primaryjoin='User.uid == \ AccessibleGroup.user_id') def __init__(self, username, password, **kw): super(UIDMixin, self).__init__() super(Base, self).__init__() self.salt = reduce( lambda x, y: x + y, [random.choice(string.printable) for _ in range(256)]).decode('utf-8') self.username = self.sid = username self.password = password self.realname = kw['realname'] self.email = kw['email'] self.phone = kw['phone'] self.birthday = kw['birthday'] self.classes = kw['classes'] self.state = kw['state'] for degree in 'bs ms phd'.split(' '): if kw['%s_number' % degree]: try: setattr(self, '%s_year' % degree, int(kw['%s_number' % degree].split('-')[0])) except: pass self.info = {} for info_field in ('bs_number', 'ms_number', 'phd_number', 'classes', 'graduate'): self.info[info_field] = kw[info_field] @hybrid_property def password(self): return self._password @password.setter def set_password(self, value): self._password = self._make_password(value) @property def recommended_uids(self): return [r.target_id for r in self.recommended] def _make_password(self, value): hash_value = self.__dict__['salt'] + value + self.__dict__['salt'] return hashlib.sha3_512(hash_value.encode('utf-8')).hexdigest().\ decode('utf-8') def correct_password(self, value): return self.password == self._make_password(value) @property def favorite_uids(self): return [b.target_id for b in self.favorites] @property def classes_str(self): try: return User.CLASSES[self.info['classes'][0]] except: return u'' @hybrid_property def profile(self): return self.info.get('profile', u'') @profile.setter def set_profile(self, value): self.info['profile'] = value @hybrid_property def signature(self): return self.info.get('signature', u'') @signature.setter def set_signature(self, value): self.info['signature'] = value @property def unread_messages(self): return [m for m in self.received_messages if not m.is_read]
class Article(UIDMixin, Base, Anonymity): RENDER_TYPES = ('html<br />', 'html', 'text') uid = Column(Integer, ForeignKey('Base.uid'), primary_key=True) is_notice = Column(Boolean, nullable=False, default=False, index=True) title = Column(UnicodeText, nullable=False, default=u'') _content = Column('content', UnicodeText, nullable=False, default=u'') render_type = Column(Choice(RENDER_TYPES), nullable=False, default='html<br />') author_id = Column(Integer, ForeignKey('User.uid'), nullable=True, default=None) anonymous = Column(JSONEncodedDict, nullable=True, default=None) view_count = Column(Integer, nullable=False, default=0) created_at = Column(DateTime, nullable=False, default=func.now()) parent_article_id = Column(Integer, ForeignKey('Article.uid', ondelete='set null'), nullable=True, index=True) ancestor_article_id = Column(Integer, ForeignKey('Article.uid', ondelete='set null'), nullable=True, index=True) author = relationship('User', primaryjoin=(author_id == User.uid), backref='articles') boards = relationship( 'Board', secondary='BoardAndArticle', primaryjoin='Article.uid == BoardAndArticle.c.article_id') parent_article = relationship('Article', primaryjoin='Article.\ parent_article_id == Article.uid', remote_side=[uid], backref='child_articles', post_update=True) ancestor_article = relationship('Article', primaryjoin='Article.\ ancestor_article_id == Article.uid', remote_side=[uid], backref='descendant_articles', post_update=True) @hybrid_property def content(self): if self.render_type == 'html<br />': return self._content.replace('\n', '<br />\n') elif self.render_type == 'html': return self._content elif self.render_type == 'text': return escape(self._content).replace('\n', '<br />\n') @content.setter def set_content(self, value): self._content = value @property def author_uid(self): return self.author.uid if self.author else None @property def author_name(self): return self.author.realname if self.author else \ self.anonymous_name @property def is_new(self): return datetime.now() - self.created_at <= timedelta(hours=24) @property def content_for_read(self): # 글 읽기 페이지에선 멘션을 변환하고, 첨부파일 링크해 놓은 걸 변환해야 # 하는 등 여러 작업이 필요하다. 그 작업을 여기서 처리한다. # TODO: 멘션 변환은 지금 글과 댓글에서 모두 쓰고 있고, jinja 필터로 # 들어있다. 변환하는 함수를 따로 만들고, 필터에선 그 함수를 부르는 # 식으로 변경해야 할 듯. file_list = {f.upload_filename: f.uid for f in self.files} def replace_filename_func(match): filename = match.group(2) if filename not in file_list: return match.group() return url_for('main.go_uid', uid=file_list[filename]) content = _find_file_re.sub(replace_filename_func, self.content) return content @property def in_album_board(self): return any([b.board_type == 'album' for b in self.boards]) @property def image_files(self): return [f for f in self.files if f.mime.startswith('image')] def thumbnail_image(self, scale=None, width=0, height=0): images = [f for f in self.files if f.mime.startswith('image')] if len(images) > 0: if scale is not None: return url_for('main.thumbnail', uid=images[0].uid, scale=scale) else: return url_for('main.thumbnail', uid=images[0].uid, width=width, height=height) return None
class Survey(BaseMeta): id = Column(Integer, primary_key=True, autoincrement=True) name = Column(Unicode(255), nullable=False, default=u'') due_date = Column(DateTime, nullable=False, default=None) parent_id = Column(Integer, ForeignKey('Article.uid', ondelete='cascade'), nullable=True) owner_id = Column(Integer, ForeignKey('User.uid', ondelete='set null'), nullable=True) is_anonymous = Column(Boolean, nullable=False, default=False) permission_type = Column(Choice(['all', 'select', 'except', 'firstcome']), nullable=False, default='all') permission_value = Column(Unicode(100), nullable=False, default=u'') expose_level = Column(Integer, nullable=False, default=0) min_vote_num = Column(Integer, nullable=True, default=None) _answered_user = Column('answered_user', UnicodeText, nullable=False, default=u'[]') created_at = Column(DateTime, nullable=False, default=func.now()) parent = relationship('Article', backref=backref('survey', uselist=False)) owner = relationship('User') questions = relationship('SurveyQuestion', order_by='SurveyQuestion.id') @hybrid_property def answered_user(self): return json.loads(self._answered_user) @answered_user.setter def set_answered_user(self, value): assert isinstance(value, (list, tuple)) and \ all(isinstance(n, (int, long)) for n in value) self._answered_user = json.dumps(value) @property def parsed_permission_value(self): if self.permission_type in ('select', 'except'): return [int(year) for year in self.permission_value.split(',')] elif self.permission_type == 'firstcome': return int(self.permission_value) return None @property def is_finished(self): return datetime.now() >= self.due_date or \ (self.permission_type == 'firstcome' and len(self.answered_user) >= self.parsed_permission_value) def votable(self, user): return not self.is_finished and user.uid not in self.answered_user and\ (self.permission_type == 'all' or (self.permission_type == 'select' and user.bs_year in self.parsed_permission_value) or (self.permission_type == 'except' and user.bs_year not in self.parsed_permission_value) or self.permission_type == 'firstcome' ) def visible(self, user): return self.is_finished or \ self.expose_level == 0 or \ self.expose_level == 1 and user.uid in self.answered_user or \ self.expose_level == 2 and datetime.now() >= self.due_date or \ (self.expose_level == 3 and len(self.answered_user) >= self.min_vote_num) @property def answers(self): if getattr(self, '_answers', None) is not None: return self._answers result = [] percentage = (1. / len(self.answered_user)) * 100 \ if len(self.answered_user) > 0 else 0 for i, question in enumerate(self.questions): result.append([]) for _ in range(len(question.examples)): result[i].append({ 'users': [], 'percentage': 0, }) answers = db.query(SurveyAnswer).filter( SurveyAnswer.survey_question_id.in_([q.id for q in self.questions])) for answer in answers: result[answer.survey_question.order][answer.answer]['users'].\ append(answer.user) result[answer.survey_question.order][answer.answer]['percentage'] \ += percentage self._answers = result return result