Beispiel #1
0
    def get(self, url, date, _from_head=False):
        article = Article.get_by_url(url)
        if not article:
            redirect_url = Article.search(date, url)
            if redirect_url:
                self.redirect(CONFIG.BLOG_HOME_RELATIVE_PATH + redirect_url, permanent=True)
                return
            else:
                raise HTTPError(404)

        if not (article.public or self.is_admin):
            raise HTTPError(404)

        previous_article, next_article = article.get_nearby_articles()
        if _from_head or self.request.headers.get('If-None-Match') or self.is_spider:
            hit_count = ArticleHitCount.get(article.id)
        else:
            hit_count = ArticleHitCount.increase(article.id)
        replies = ArticleComments.get_comment_count_of_article(article.id)

        self.set_cache(CONFIG.DEFAULT_CACHE_TIME, is_public=article.public)
        self.render('web/article.html', {
            'title': article.title,
            'page': 'article',
            'article': article,
            'previous_article': previous_article,
            'next_article': next_article,
            'hits': hit_count,
            'replies': replies
        })
Beispiel #2
0
    def get(self, url, date, _from_head=False):
        article = Article.get_by_url(url)
        if not article:
            redirect_url = Article.search(date, url)
            if redirect_url:
                self.redirect(CONFIG.BLOG_HOME_RELATIVE_PATH + redirect_url,
                              permanent=True)
                return
            else:
                raise HTTPError(404)

        if not (article.public or self.is_admin):
            raise HTTPError(404)

        previous_article, next_article = article.get_nearby_articles()
        if _from_head or self.request.headers.get(
                'If-None-Match') or self.is_spider:
            hit_count = ArticleHitCount.get(article.id)
        else:
            hit_count = ArticleHitCount.increase(article.id)
        replies = ArticleComments.get_comment_count_of_article(article.id)

        self.set_cache(CONFIG.DEFAULT_CACHE_TIME, is_public=article.public)
        self.render(
            'web/article.html', {
                'title': article.title,
                'page': 'article',
                'article': article,
                'previous_article': previous_article,
                'next_article': next_article,
                'hits': hit_count,
                'replies': replies
            })
Beispiel #3
0
    def get(self, article_id, order, page):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if not article:
            raise HTTPError(404)
        if not article.public:
            self.set_cache(is_public=False)
            if not self.is_admin:
                raise HTTPError(404)

        page = int(page)

        comments_list = []
        comments, has_next_page = Comment.get_comments_of_article(
            article_id, order == 'asc', page)
        if comments:
            user_ids = {comment.user_id for comment in comments}
            users = User.get_by_ids(user_ids, filter_empty=True)
            if users:
                user_dict = {user.id: user for user in users}
            else:
                user_dict = {}
            for comment in comments:
                user = user_dict.get(comment.user_id)
                if user:
                    user_name = user.name
                    user_site = escape(user.site) if user.site else ''
                else:
                    user_name = u'匿名用户'
                    user_site = ''
                comments_list.append({
                    'user_name':
                    user_name,
                    'url':
                    user_site,
                    'img':
                    user.get_avatar(),
                    'ua':
                    comment.ua,
                    'time':
                    formatted_time(timestamp_to_datetime(comment.time)),
                    'id':
                    comment.id,
                    'content':
                    comment.html_content()
                })

        output = {'comments': comments_list}
        if has_next_page:
            output['next_page'] = page + 1
        self.write_json(output)
Beispiel #4
0
    def get(self):
        self.set_content_type('atom')
        # todo: handler subscribers

        articles = Article.get_articles_for_feed()
        if articles:
            last_updated = iso_time_format(timestamp_to_datetime(articles[0].mod_time))
        else:
            last_updated = iso_time_now()

        self.render('web/feed.xml', {
            'articles': articles,
            'last_updated': last_updated
        })
Beispiel #5
0
    def get(self, article_id):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if article:
            if not article.public:
                self.set_cache(is_public=False)
                if not self.is_admin:
                    raise HTTPError(404)
            self.redirect(CONFIG.BLOG_HOME_RELATIVE_PATH + article.url, permanent=True)
        else:
            raise HTTPError(404)
Beispiel #6
0
    def get(self, article_id):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if article:
            if not article.public:
                self.set_cache(is_public=False)
                if not self.is_admin:
                    raise HTTPError(404)
            self.redirect(CONFIG.BLOG_HOME_RELATIVE_PATH + article.url,
                          permanent=True)
        else:
            raise HTTPError(404)
Beispiel #7
0
    def get(self, article_id, order, page):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if not article:
            raise HTTPError(404)
        if not article.public:
            self.set_cache(is_public=False)
            if not self.is_admin:
                raise HTTPError(404)

        page = int(page)

        comments_list = []
        comments, has_next_page = Comment.get_comments_of_article(article_id, order == 'asc', page)
        if comments:
            user_ids = {comment.user_id for comment in comments}
            users = User.get_by_ids(user_ids, filter_empty=True)
            if users:
                user_dict = {user.id: user for user in users}
            else:
                user_dict = {}
            for comment in comments:
                user = user_dict.get(comment.user_id)
                if user:
                    user_name = user.name
                    user_site = escape(user.site) if user.site else ''
                else:
                    user_name = u'匿名用户'
                    user_site = ''
                comments_list.append({
                    'user_name': user_name,
                    'url': user_site,
                    'img': user.get_avatar(),
                    'ua': comment.ua,
                    'time': formatted_time(timestamp_to_datetime(comment.time)),
                    'id': comment.id,
                    'content': comment.html_content()
                })

        output = {'comments': comments_list}
        if has_next_page:
            output['next_page'] = page + 1
        self.write_json(output)
Beispiel #8
0
    def get(self):
        articles, next_cursor = Article.get_articles_for_homepage(self.cursor)
        if articles:
            article_ids = [article.id for article in articles]
            hit_counts = ArticleHitCount.get_by_ids(article_ids)
            replies_dict = ArticleComments.get_comment_count_of_articles(article_ids)
        else:
            hit_counts = replies_dict = {}

        self.set_cache(CONFIG.DEFAULT_CACHE_TIME, is_public=True)
        self.render('web/home.html', {
            'title': CONFIG.BLOG_TITLE,
            'page': 'home',
            'articles': articles,
            'hit_counts': hit_counts,
            'replies_dict': replies_dict,
            'next_cursor': next_cursor
        })
Beispiel #9
0
    def get(self, article_id):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if not article:
            raise HTTPError(404)

        categories = Category.get_all_names_with_paths()
        tags = Tag.get_all()

        self.render('admin/edit_article.html', {
            'title': u'编辑《%s》' % article.title,
            'page': 'edit_article',
            'article': article,
            'categories': categories,
            'tags': sorted(tags)
        })
Beispiel #10
0
    def get(self):
        articles, next_cursor = Article.get_articles_for_homepage(self.cursor)
        if articles:
            article_ids = [article.id for article in articles]
            hit_counts = ArticleHitCount.get_by_ids(article_ids)
            replies_dict = ArticleComments.get_comment_count_of_articles(
                article_ids)
        else:
            hit_counts = replies_dict = {}

        self.set_cache(CONFIG.DEFAULT_CACHE_TIME, is_public=True)
        self.render(
            'web/home.html', {
                'title': CONFIG.BLOG_TITLE,
                'page': 'home',
                'articles': articles,
                'hit_counts': hit_counts,
                'replies_dict': replies_dict,
                'next_cursor': next_cursor
            })
Beispiel #11
0
    def get(self, page):
        if page:
            page = int(page)
        else:
            page = 1
        articles = Article.get_unpublished_articles(page)
        if articles:
            article_ids = [article.id for article in articles]
            hit_counts = ArticleHitCount.get_by_ids(article_ids)
            replies_dict = ArticleComments.get_comment_count_of_articles(article_ids)
        else:
            hit_counts = replies_dict = {}

        self.render('admin/unpublished_articles.html', {
            'title': u'未发布的文章',
            'page': 'unpublished_articles',
            'articles': articles,
            'hit_counts': hit_counts,
            'replies_dict': replies_dict,
            'next_page': page + 1
        })
Beispiel #12
0
    def get(self):
        keywords = self.get_argument('keywords', None)
        if keywords:
            article_ids = KeywordArticle.query_by_keyword(keywords)
            if article_ids:
                articles = Article.get_by_ids(article_ids, public_only=True)
                article_ids = [article.id for article in articles]
                hit_counts = ArticleHitCount.get_by_ids(article_ids)
                replies_dict = ArticleComments.get_comment_count_of_articles(article_ids)
            else:
                articles = []
                hit_counts = replies_dict = {}

            self.set_cache(CONFIG.DEFAULT_CACHE_TIME, is_public=True)
            self.render('web/search.html', {
                'title': u'搜索《%s》' % keywords,
                'page': 'search',
                'keywords': keywords,
                'articles': articles,
                'hit_counts': hit_counts,
                'replies_dict': replies_dict
            })
        else:
            raise HTTPError(400)
Beispiel #13
0
    def post(self, article_id):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if not article:
            raise HTTPError(404)

        title = self.get_argument('title', None)
        if title:
            article.title = title

        now = None
        pub_time = self.get_argument('pub_time', None)
        if pub_time:
            pub_time = parse_time(pub_time)
        if pub_time:
            pub_timestamp = datetime_to_timestamp(pub_time)
        else:
            pub_time = datetime.utcnow()
            pub_timestamp = now = datetime_to_timestamp(pub_time)
        article.pub_time = pub_timestamp

        mod_time = self.get_argument('mod_time', None)
        if mod_time:
            mod_time = parse_time(mod_time)
        if mod_time:
            mod_timestamp = datetime_to_timestamp(mod_time)
        else:
            mod_timestamp = now
        article.mod_time = mod_timestamp

        url = self.get_argument('url', None)
        # todo: check url pattern
        if not url:
            formatted_date = formatted_date_for_url(pub_time)
            if CONFIG.REPLACE_SPECIAL_CHARACTERS_FOR_URL:
                url = formatted_date + replace_special_characters_for_url(article.title)
            else:
                url = formatted_date + article.title
        if article.url != url:
            if Article.exist_url(url):
                self.finish('编辑失败,同链接的文章已存在')
                return
            else:
                article.url = url

        article.public = self.get_argument('public', None) == 'on'
        bbcode = self.get_argument('bbcode', None) == 'on'
        html = self.get_argument('html', None) == 'on'
        article.format = bbcode * ContentFormatFlag.BBCODE + html * ContentFormatFlag.HTML
        article.content = self.get_argument('content').replace('\r\n', '\n').replace('\r', '\n')

        category_name = self.get_argument('category', None)
        if category_name:
            if not Category.exists(category_name):
                self.finish('发表失败,分类不存在')
                return
        article.category = category_name or None

        tag_names = self.get_arguments('tags')
        if tag_names:
            tag_names = set(tag_names)
            tag_names.discard('')
        article.tags = sorted(tag_names) if tag_names else None

        keywords = self.get_argument('keywords', None)
        if keywords:
            keywords = keywords.lower()
        article.keywords = keywords

        try:
            article.save()
        except IntegrityError:
            quoted_url = article.quoted_url()
            self.finish('编辑失败,已有<a href="%s%s">相同链接的文章</a>存在' % (CONFIG.BLOG_HOME_RELATIVE_PATH, quoted_url))
        except Exception:
            logging.exception('failed to save modified article')
            self.finish('编辑失败')
        else:
            quoted_url = article.quoted_url()
            # 增加时间戳,已避免缓存造成影响,打开后会被 history.replaceState 去掉 query 部分
            self.finish('编辑成功,查看<a href="%s%s?t=%d">更新后的文章</a>' % (CONFIG.BLOG_HOME_RELATIVE_PATH, quoted_url, int(time.time())))
Beispiel #14
0
    def post(self, article_id):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if not (article and (article.public or self.is_admin)):
            raise HTTPError(404)

        content = self.get_argument('comment', None)
        if not content:
            raise HTTPError(400)
        content = content.strip().replace('\r\n', '\n').replace('\r', '\n')
        if not content:
            raise HTTPError(400)

        if self.get_argument('bbcode', None) == 'on':
            format = ContentFormatFlag.BBCODE
        else:
            format = ContentFormatFlag.PLAIN

        ua = []
        browser, platform, os, os_version, vendor = self.ua_details
        if platform:
            if platform in ('iPhone', 'iPod Touch', 'iPad', 'Android'):
                ua.append(platform)
            elif self.is_mobile:
                ua.append('mobile')
        else:
            if self.is_mobile:
                ua.append('mobile')
            elif os and os in ('Windows', 'Mac OS', 'Linux', 'FreeBSD'):
                ua.append(os)
        if browser:
            if browser == 'Internet Explorer':
                ua.append('IE')
            elif browser in ('Firefox', 'Chrome', 'Safari', 'Opera'):
                ua.append(browser)
            elif browser == 'Mobile Safari':
                ua.append('Safari')
            elif browser in ('Opera Mini', 'Opera Mobile'):
                ua.append('Opera')

        # todo: check last comment

        current_user_id = self.current_user_id
        comment = Comment(article_id=article_id,
                          user_id=current_user_id,
                          content=content,
                          format=format,
                          ua=ua,
                          public=True)
        comment.save(inserting=True)

        current_user = self.current_user
        self.write_json({
            'comment': {
                'user_name': current_user.name,
                'url': current_user.site,
                'img': current_user.get_avatar(),
                'ua': comment.ua,
                'time': formatted_time(timestamp_to_datetime(comment.time)),
                'id': comment.id,
                'content': comment.html_content()
            }
        })

        FragmentCache.delete('sidebar')

        if CONFIG.MAILGUN_API_KEY:
            url = html_content = html_body = title = ''

            if CONFIG.ADMIN_EMAIL and not self.is_admin:
                url = u'%s%s#comment-id-%d' % (CONFIG.BLOG_HOME_FULL_URL,
                                               article.quoted_url(),
                                               comment.id)
                html_content = comment.html_content_with_full_url(url)
                html_body = u'%s 在 <a href="%s">%s</a> 评论道:<br/>%s' % (escape(
                    current_user.name), url, article.title, html_content)
                title = u'Re: ' + article.title
                yield send_email(to=CONFIG.ADMIN_EMAIL,
                                 subject=title,
                                 html=html_body)

            if format != ContentFormatFlag.PLAIN:
                if not html_content:
                    url = u'%s%s#comment-id-%d' % (CONFIG.BLOG_HOME_FULL_URL,
                                                   article.quoted_url(),
                                                   comment.id)
                    html_content = comment.html_content_with_full_url(url)
                comment_ids = set(
                    int(comment_id) for comment_id in
                    Comment.REPLY_LINK_PATTERN.findall(html_content))
                if comment_ids:
                    comments = Comment.get_by_ids(comment_ids)
                    if comments:
                        user_ids = {comment.user_id for comment in comments}
                        user_ids.discard(
                            CONFIG.ADMIN_USER_ID)  # already sent to admin
                        if user_ids:
                            users = User.get_by_ids(user_ids)
                            if users:
                                if not html_body:
                                    html_body = u'%s 在 <a href="%s">%s</a> 评论道:<br/>%s<hr/>您收到这封邮件是因为有人回复了您的评论。您可前往原文回复,请勿直接回复该邮件。' % (
                                        escape(current_user.name), url,
                                        article.title, html_content)
                                    title = u'Re: ' + article.title
                                else:
                                    html_body += u'<hr/>您收到这封邮件是因为有人回复了您的评论。您可前往原文回复,请勿直接回复该邮件。'

                                for user in users:
                                    yield send_email(to=user.email,
                                                     subject=title,
                                                     html=html_body)
Beispiel #15
0
# -*- coding: utf-8 -*-

from doodle.core.models.article import Article, ArticleUpdateTime, PublicArticlePublishTime, PrivateArticlePublishTime
from doodle.core.models.category import CategoryArticle
from doodle.core.models.tag import TagArticle
from doodle.core.redis_client import redis_main_client


redis_main_client.delete(PublicArticlePublishTime.KEY, ArticleUpdateTime.KEY, PrivateArticlePublishTime.KEY)
articles_data = redis_main_client.lrange(Article.KEY, 0, -1)
for article_data in articles_data:
    article = Article.from_json(article_data)
    if article.public:
        PublicArticlePublishTime(article_id=article.id, time=article.pub_time).save(redis_main_client)
        ArticleUpdateTime(article_id=article.id, time=article.mod_time).save(redis_main_client)
        PrivateArticlePublishTime(article_id=article.id, time=None).save(redis_main_client)
    else:
        PublicArticlePublishTime(article_id=article.id, time=None).save(redis_main_client)
        ArticleUpdateTime(article_id=article.id, time=None).save(redis_main_client)
        PrivateArticlePublishTime(article_id=article.id, time=article.pub_time).save(redis_main_client)

    if article.category:
        CategoryArticle(category=article.category, article_id=article.id, time=article.pub_time).save(redis_main_client, inserting=True)

    for tag_name in article.tags:
        TagArticle(tag=tag_name, article_id=article.id, time=article.pub_time).save(redis_main_client, inserting=True)
Beispiel #16
0
    def post(self, article_id):
        article_id = int(article_id)
        if not article_id:
            raise HTTPError(404)

        article = Article.get_by_id(article_id)
        if not (article and (article.public or self.is_admin)):
            raise HTTPError(404)

        content = self.get_argument('comment', None)
        if not content:
            raise HTTPError(400)
        content = content.strip().replace('\r\n', '\n').replace('\r', '\n')
        if not content:
            raise HTTPError(400)

        if self.get_argument('bbcode', None) == 'on':
            format = ContentFormatFlag.BBCODE
        else:
            format = ContentFormatFlag.PLAIN

        ua = []
        browser, platform, os, os_version, vendor = self.ua_details
        if platform:
            if platform in ('iPhone', 'iPod Touch', 'iPad', 'Android'):
                ua.append(platform)
            elif self.is_mobile:
                ua.append('mobile')
        else:
            if self.is_mobile:
                ua.append('mobile')
            elif os and os in ('Windows', 'Mac OS', 'Linux', 'FreeBSD'):
                ua.append(os)
        if browser:
            if browser == 'Internet Explorer':
                ua.append('IE')
            elif browser in ('Firefox', 'Chrome', 'Safari', 'Opera'):
                ua.append(browser)
            elif browser == 'Mobile Safari':
                ua.append('Safari')
            elif browser in ('Opera Mini', 'Opera Mobile'):
                ua.append('Opera')

        # todo: check last comment

        current_user_id = self.current_user_id
        comment = Comment(
            article_id=article_id,
            user_id=current_user_id,
            content=content,
            format=format,
            ua=ua,
            public=True
        )
        comment.save(inserting=True)

        current_user = self.current_user
        self.write_json({
            'comment': {
                'user_name': current_user.name,
                'url': current_user.site,
                'img': current_user.get_avatar(),
                'ua': comment.ua,
                'time': formatted_time(timestamp_to_datetime(comment.time)),
                'id': comment.id,
                'content': comment.html_content()
            }
        })

        FragmentCache.delete('sidebar')

        if CONFIG.MAILGUN_API_KEY:
            url = html_content = html_body = title = ''

            if CONFIG.ADMIN_EMAIL and not self.is_admin:
                url = u'%s%s#comment-id-%d' % (CONFIG.BLOG_HOME_FULL_URL, article.quoted_url(), comment.id)
                html_content = comment.html_content_with_full_url(url)
                html_body = u'%s 在 <a href="%s">%s</a> 评论道:<br/>%s' % (escape(current_user.name), url, article.title, html_content)
                title = u'Re: ' + article.title
                yield send_email(to=CONFIG.ADMIN_EMAIL, subject=title, html=html_body)

            if format != ContentFormatFlag.PLAIN:
                if not html_content:
                    url = u'%s%s#comment-id-%d' % (CONFIG.BLOG_HOME_FULL_URL, article.quoted_url(), comment.id)
                    html_content = comment.html_content_with_full_url(url)
                comment_ids = set(int(comment_id) for comment_id in Comment.REPLY_LINK_PATTERN.findall(html_content))
                if comment_ids:
                    comments = Comment.get_by_ids(comment_ids)
                    if comments:
                        user_ids = {comment.user_id for comment in comments}
                        user_ids.discard(CONFIG.ADMIN_USER_ID)  # already sent to admin
                        if user_ids:
                            users = User.get_by_ids(user_ids)
                            if users:
                                if not html_body:
                                    html_body = u'%s 在 <a href="%s">%s</a> 评论道:<br/>%s<hr/>您收到这封邮件是因为有人回复了您的评论。您可前往原文回复,请勿直接回复该邮件。' % (escape(current_user.name), url, article.title, html_content)
                                    title = u'Re: ' + article.title
                                else:
                                    html_body += u'<hr/>您收到这封邮件是因为有人回复了您的评论。您可前往原文回复,请勿直接回复该邮件。'

                                for user in users:
                                    yield send_email(to=user.email, subject=title, html=html_body)
Beispiel #17
0
    def post(self):
        title = self.get_argument('title', None)
        if not title:
            self.finish('发表失败,标题不能为空')
            return

        pub_time = self.get_argument('pub_time', None)
        if pub_time:
            pub_time = parse_time(pub_time)
        if not pub_time:
            pub_time = datetime.utcnow()
        pub_timestamp = datetime_to_timestamp(pub_time)

        mod_time = self.get_argument('mod_time', None)
        if mod_time:
            mod_time = parse_time(mod_time)
        if not mod_time:
            mod_timestamp = pub_timestamp
        else:
            mod_timestamp = datetime_to_timestamp(mod_time)

        url = self.get_argument('url', None)
        # todo: check url pattern
        if not url:
            formatted_date = formatted_date_for_url(pub_time)
            if CONFIG.REPLACE_SPECIAL_CHARACTERS_FOR_URL:
                url = formatted_date + replace_special_characters_for_url(title)
            else:
                url = formatted_date + title
        if Article.exist_url(url):
            self.finish('发表失败,同链接的文章已存在')
            return

        public = self.get_argument('public', None) == 'on'
        bbcode = self.get_argument('bbcode', None) == 'on'
        html = self.get_argument('html', None) == 'on'
        format = bbcode * ContentFormatFlag.BBCODE + html * ContentFormatFlag.HTML
        content = self.get_argument('content').replace('\r\n', '\n').replace('\r', '\n')

        category_name = self.get_argument('category', None)
        if category_name:
            if not Category.exists(category_name):
                self.finish('发表失败,分类不存在')
                return

        tag_names = self.get_arguments('tags')
        if tag_names:
            tag_names = set(tag_names)
            tag_names.discard('')

        keywords = self.get_argument('keywords', None)
        if keywords:
            keywords = keywords.lower()

        article = Article(
            title=title,
            url=url,
            content=content,
            format=format,
            category=category_name or None,
            tags=sorted(tag_names) if tag_names else None,
            keywords=keywords,
            public=public,
            pub_time=pub_timestamp,
            mod_time=mod_timestamp
        )
        try:
            article.save(inserting=True)
        except IntegrityError:
            quoted_url = article.quoted_url()
            self.finish('发表失败,已有<a href="%s%s">相同链接的文章</a>存在' % (CONFIG.BLOG_HOME_RELATIVE_PATH, quoted_url))
        except Exception:
            logging.exception('failed to save new article')
            self.finish('发表失败')
        else:
            quoted_url = article.quoted_url()
            self.finish('发表成功,查看<a href="%s%s">发表后的文章</a>' % (CONFIG.BLOG_HOME_RELATIVE_PATH, quoted_url))
Beispiel #18
0
    def test_save_and_get_by_id(self):
        self.assertIsNone(Article.get_by_id(1))

        now = int(time.time())
        article = Article(
            title='test title',
            url='test-url',
            content=u'测试',
            category='test',
            tags=('test1', 'test2'),
        )
        self.assertRaises(PropertyError, article.save)
        article.save(inserting=True)
        self.assertEqual(article.id, 1)

        saved_article = Article.get_by_id(1)
        self.assertEqual(article, saved_article)
        self.assertGreaterEqual(saved_article.pub_time, now)
        self.assertGreaterEqual(saved_article.mod_time, now)
        self.assertIsNone(Article.get_by_id(2))

        saved_article.title = u'测试'
        saved_article.save()
        saved_article = Article.get_by_id(1)
        self.assertEqual(saved_article.title, u'测试')
        self.assertIsNone(Article.get_by_id(2))
        self.assertRaises(PropertyError, saved_article.save, inserting=True)

        article2 = Article(
            title='test',
            category='test',
            tags=('test1', 'test2'),
            pub_time=1,
            mod_time=2
        )
        article2.save(inserting=True)
        self.assertEqual(article2.id, 2)
        saved_article2 = Article.get_by_id(2)
        self.assertEqual(article2, saved_article2)