├── .gitignore ├── QsBlog ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py ├── README.md ├── blog ├── __init__.py ├── admin.py ├── adminx.py ├── apps.py ├── models.py ├── search_indexes.py ├── static │ ├── code.css │ └── style.css ├── tests.py ├── views.py └── whoosh_cn_backend.py ├── comments ├── __init__.py ├── admin.py ├── apps.py ├── forms.py ├── models.py ├── tests.py ├── urls.py └── views.py ├── github_pic ├── admin.png ├── detail.png ├── editor.png ├── index.png ├── pic_upload.png └── tag.png ├── manage.py ├── requirements.txt └── templates ├── about.html ├── base.html ├── blog.html ├── comment.html ├── footer.html ├── header.html ├── index.html ├── search ├── indexes │ └── blog │ │ └── blog_text.txt └── search.html ├── tag.html ├── tag_detail.html └── valine.html /.gitignore: -------------------------------------------------------------------------------- 1 | /blog/migrations/ 2 | /comments/migrations/ 3 | /whoosh_index/ 4 | /venv/ 5 | /.idea/ 6 | */.DS_Store -------------------------------------------------------------------------------- /QsBlog/__init__.py: -------------------------------------------------------------------------------- 1 | import pymysql 2 | 3 | pymysql.install_as_MySQLdb() -------------------------------------------------------------------------------- /QsBlog/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for QsBlog project. 3 | 4 | Generated by 'django-admin startproject' using Django 2.1. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/2.1/ref/settings/ 11 | """ 12 | 13 | import os 14 | 15 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/2.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '+@u+#&3iacnv2mu!m@q)1l%2aa@1@0w$h7)t7kr&c#&_en4lpq' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | "compressor", 41 | 'haystack', 42 | 'xadmin', 43 | 'crispy_forms', 44 | 'reversion', 45 | 'simditor', 46 | 'comments', 47 | 'blog', 48 | ] 49 | 50 | MIDDLEWARE = [ 51 | 'django.middleware.security.SecurityMiddleware', 52 | 'django.contrib.sessions.middleware.SessionMiddleware', 53 | 'django.middleware.common.CommonMiddleware', 54 | 'django.middleware.csrf.CsrfViewMiddleware', 55 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 56 | 'django.contrib.messages.middleware.MessageMiddleware', 57 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 58 | ] 59 | 60 | ROOT_URLCONF = 'QsBlog.urls' 61 | 62 | TEMPLATES = [ 63 | { 64 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 | 'DIRS': [os.path.join(BASE_DIR, 'templates')] 66 | , 67 | 'APP_DIRS': True, 68 | 'OPTIONS': { 69 | 'context_processors': [ 70 | 'django.template.context_processors.debug', 71 | 'django.template.context_processors.request', 72 | 'django.contrib.auth.context_processors.auth', 73 | 'django.contrib.messages.context_processors.messages', 74 | ], 75 | }, 76 | }, 77 | ] 78 | 79 | WSGI_APPLICATION = 'QsBlog.wsgi.application' 80 | 81 | 82 | # Database 83 | # https://docs.djangoproject.com/en/2.1/ref/settings/#databases 84 | 85 | DATABASES = { 86 | 'default': { 87 | 'ENGINE': 'django.db.backends.mysql', 88 | 'NAME': 'qsblog', 89 | 'USER':'root', 90 | 'PASSWORD':'Qxx980225QXx', 91 | 'HOST':'localhost', 92 | 'PORT':'3306', 93 | 94 | } 95 | } 96 | 97 | 98 | # Password validation 99 | # https://docs.djangoproject.com/en/2.1/ref/settings/#auth-password-validators 100 | 101 | AUTH_PASSWORD_VALIDATORS = [ 102 | { 103 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 104 | }, 105 | { 106 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 107 | }, 108 | { 109 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 110 | }, 111 | { 112 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 113 | }, 114 | ] 115 | 116 | 117 | # Internationalization 118 | # https://docs.djangoproject.com/en/2.1/topics/i18n/ 119 | 120 | LANGUAGE_CODE = 'zh-Hans' 121 | 122 | TIME_ZONE = 'UTC' 123 | 124 | USE_I18N = True 125 | 126 | USE_L10N = True 127 | 128 | USE_TZ = True 129 | 130 | 131 | # Static files (CSS, JavaScript, Images) 132 | # https://docs.djangoproject.com/en/2.1/howto/static-files/ 133 | 134 | STATIC_URL = '/static/' 135 | MEDIA_URL = '/media/' 136 | 137 | STATICFILES_FINDERS = ( 138 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 139 | 'django.contrib.staticfiles.finders.FileSystemFinder', 140 | 'compressor.finders.CompressorFinder', 141 | ) 142 | COMPRESS_OFFLINE = True 143 | COMPRESS_ENABLED = True 144 | # Static ROOT (CSS, JavaScript, Images) 145 | STATIC_ROOT = os.path.join(BASE_DIR, 'static') 146 | MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 147 | 148 | 149 | # Haystack Search Settings 150 | HAYSTACK_CONNECTIONS = { 151 | 'default': { 152 | 'ENGINE': 'blog.whoosh_cn_backend.WhooshEngine', 153 | 'PATH': os.path.join(BASE_DIR, 'whoosh_index'), 154 | }, 155 | } 156 | HAYSTACK_SEARCH_RESULTS_PER_PAGE = 10 157 | HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor' 158 | 159 | 160 | # Simditor Markdown Settings 161 | SIMDITOR_UPLOAD_PATH = 'uploads/' 162 | SIMDITOR_IMAGE_BACKEND = 'pillow' 163 | 164 | SIMDITOR_TOOLBAR = [ 165 | 'title', 'bold', 'italic', 'underline', 'strikethrough', 'fontScale', 166 | 'color', '|', 'ol', 'ul', 'blockquote', 'code', 'table', '|', 'link', 167 | 'image', 'hr', '|', 'indent', 'outdent', 'alignment', 'fullscreen', 168 | 'markdown', 169 | ] 170 | 171 | SIMDITOR_CONFIGS = { 172 | 'toolbar': SIMDITOR_TOOLBAR, 173 | 'upload': { 174 | 'url': '/simditor/upload/', 175 | 'fileKey': 'upload' 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /QsBlog/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.conf.urls import * 3 | from blog.views import * 4 | import xadmin 5 | from django.conf import settings 6 | from django.conf.urls.static import static 7 | 8 | admin.autodiscover() 9 | urlpatterns = [ 10 | # Admin 11 | # url(r'^admin/', admin.site.urls), 12 | url(r'^admin/', xadmin.site.urls), 13 | url(r'^simditor/', include('simditor.urls')), 14 | 15 | # Index BlogList 16 | url(r'^$', BlogListView.as_view(), name='index'), 17 | 18 | # Tag 19 | url(r'^tag/$', TagView.as_view(), name='tag_list'), 20 | url(r'^tag/(?P[a-zA-Z0-9])+/$', TagDetailView.as_view(), name='tag_detail'), 21 | 22 | # About 23 | url(r'^about/', about, name='about'), 24 | 25 | # RSS 26 | url(r'^feed/$', RSSFeed(), name="RSS"), 27 | 28 | # Blog Detail Page 29 | url(r'^blog/(?P[0-9])+/$', BlogDetailView.as_view(), name="blog_detail"), 30 | 31 | # Haystack Search 32 | url(r'^search/', include('haystack.urls')), 33 | 34 | url(r'', include('comments.urls')), 35 | ] 36 | 37 | 38 | # Simditor Markdown Upload 39 | urlpatterns += static( 40 | settings.STATIC_URL, 41 | document_root=settings.STATIC_ROOT 42 | ) 43 | 44 | urlpatterns += static( 45 | settings.MEDIA_URL, 46 | document_root=settings.MEDIA_ROOT 47 | ) 48 | -------------------------------------------------------------------------------- /QsBlog/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for QsBlog project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/2.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'QsBlog.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Django Blog 2 | 3 | 基于 Django 开发的博客系统: 4 | 5 | - Python 3.6.6 和 Django 2.1.4 6 | - MySQL 7 | - [xadmin](https://github.com/sshwsfc/xadmin) 后台管理 8 | - [Simditor Markdown](https://github.com/istommao/django-simditor) 编辑器,图片 Drag and Drop 上传 9 | - 代码高亮 10 | - RSS订阅 11 | - 标签、阅读量 12 | - [haystack](https://github.com/django-haystack/django-haystack) 文章内容搜索 13 | - [Valine](https://github.com/xCss/Valine) 评论系统 14 | - 集成 django-compressor,静态文件压缩 15 | 16 | Usage: 17 | 18 | - 新建虚拟环境 19 | 20 | ``` 21 | git clone git@github.com:chiuxingxiang/Django-Blog.git 22 | virtualenv --python= venv 23 | . venv/bin/activate 24 | ``` 25 | 26 | - 安装依赖 27 | 28 | ``` 29 | pip install -r requirements.txt 30 | ``` 31 | 32 | - 数据库迁移 33 | 34 | ``` 35 | python manage.py makemigrations 36 | python manage.py migrate 37 | ``` 38 | 39 | - 创建管理员 40 | 41 | ``` 42 | python manage.py shell 43 | from django.contrib.auth.models import User 44 | user=User.objects.create_superuser('用户名','邮箱','密码') 45 | ``` 46 | 47 | - 创建搜索索引 48 | 49 | ``` 50 | python manage.py rebuild_index 51 | ``` 52 | 53 | - 压缩静态文件 54 | 55 | ``` 56 | python manage.py collectstatic 57 | python manage.py compress 58 | ``` 59 | ------ 60 | 61 | ### 首页 62 | 63 | ![index](/github_pic/index.png) 64 | 65 | ### 详情页 + 评论 66 | 67 | ![detail](/github_pic/detail.png) 68 | 69 | ### Tag List 70 | 71 | ![tag_list](/github_pic/tag.png) 72 | 73 | ### xadmin后台 74 | 75 | ![admin](/github_pic/admin.png) 76 | 77 | ### Simditor Markdown 文章编辑器 图片上传 78 | 79 | ![pic_upload](/github_pic/pic_upload.png) 80 | -------------------------------------------------------------------------------- /blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/blog/__init__.py -------------------------------------------------------------------------------- /blog/admin.py: -------------------------------------------------------------------------------- 1 | from blog.models import * 2 | from django.contrib import admin 3 | 4 | 5 | @admin.register(Blog) 6 | class BlogAdmin(admin.ModelAdmin): 7 | list_display = ('id', 'title', 'views', 'tags_list', 'created_time') 8 | list_display_links = ('title',) 9 | list_per_page = 20 10 | ordering = ('-created_time',) 11 | list_filter = ('tags',) 12 | readonly_fields = ['views'] 13 | search_fields = ('title',) 14 | 15 | filter_horizontal = ('tags',) 16 | fieldsets = ( 17 | ("base info", {'fields': ['title', 'author', 'tags']}), 18 | ("Content", {'fields': ['content']}) 19 | ) 20 | 21 | @staticmethod 22 | def tags_list(blog): 23 | tags = map(lambda x: x.name, blog.tags.all()) 24 | return ', '.join(tags) 25 | 26 | 27 | class TagAdmin(object): 28 | list_display = ('name',) 29 | 30 | 31 | admin.site.register(Tag) 32 | -------------------------------------------------------------------------------- /blog/adminx.py: -------------------------------------------------------------------------------- 1 | from blog.models import * 2 | import xadmin 3 | from xadmin import views 4 | from comments.models import Comment 5 | 6 | 7 | class GlobalSetting(object): 8 | site_title = 'Q\'s BLog' 9 | site_footer = 'Copyright 2013 - 2018 by Q.' 10 | 11 | def get_site_menu(self): 12 | return ( 13 | {'title': 'Blog', 'perm': self.get_model_perm(Blog, 'change'), 'menus': ( 14 | {'title': 'Blog', 'url': self.get_model_url(Blog, 'changelist')}, 15 | {'title': 'Tag', 'url': self.get_model_url(Tag, 'changelist')}, 16 | {'title': 'Comment', 'url': self.get_model_url(Comment, 'changelist')}, 17 | )}, 18 | ) 19 | 20 | def get_nav_menu(self): 21 | # 直接返回新增的菜单栏,源码中还有很大一部分的合并功能 22 | site_menu = list(self.get_site_menu() or []) 23 | return site_menu 24 | 25 | 26 | class BlogAdmin(object): 27 | list_display = ('id', 'title', 'views', 'tags_list', 'created_time') 28 | list_display_links = ('title',) 29 | list_per_page = 20 30 | ordering = ('-created_time',) 31 | list_filter = ('tags',) 32 | readonly_fields = ['views'] 33 | search_fields = ('title',) 34 | 35 | @staticmethod 36 | def tags_list(blog): 37 | tags = map(lambda x: x.name, blog.tags.all()) 38 | return ', '.join(tags) 39 | 40 | 41 | xadmin.site.register(views.CommAdminView, GlobalSetting) 42 | xadmin.site.register(Blog, BlogAdmin) 43 | xadmin.site.register(Tag) 44 | xadmin.site.register(Comment) 45 | -------------------------------------------------------------------------------- /blog/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class BlogConfig(AppConfig): 5 | name = 'blog' 6 | -------------------------------------------------------------------------------- /blog/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | from django.contrib.auth.models import User 3 | from simditor.fields import RichTextField 4 | 5 | 6 | class Tag(models.Model): 7 | name = models.CharField('标签', max_length=20, unique=True) 8 | 9 | class Meta: 10 | verbose_name_plural = verbose_name = '标签' 11 | 12 | def __str__(self): 13 | return self.name 14 | 15 | 16 | class Blog(models.Model): 17 | id = models.AutoField(primary_key=True) 18 | author = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name='作者') 19 | title = models.CharField('标题', max_length=50) 20 | content = RichTextField('内容') 21 | created_time = models.DateTimeField('创建日期') 22 | tags = models.ManyToManyField(Tag, verbose_name='标签') 23 | views = models.PositiveIntegerField(default=0) 24 | 25 | class Meta: 26 | verbose_name_plural = verbose_name = '文章' 27 | ordering = ['-created_time'] 28 | 29 | def count_views(self): 30 | self.views += 1 31 | self.save(update_fields=['views']) 32 | 33 | def get_absolute_url(self): 34 | return "/blog/" + str(self.id) 35 | 36 | def pre_blog(self): 37 | return Blog.objects.filter(id__lt=self.id).first() 38 | 39 | def next_blog(self): 40 | return Blog.objects.filter(id__gt=self.id).first() 41 | 42 | def __str__(self): 43 | return self.title 44 | -------------------------------------------------------------------------------- /blog/search_indexes.py: -------------------------------------------------------------------------------- 1 | from haystack import indexes 2 | from .models import Blog 3 | 4 | 5 | class PostIndex(indexes.SearchIndex, indexes.Indexable): 6 | text = indexes.CharField(document=True, use_template=True) 7 | 8 | def get_model(self): 9 | return Blog 10 | 11 | def index_queryset(self, using=None): 12 | return self.get_model().objects.all() 13 | -------------------------------------------------------------------------------- /blog/static/code.css: -------------------------------------------------------------------------------- 1 | .codehilite .hll { background-color: #ffffcc } 2 | .codehilite { background: #f8f8f8; } 3 | .codehilite .c { color: #408080; font-style: italic } /* Comment */ 4 | .codehilite .err { border: 1px solid #FF0000 } /* Error */ 5 | .codehilite .k { color: #008000; font-weight: bold } /* Keyword */ 6 | .codehilite .o { color: #666666 } /* Operator */ 7 | .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */ 8 | .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */ 9 | .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */ 10 | .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */ 11 | .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */ 12 | .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */ 13 | .codehilite .gd { color: #A00000 } /* Generic.Deleted */ 14 | .codehilite .ge { font-style: italic } /* Generic.Emph */ 15 | .codehilite .gr { color: #FF0000 } /* Generic.Error */ 16 | .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 17 | .codehilite .gi { color: #00A000 } /* Generic.Inserted */ 18 | .codehilite .go { color: #888888 } /* Generic.Output */ 19 | .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ 20 | .codehilite .gs { font-weight: bold } /* Generic.Strong */ 21 | .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 22 | .codehilite .gt { color: #0044DD } /* Generic.Traceback */ 23 | .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ 24 | .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ 25 | .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ 26 | .codehilite .kp { color: #008000 } /* Keyword.Pseudo */ 27 | .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ 28 | .codehilite .kt { color: #B00040 } /* Keyword.Type */ 29 | .codehilite .m { color: #666666 } /* Literal.Number */ 30 | .codehilite .s { color: #BA2121 } /* Literal.String */ 31 | .codehilite .na { color: #7D9029 } /* Name.Attribute */ 32 | .codehilite .nb { color: #008000 } /* Name.Builtin */ 33 | .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */ 34 | .codehilite .no { color: #880000 } /* Name.Constant */ 35 | .codehilite .nd { color: #AA22FF } /* Name.Decorator */ 36 | .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */ 37 | .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ 38 | .codehilite .nf { color: #0000FF } /* Name.Function */ 39 | .codehilite .nl { color: #A0A000 } /* Name.Label */ 40 | .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ 41 | .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */ 42 | .codehilite .nv { color: #19177C } /* Name.Variable */ 43 | .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ 44 | .codehilite .w { color: #bbbbbb } /* Text.Whitespace */ 45 | .codehilite .mb { color: #666666 } /* Literal.Number.Bin */ 46 | .codehilite .mf { color: #666666 } /* Literal.Number.Float */ 47 | .codehilite .mh { color: #666666 } /* Literal.Number.Hex */ 48 | .codehilite .mi { color: #666666 } /* Literal.Number.Integer */ 49 | .codehilite .mo { color: #666666 } /* Literal.Number.Oct */ 50 | .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */ 51 | .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */ 52 | .codehilite .sc { color: #BA2121 } /* Literal.String.Char */ 53 | .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */ 54 | .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ 55 | .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */ 56 | .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ 57 | .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */ 58 | .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ 59 | .codehilite .sx { color: #008000 } /* Literal.String.Other */ 60 | .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */ 61 | .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */ 62 | .codehilite .ss { color: #19177C } /* Literal.String.Symbol */ 63 | .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */ 64 | .codehilite .fm { color: #0000FF } /* Name.Function.Magic */ 65 | .codehilite .vc { color: #19177C } /* Name.Variable.Class */ 66 | .codehilite .vg { color: #19177C } /* Name.Variable.Global */ 67 | .codehilite .vi { color: #19177C } /* Name.Variable.Instance */ 68 | .codehilite .vm { color: #19177C } /* Name.Variable.Magic */ 69 | .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */ 70 | -------------------------------------------------------------------------------- /blog/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /blog/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import get_object_or_404, render 2 | from django.views.generic import ListView, DetailView 3 | from django.contrib.syndication.views import Feed 4 | from .models import * 5 | from comments.forms import CommentForm 6 | 7 | 8 | # def blog_detail(request, id): 9 | # blog = get_object_or_404(Blog, id=id) 10 | # blog.count_views() 11 | # return render(request, 'blog.html', context={'blog': blog}) 12 | 13 | def about(request): 14 | return render(request, 'about.html') 15 | 16 | 17 | class BlogListView(ListView): 18 | model = Blog 19 | context_object_name = 'blogs' 20 | template_name = 'index.html' 21 | 22 | 23 | class BlogDetailView(DetailView): 24 | model = Blog 25 | template_name = 'blog.html' 26 | context_object_name = 'blog' 27 | 28 | def get_object(self, queryset=None): 29 | blog = super(BlogDetailView, self).get_object() 30 | blog.count_views() 31 | return blog 32 | 33 | def get_context_data(self, **kwargs): 34 | context = super(BlogDetailView, self).get_context_data(**kwargs) 35 | form = CommentForm() 36 | comment_list = self.object.comment_set.all() 37 | 38 | temp = Blog.objects.get(id=self.kwargs.get('pk')) 39 | pre_blog = temp.pre_blog() 40 | next_blog = temp.next_blog() 41 | 42 | context.update({ 43 | 'form': form, 44 | 'comment_list': comment_list, 45 | 'pre_blog': pre_blog, 46 | 'next_blog': next_blog, 47 | }) 48 | return context 49 | 50 | 51 | class TagView(ListView): 52 | model = Blog 53 | context_object_name = 'tag_list' 54 | template_name = 'tag.html' 55 | 56 | def get_context_data(self, *, object_list=None, **kwargs): 57 | context = super(TagView, self).get_context_data(**kwargs) 58 | tags_dic = {} 59 | tags = Tag.objects.all() 60 | 61 | for tag in tags: 62 | tags_dic[tag.name] = list(Blog.objects.get_queryset().filter(tags=tag).values('id', 'title', 'created_time')) 63 | 64 | context['tags_dic'] = tags_dic 65 | print(context['tags_dic']) 66 | return context 67 | 68 | 69 | class TagDetailView(ListView): 70 | model = Blog 71 | context_object_name = 'blogs' 72 | template_name = 'tag_detail.html' 73 | 74 | def get_queryset(self): 75 | tag = get_object_or_404(Tag, id=self.kwargs.get('id')) 76 | return super(TagDetailView, self).get_queryset().filter(tags=tag) 77 | 78 | def get_context_data(self, *, object_list=None, **kwargs): 79 | context = super().get_context_data(**kwargs) 80 | tag = get_object_or_404(Tag, id=self.kwargs.get('id')) 81 | context['tag_name'] = tag.name 82 | return context 83 | 84 | 85 | class RSSFeed(Feed): 86 | title = "Q's Blog" 87 | link = "feeds/" 88 | description = "RSS feed - blog posts" 89 | 90 | @staticmethod 91 | def items(): 92 | return Blog.objects.order_by('-created_time') 93 | 94 | def item_title(self, item): 95 | return item.title 96 | 97 | def item_description(self, item): 98 | return item.content 99 | -------------------------------------------------------------------------------- /blog/whoosh_cn_backend.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | from __future__ import absolute_import, division, print_function, unicode_literals 4 | 5 | import json 6 | import os 7 | import re 8 | import shutil 9 | import threading 10 | import warnings 11 | 12 | from django.conf import settings 13 | from django.core.exceptions import ImproperlyConfigured 14 | from django.utils import six 15 | from django.utils.datetime_safe import datetime 16 | from django.utils.encoding import force_text 17 | 18 | from haystack.backends import BaseEngine, BaseSearchBackend, BaseSearchQuery, EmptyResults, log_query 19 | from haystack.constants import DJANGO_CT, DJANGO_ID, ID 20 | from haystack.exceptions import MissingDependency, SearchBackendError, SkipDocument 21 | from haystack.inputs import Clean, Exact, PythonData, Raw 22 | from haystack.models import SearchResult 23 | from haystack.utils import log as logging 24 | from haystack.utils import get_identifier, get_model_ct 25 | from haystack.utils.app_loading import haystack_get_model 26 | 27 | try: 28 | import whoosh 29 | except ImportError: 30 | raise MissingDependency("The 'whoosh' backend requires the installation of 'Whoosh'. Please refer to the documentation.") 31 | 32 | # Handle minimum requirement. 33 | if not hasattr(whoosh, '__version__') or whoosh.__version__ < (2, 5, 0): 34 | raise MissingDependency("The 'whoosh' backend requires version 2.5.0 or greater.") 35 | 36 | # Bubble up the correct error. 37 | from whoosh import index 38 | from whoosh.analysis import StemmingAnalyzer 39 | from whoosh.fields import ID as WHOOSH_ID 40 | from whoosh.fields import BOOLEAN, DATETIME, IDLIST, KEYWORD, NGRAM, NGRAMWORDS, NUMERIC, Schema, TEXT 41 | from whoosh.filedb.filestore import FileStorage, RamStorage 42 | from whoosh.highlight import highlight as whoosh_highlight 43 | from whoosh.highlight import ContextFragmenter, HtmlFormatter 44 | from whoosh.qparser import QueryParser 45 | from whoosh.searching import ResultsPage 46 | from whoosh.writing import AsyncWriter 47 | from jieba.analyse import ChineseAnalyzer 48 | 49 | 50 | DATETIME_REGEX = re.compile('^(?P\d{4})-(?P\d{2})-(?P\d{2})T(?P\d{2}):(?P\d{2}):(?P\d{2})(\.\d{3,6}Z?)?$') 51 | LOCALS = threading.local() 52 | LOCALS.RAM_STORE = None 53 | 54 | 55 | class WhooshHtmlFormatter(HtmlFormatter): 56 | """ 57 | This is a HtmlFormatter simpler than the whoosh.HtmlFormatter. 58 | We use it to have consistent results across backends. Specifically, 59 | Solr, Xapian and Elasticsearch are using this formatting. 60 | """ 61 | template = '<%(tag)s>%(t)s' 62 | 63 | 64 | class WhooshSearchBackend(BaseSearchBackend): 65 | # Word reserved by Whoosh for special use. 66 | RESERVED_WORDS = ( 67 | 'AND', 68 | 'NOT', 69 | 'OR', 70 | 'TO', 71 | ) 72 | 73 | # Characters reserved by Whoosh for special use. 74 | # The '\\' must come first, so as not to overwrite the other slash replacements. 75 | RESERVED_CHARACTERS = ( 76 | '\\', '+', '-', '&&', '||', '!', '(', ')', '{', '}', 77 | '[', ']', '^', '"', '~', '*', '?', ':', '.', 78 | ) 79 | 80 | def __init__(self, connection_alias, **connection_options): 81 | super(WhooshSearchBackend, self).__init__(connection_alias, **connection_options) 82 | self.setup_complete = False 83 | self.use_file_storage = True 84 | self.post_limit = getattr(connection_options, 'POST_LIMIT', 128 * 1024 * 1024) 85 | self.path = connection_options.get('PATH') 86 | 87 | if connection_options.get('STORAGE', 'file') != 'file': 88 | self.use_file_storage = False 89 | 90 | if self.use_file_storage and not self.path: 91 | raise ImproperlyConfigured("You must specify a 'PATH' in your settings for connection '%s'." % connection_alias) 92 | 93 | self.log = logging.getLogger('haystack') 94 | 95 | def setup(self): 96 | """ 97 | Defers loading until needed. 98 | """ 99 | from haystack import connections 100 | new_index = False 101 | 102 | # Make sure the index is there. 103 | if self.use_file_storage and not os.path.exists(self.path): 104 | os.makedirs(self.path) 105 | new_index = True 106 | 107 | if self.use_file_storage and not os.access(self.path, os.W_OK): 108 | raise IOError("The path to your Whoosh index '%s' is not writable for the current user/group." % self.path) 109 | 110 | if self.use_file_storage: 111 | self.storage = FileStorage(self.path) 112 | else: 113 | global LOCALS 114 | 115 | if getattr(LOCALS, 'RAM_STORE', None) is None: 116 | LOCALS.RAM_STORE = RamStorage() 117 | 118 | self.storage = LOCALS.RAM_STORE 119 | 120 | self.content_field_name, self.schema = self.build_schema(connections[self.connection_alias].get_unified_index().all_searchfields()) 121 | self.parser = QueryParser(self.content_field_name, schema=self.schema) 122 | 123 | if new_index is True: 124 | self.index = self.storage.create_index(self.schema) 125 | else: 126 | try: 127 | self.index = self.storage.open_index(schema=self.schema) 128 | except index.EmptyIndexError: 129 | self.index = self.storage.create_index(self.schema) 130 | 131 | self.setup_complete = True 132 | 133 | def build_schema(self, fields): 134 | schema_fields = { 135 | ID: WHOOSH_ID(stored=True, unique=True), 136 | DJANGO_CT: WHOOSH_ID(stored=True), 137 | DJANGO_ID: WHOOSH_ID(stored=True), 138 | } 139 | # Grab the number of keys that are hard-coded into Haystack. 140 | # We'll use this to (possibly) fail slightly more gracefully later. 141 | initial_key_count = len(schema_fields) 142 | content_field_name = '' 143 | 144 | for field_name, field_class in fields.items(): 145 | if field_class.is_multivalued: 146 | if field_class.indexed is False: 147 | schema_fields[field_class.index_fieldname] = IDLIST(stored=True, field_boost=field_class.boost) 148 | else: 149 | schema_fields[field_class.index_fieldname] = KEYWORD(stored=True, commas=True, scorable=True, field_boost=field_class.boost) 150 | elif field_class.field_type in ['date', 'datetime']: 151 | schema_fields[field_class.index_fieldname] = DATETIME(stored=field_class.stored, sortable=True) 152 | elif field_class.field_type == 'integer': 153 | schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=int, field_boost=field_class.boost) 154 | elif field_class.field_type == 'float': 155 | schema_fields[field_class.index_fieldname] = NUMERIC(stored=field_class.stored, numtype=float, field_boost=field_class.boost) 156 | elif field_class.field_type == 'boolean': 157 | # Field boost isn't supported on BOOLEAN as of 1.8.2. 158 | schema_fields[field_class.index_fieldname] = BOOLEAN(stored=field_class.stored) 159 | elif field_class.field_type == 'ngram': 160 | schema_fields[field_class.index_fieldname] = NGRAM(minsize=3, maxsize=15, stored=field_class.stored, field_boost=field_class.boost) 161 | elif field_class.field_type == 'edge_ngram': 162 | schema_fields[field_class.index_fieldname] = NGRAMWORDS(minsize=2, maxsize=15, at='start', stored=field_class.stored, field_boost=field_class.boost) 163 | else: 164 | schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.boost, sortable=True) 165 | 166 | if field_class.document is True: 167 | content_field_name = field_class.index_fieldname 168 | schema_fields[field_class.index_fieldname].spelling = True 169 | 170 | # Fail more gracefully than relying on the backend to die if no fields 171 | # are found. 172 | if len(schema_fields) <= initial_key_count: 173 | raise SearchBackendError("No fields were found in any search_indexes. Please correct this before attempting to search.") 174 | 175 | return (content_field_name, Schema(**schema_fields)) 176 | 177 | def update(self, index, iterable, commit=True): 178 | if not self.setup_complete: 179 | self.setup() 180 | 181 | self.index = self.index.refresh() 182 | writer = AsyncWriter(self.index) 183 | 184 | for obj in iterable: 185 | try: 186 | doc = index.full_prepare(obj) 187 | except SkipDocument: 188 | self.log.debug(u"Indexing for object `%s` skipped", obj) 189 | else: 190 | # Really make sure it's unicode, because Whoosh won't have it any 191 | # other way. 192 | for key in doc: 193 | doc[key] = self._from_python(doc[key]) 194 | 195 | # Document boosts aren't supported in Whoosh 2.5.0+. 196 | if 'boost' in doc: 197 | del doc['boost'] 198 | 199 | try: 200 | writer.update_document(**doc) 201 | except Exception as e: 202 | if not self.silently_fail: 203 | raise 204 | 205 | # We'll log the object identifier but won't include the actual object 206 | # to avoid the possibility of that generating encoding errors while 207 | # processing the log message: 208 | self.log.error(u"%s while preparing object for update" % e.__class__.__name__, 209 | exc_info=True, extra={"data": {"index": index, 210 | "object": get_identifier(obj)}}) 211 | 212 | if len(iterable) > 0: 213 | # For now, commit no matter what, as we run into locking issues otherwise. 214 | writer.commit() 215 | 216 | def remove(self, obj_or_string, commit=True): 217 | if not self.setup_complete: 218 | self.setup() 219 | 220 | self.index = self.index.refresh() 221 | whoosh_id = get_identifier(obj_or_string) 222 | 223 | try: 224 | self.index.delete_by_query(q=self.parser.parse(u'%s:"%s"' % (ID, whoosh_id))) 225 | except Exception as e: 226 | if not self.silently_fail: 227 | raise 228 | 229 | self.log.error("Failed to remove document '%s' from Whoosh: %s", whoosh_id, e, exc_info=True) 230 | 231 | def clear(self, models=None, commit=True): 232 | if not self.setup_complete: 233 | self.setup() 234 | 235 | self.index = self.index.refresh() 236 | 237 | if models is not None: 238 | assert isinstance(models, (list, tuple)) 239 | 240 | try: 241 | if models is None: 242 | self.delete_index() 243 | else: 244 | models_to_delete = [] 245 | 246 | for model in models: 247 | models_to_delete.append(u"%s:%s" % (DJANGO_CT, get_model_ct(model))) 248 | 249 | self.index.delete_by_query(q=self.parser.parse(u" OR ".join(models_to_delete))) 250 | except Exception as e: 251 | if not self.silently_fail: 252 | raise 253 | 254 | if models is not None: 255 | self.log.error("Failed to clear Whoosh index of models '%s': %s", ','.join(models_to_delete), 256 | e, exc_info=True) 257 | else: 258 | self.log.error("Failed to clear Whoosh index: %s", e, exc_info=True) 259 | 260 | def delete_index(self): 261 | # Per the Whoosh mailing list, if wiping out everything from the index, 262 | # it's much more efficient to simply delete the index files. 263 | if self.use_file_storage and os.path.exists(self.path): 264 | shutil.rmtree(self.path) 265 | elif not self.use_file_storage: 266 | self.storage.clean() 267 | 268 | # Recreate everything. 269 | self.setup() 270 | 271 | def optimize(self): 272 | if not self.setup_complete: 273 | self.setup() 274 | 275 | self.index = self.index.refresh() 276 | self.index.optimize() 277 | 278 | def calculate_page(self, start_offset=0, end_offset=None): 279 | # Prevent against Whoosh throwing an error. Requires an end_offset 280 | # greater than 0. 281 | if end_offset is not None and end_offset <= 0: 282 | end_offset = 1 283 | 284 | # Determine the page. 285 | page_num = 0 286 | 287 | if end_offset is None: 288 | end_offset = 1000000 289 | 290 | if start_offset is None: 291 | start_offset = 0 292 | 293 | page_length = end_offset - start_offset 294 | 295 | if page_length and page_length > 0: 296 | page_num = int(start_offset / page_length) 297 | 298 | # Increment because Whoosh uses 1-based page numbers. 299 | page_num += 1 300 | return page_num, page_length 301 | 302 | @log_query 303 | def search(self, query_string, sort_by=None, start_offset=0, end_offset=None, 304 | fields='', highlight=False, facets=None, date_facets=None, query_facets=None, 305 | narrow_queries=None, spelling_query=None, within=None, 306 | dwithin=None, distance_point=None, models=None, 307 | limit_to_registered_models=None, result_class=None, **kwargs): 308 | if not self.setup_complete: 309 | self.setup() 310 | 311 | # A zero length query should return no results. 312 | if len(query_string) == 0: 313 | return { 314 | 'results': [], 315 | 'hits': 0, 316 | } 317 | 318 | query_string = force_text(query_string) 319 | 320 | # A one-character query (non-wildcard) gets nabbed by a stopwords 321 | # filter and should yield zero results. 322 | if len(query_string) <= 1 and query_string != u'*': 323 | return { 324 | 'results': [], 325 | 'hits': 0, 326 | } 327 | 328 | reverse = False 329 | 330 | if sort_by is not None: 331 | # Determine if we need to reverse the results and if Whoosh can 332 | # handle what it's being asked to sort by. Reversing is an 333 | # all-or-nothing action, unfortunately. 334 | sort_by_list = [] 335 | reverse_counter = 0 336 | 337 | for order_by in sort_by: 338 | if order_by.startswith('-'): 339 | reverse_counter += 1 340 | 341 | if reverse_counter and reverse_counter != len(sort_by): 342 | raise SearchBackendError("Whoosh requires all order_by fields" 343 | " to use the same sort direction") 344 | 345 | for order_by in sort_by: 346 | if order_by.startswith('-'): 347 | sort_by_list.append(order_by[1:]) 348 | 349 | if len(sort_by_list) == 1: 350 | reverse = True 351 | else: 352 | sort_by_list.append(order_by) 353 | 354 | if len(sort_by_list) == 1: 355 | reverse = False 356 | 357 | sort_by = sort_by_list 358 | 359 | if facets is not None: 360 | warnings.warn("Whoosh does not handle faceting.", Warning, stacklevel=2) 361 | 362 | if date_facets is not None: 363 | warnings.warn("Whoosh does not handle date faceting.", Warning, stacklevel=2) 364 | 365 | if query_facets is not None: 366 | warnings.warn("Whoosh does not handle query faceting.", Warning, stacklevel=2) 367 | 368 | narrowed_results = None 369 | self.index = self.index.refresh() 370 | 371 | if limit_to_registered_models is None: 372 | limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) 373 | 374 | if models and len(models): 375 | model_choices = sorted(get_model_ct(model) for model in models) 376 | elif limit_to_registered_models: 377 | # Using narrow queries, limit the results to only models handled 378 | # with the current routers. 379 | model_choices = self.build_models_list() 380 | else: 381 | model_choices = [] 382 | 383 | if len(model_choices) > 0: 384 | if narrow_queries is None: 385 | narrow_queries = set() 386 | 387 | narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) 388 | 389 | narrow_searcher = None 390 | 391 | if narrow_queries is not None: 392 | # Potentially expensive? I don't see another way to do it in Whoosh... 393 | narrow_searcher = self.index.searcher() 394 | 395 | for nq in narrow_queries: 396 | recent_narrowed_results = narrow_searcher.search(self.parser.parse(force_text(nq)), 397 | limit=None) 398 | 399 | if len(recent_narrowed_results) <= 0: 400 | return { 401 | 'results': [], 402 | 'hits': 0, 403 | } 404 | 405 | if narrowed_results: 406 | narrowed_results.filter(recent_narrowed_results) 407 | else: 408 | narrowed_results = recent_narrowed_results 409 | 410 | self.index = self.index.refresh() 411 | 412 | if self.index.doc_count(): 413 | searcher = self.index.searcher() 414 | parsed_query = self.parser.parse(query_string) 415 | 416 | # In the event of an invalid/stopworded query, recover gracefully. 417 | if parsed_query is None: 418 | return { 419 | 'results': [], 420 | 'hits': 0, 421 | } 422 | 423 | page_num, page_length = self.calculate_page(start_offset, end_offset) 424 | 425 | search_kwargs = { 426 | 'pagelen': page_length, 427 | 'sortedby': sort_by, 428 | 'reverse': reverse, 429 | } 430 | 431 | # Handle the case where the results have been narrowed. 432 | if narrowed_results is not None: 433 | search_kwargs['filter'] = narrowed_results 434 | 435 | try: 436 | raw_page = searcher.search_page( 437 | parsed_query, 438 | page_num, 439 | **search_kwargs 440 | ) 441 | except ValueError: 442 | if not self.silently_fail: 443 | raise 444 | 445 | return { 446 | 'results': [], 447 | 'hits': 0, 448 | 'spelling_suggestion': None, 449 | } 450 | 451 | # Because as of Whoosh 2.5.1, it will return the wrong page of 452 | # results if you request something too high. :( 453 | if raw_page.pagenum < page_num: 454 | return { 455 | 'results': [], 456 | 'hits': 0, 457 | 'spelling_suggestion': None, 458 | } 459 | 460 | results = self._process_results(raw_page, highlight=highlight, query_string=query_string, spelling_query=spelling_query, result_class=result_class) 461 | searcher.close() 462 | 463 | if hasattr(narrow_searcher, 'close'): 464 | narrow_searcher.close() 465 | 466 | return results 467 | else: 468 | if self.include_spelling: 469 | if spelling_query: 470 | spelling_suggestion = self.create_spelling_suggestion(spelling_query) 471 | else: 472 | spelling_suggestion = self.create_spelling_suggestion(query_string) 473 | else: 474 | spelling_suggestion = None 475 | 476 | return { 477 | 'results': [], 478 | 'hits': 0, 479 | 'spelling_suggestion': spelling_suggestion, 480 | } 481 | 482 | def more_like_this(self, model_instance, additional_query_string=None, 483 | start_offset=0, end_offset=None, models=None, 484 | limit_to_registered_models=None, result_class=None, **kwargs): 485 | if not self.setup_complete: 486 | self.setup() 487 | 488 | field_name = self.content_field_name 489 | narrow_queries = set() 490 | narrowed_results = None 491 | self.index = self.index.refresh() 492 | 493 | if limit_to_registered_models is None: 494 | limit_to_registered_models = getattr(settings, 'HAYSTACK_LIMIT_TO_REGISTERED_MODELS', True) 495 | 496 | if models and len(models): 497 | model_choices = sorted(get_model_ct(model) for model in models) 498 | elif limit_to_registered_models: 499 | # Using narrow queries, limit the results to only models handled 500 | # with the current routers. 501 | model_choices = self.build_models_list() 502 | else: 503 | model_choices = [] 504 | 505 | if len(model_choices) > 0: 506 | if narrow_queries is None: 507 | narrow_queries = set() 508 | 509 | narrow_queries.add(' OR '.join(['%s:%s' % (DJANGO_CT, rm) for rm in model_choices])) 510 | 511 | if additional_query_string and additional_query_string != '*': 512 | narrow_queries.add(additional_query_string) 513 | 514 | narrow_searcher = None 515 | 516 | if narrow_queries is not None: 517 | # Potentially expensive? I don't see another way to do it in Whoosh... 518 | narrow_searcher = self.index.searcher() 519 | 520 | for nq in narrow_queries: 521 | recent_narrowed_results = narrow_searcher.search(self.parser.parse(force_text(nq)), 522 | limit=None) 523 | 524 | if len(recent_narrowed_results) <= 0: 525 | return { 526 | 'results': [], 527 | 'hits': 0, 528 | } 529 | 530 | if narrowed_results: 531 | narrowed_results.filter(recent_narrowed_results) 532 | else: 533 | narrowed_results = recent_narrowed_results 534 | 535 | page_num, page_length = self.calculate_page(start_offset, end_offset) 536 | 537 | self.index = self.index.refresh() 538 | raw_results = EmptyResults() 539 | 540 | searcher = None 541 | if self.index.doc_count(): 542 | query = "%s:%s" % (ID, get_identifier(model_instance)) 543 | searcher = self.index.searcher() 544 | parsed_query = self.parser.parse(query) 545 | results = searcher.search(parsed_query) 546 | 547 | if len(results): 548 | raw_results = results[0].more_like_this(field_name, top=end_offset) 549 | 550 | # Handle the case where the results have been narrowed. 551 | if narrowed_results is not None and hasattr(raw_results, 'filter'): 552 | raw_results.filter(narrowed_results) 553 | 554 | try: 555 | raw_page = ResultsPage(raw_results, page_num, page_length) 556 | except ValueError: 557 | if not self.silently_fail: 558 | raise 559 | 560 | return { 561 | 'results': [], 562 | 'hits': 0, 563 | 'spelling_suggestion': None, 564 | } 565 | 566 | # Because as of Whoosh 2.5.1, it will return the wrong page of 567 | # results if you request something too high. :( 568 | if raw_page.pagenum < page_num: 569 | return { 570 | 'results': [], 571 | 'hits': 0, 572 | 'spelling_suggestion': None, 573 | } 574 | 575 | results = self._process_results(raw_page, result_class=result_class) 576 | 577 | if searcher: 578 | searcher.close() 579 | 580 | if hasattr(narrow_searcher, 'close'): 581 | narrow_searcher.close() 582 | 583 | return results 584 | 585 | def _process_results(self, raw_page, highlight=False, query_string='', spelling_query=None, result_class=None): 586 | from haystack import connections 587 | results = [] 588 | 589 | # It's important to grab the hits first before slicing. Otherwise, this 590 | # can cause pagination failures. 591 | hits = len(raw_page) 592 | 593 | if result_class is None: 594 | result_class = SearchResult 595 | 596 | facets = {} 597 | spelling_suggestion = None 598 | unified_index = connections[self.connection_alias].get_unified_index() 599 | indexed_models = unified_index.get_indexed_models() 600 | 601 | for doc_offset, raw_result in enumerate(raw_page): 602 | score = raw_page.score(doc_offset) or 0 603 | app_label, model_name = raw_result[DJANGO_CT].split('.') 604 | additional_fields = {} 605 | model = haystack_get_model(app_label, model_name) 606 | 607 | if model and model in indexed_models: 608 | for key, value in raw_result.items(): 609 | index = unified_index.get_index(model) 610 | string_key = str(key) 611 | 612 | if string_key in index.fields and hasattr(index.fields[string_key], 'convert'): 613 | # Special-cased due to the nature of KEYWORD fields. 614 | if index.fields[string_key].is_multivalued: 615 | if value is None or len(value) is 0: 616 | additional_fields[string_key] = [] 617 | else: 618 | additional_fields[string_key] = value.split(',') 619 | else: 620 | additional_fields[string_key] = index.fields[string_key].convert(value) 621 | else: 622 | additional_fields[string_key] = self._to_python(value) 623 | 624 | del(additional_fields[DJANGO_CT]) 625 | del(additional_fields[DJANGO_ID]) 626 | 627 | if highlight: 628 | sa = StemmingAnalyzer() 629 | formatter = WhooshHtmlFormatter('em') 630 | terms = [token.text for token in sa(query_string)] 631 | 632 | whoosh_result = whoosh_highlight( 633 | additional_fields.get(self.content_field_name), 634 | terms, 635 | sa, 636 | ContextFragmenter(), 637 | formatter 638 | ) 639 | additional_fields['highlighted'] = { 640 | self.content_field_name: [whoosh_result], 641 | } 642 | 643 | result = result_class(app_label, model_name, raw_result[DJANGO_ID], score, **additional_fields) 644 | results.append(result) 645 | else: 646 | hits -= 1 647 | 648 | if self.include_spelling: 649 | if spelling_query: 650 | spelling_suggestion = self.create_spelling_suggestion(spelling_query) 651 | else: 652 | spelling_suggestion = self.create_spelling_suggestion(query_string) 653 | 654 | return { 655 | 'results': results, 656 | 'hits': hits, 657 | 'facets': facets, 658 | 'spelling_suggestion': spelling_suggestion, 659 | } 660 | 661 | def create_spelling_suggestion(self, query_string): 662 | spelling_suggestion = None 663 | reader = self.index.reader() 664 | corrector = reader.corrector(self.content_field_name) 665 | cleaned_query = force_text(query_string) 666 | 667 | if not query_string: 668 | return spelling_suggestion 669 | 670 | # Clean the string. 671 | for rev_word in self.RESERVED_WORDS: 672 | cleaned_query = cleaned_query.replace(rev_word, '') 673 | 674 | for rev_char in self.RESERVED_CHARACTERS: 675 | cleaned_query = cleaned_query.replace(rev_char, '') 676 | 677 | # Break it down. 678 | query_words = cleaned_query.split() 679 | suggested_words = [] 680 | 681 | for word in query_words: 682 | suggestions = corrector.suggest(word, limit=1) 683 | 684 | if len(suggestions) > 0: 685 | suggested_words.append(suggestions[0]) 686 | 687 | spelling_suggestion = ' '.join(suggested_words) 688 | return spelling_suggestion 689 | 690 | def _from_python(self, value): 691 | """ 692 | Converts Python values to a string for Whoosh. 693 | 694 | Code courtesy of pysolr. 695 | """ 696 | if hasattr(value, 'strftime'): 697 | if not hasattr(value, 'hour'): 698 | value = datetime(value.year, value.month, value.day, 0, 0, 0) 699 | elif isinstance(value, bool): 700 | if value: 701 | value = 'true' 702 | else: 703 | value = 'false' 704 | elif isinstance(value, (list, tuple)): 705 | value = u','.join([force_text(v) for v in value]) 706 | elif isinstance(value, (six.integer_types, float)): 707 | # Leave it alone. 708 | pass 709 | else: 710 | value = force_text(value) 711 | return value 712 | 713 | def _to_python(self, value): 714 | """ 715 | Converts values from Whoosh to native Python values. 716 | 717 | A port of the same method in pysolr, as they deal with data the same way. 718 | """ 719 | if value == 'true': 720 | return True 721 | elif value == 'false': 722 | return False 723 | 724 | if value and isinstance(value, six.string_types): 725 | possible_datetime = DATETIME_REGEX.search(value) 726 | 727 | if possible_datetime: 728 | date_values = possible_datetime.groupdict() 729 | 730 | for dk, dv in date_values.items(): 731 | date_values[dk] = int(dv) 732 | 733 | return datetime(date_values['year'], date_values['month'], date_values['day'], date_values['hour'], date_values['minute'], date_values['second']) 734 | 735 | try: 736 | # Attempt to use json to load the values. 737 | converted_value = json.loads(value) 738 | 739 | # Try to handle most built-in types. 740 | if isinstance(converted_value, (list, tuple, set, dict, six.integer_types, float, complex)): 741 | return converted_value 742 | except: 743 | # If it fails (SyntaxError or its ilk) or we don't trust it, 744 | # continue on. 745 | pass 746 | 747 | return value 748 | 749 | 750 | class WhooshSearchQuery(BaseSearchQuery): 751 | def _convert_datetime(self, date): 752 | if hasattr(date, 'hour'): 753 | return force_text(date.strftime('%Y%m%d%H%M%S')) 754 | else: 755 | return force_text(date.strftime('%Y%m%d000000')) 756 | 757 | def clean(self, query_fragment): 758 | """ 759 | Provides a mechanism for sanitizing user input before presenting the 760 | value to the backend. 761 | 762 | Whoosh 1.X differs here in that you can no longer use a backslash 763 | to escape reserved characters. Instead, the whole word should be 764 | quoted. 765 | """ 766 | words = query_fragment.split() 767 | cleaned_words = [] 768 | 769 | for word in words: 770 | if word in self.backend.RESERVED_WORDS: 771 | word = word.replace(word, word.lower()) 772 | 773 | for char in self.backend.RESERVED_CHARACTERS: 774 | if char in word: 775 | word = "'%s'" % word 776 | break 777 | 778 | cleaned_words.append(word) 779 | 780 | return ' '.join(cleaned_words) 781 | 782 | def build_query_fragment(self, field, filter_type, value): 783 | from haystack import connections 784 | query_frag = '' 785 | is_datetime = False 786 | 787 | if not hasattr(value, 'input_type_name'): 788 | # Handle when we've got a ``ValuesListQuerySet``... 789 | if hasattr(value, 'values_list'): 790 | value = list(value) 791 | 792 | if hasattr(value, 'strftime'): 793 | is_datetime = True 794 | 795 | if isinstance(value, six.string_types) and value != ' ': 796 | # It's not an ``InputType``. Assume ``Clean``. 797 | value = Clean(value) 798 | else: 799 | value = PythonData(value) 800 | 801 | # Prepare the query using the InputType. 802 | prepared_value = value.prepare(self) 803 | 804 | if not isinstance(prepared_value, (set, list, tuple)): 805 | # Then convert whatever we get back to what pysolr wants if needed. 806 | prepared_value = self.backend._from_python(prepared_value) 807 | 808 | # 'content' is a special reserved word, much like 'pk' in 809 | # Django's ORM layer. It indicates 'no special field'. 810 | if field == 'content': 811 | index_fieldname = '' 812 | else: 813 | index_fieldname = u'%s:' % connections[self._using].get_unified_index().get_index_fieldname(field) 814 | 815 | filter_types = { 816 | 'content': '%s', 817 | 'contains': '*%s*', 818 | 'endswith': "*%s", 819 | 'startswith': "%s*", 820 | 'exact': '%s', 821 | 'gt': "{%s to}", 822 | 'gte': "[%s to]", 823 | 'lt': "{to %s}", 824 | 'lte': "[to %s]", 825 | 'fuzzy': u'%s~', 826 | } 827 | 828 | if value.post_process is False: 829 | query_frag = prepared_value 830 | else: 831 | if filter_type in ['content', 'contains', 'startswith', 'endswith', 'fuzzy']: 832 | if value.input_type_name == 'exact': 833 | query_frag = prepared_value 834 | else: 835 | # Iterate over terms & incorportate the converted form of each into the query. 836 | terms = [] 837 | 838 | if isinstance(prepared_value, six.string_types): 839 | possible_values = prepared_value.split(' ') 840 | else: 841 | if is_datetime is True: 842 | prepared_value = self._convert_datetime(prepared_value) 843 | 844 | possible_values = [prepared_value] 845 | 846 | for possible_value in possible_values: 847 | terms.append(filter_types[filter_type] % self.backend._from_python(possible_value)) 848 | 849 | if len(terms) == 1: 850 | query_frag = terms[0] 851 | else: 852 | query_frag = u"(%s)" % " AND ".join(terms) 853 | elif filter_type == 'in': 854 | in_options = [] 855 | 856 | for possible_value in prepared_value: 857 | is_datetime = False 858 | 859 | if hasattr(possible_value, 'strftime'): 860 | is_datetime = True 861 | 862 | pv = self.backend._from_python(possible_value) 863 | 864 | if is_datetime is True: 865 | pv = self._convert_datetime(pv) 866 | 867 | if isinstance(pv, six.string_types) and not is_datetime: 868 | in_options.append('"%s"' % pv) 869 | else: 870 | in_options.append('%s' % pv) 871 | 872 | query_frag = "(%s)" % " OR ".join(in_options) 873 | elif filter_type == 'range': 874 | start = self.backend._from_python(prepared_value[0]) 875 | end = self.backend._from_python(prepared_value[1]) 876 | 877 | if hasattr(prepared_value[0], 'strftime'): 878 | start = self._convert_datetime(start) 879 | 880 | if hasattr(prepared_value[1], 'strftime'): 881 | end = self._convert_datetime(end) 882 | 883 | query_frag = u"[%s to %s]" % (start, end) 884 | elif filter_type == 'exact': 885 | if value.input_type_name == 'exact': 886 | query_frag = prepared_value 887 | else: 888 | prepared_value = Exact(prepared_value).prepare(self) 889 | query_frag = filter_types[filter_type] % prepared_value 890 | else: 891 | if is_datetime is True: 892 | prepared_value = self._convert_datetime(prepared_value) 893 | 894 | query_frag = filter_types[filter_type] % prepared_value 895 | 896 | if len(query_frag) and not isinstance(value, Raw): 897 | if not query_frag.startswith('(') and not query_frag.endswith(')'): 898 | query_frag = "(%s)" % query_frag 899 | 900 | return u"%s%s" % (index_fieldname, query_frag) 901 | 902 | 903 | class WhooshEngine(BaseEngine): 904 | backend = WhooshSearchBackend 905 | query = WhooshSearchQuery 906 | -------------------------------------------------------------------------------- /comments/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/comments/__init__.py -------------------------------------------------------------------------------- /comments/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /comments/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class CommentsConfig(AppConfig): 5 | name = 'comments' 6 | -------------------------------------------------------------------------------- /comments/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from .models import Comment 3 | 4 | 5 | class CommentForm(forms.ModelForm): 6 | class Meta: 7 | model = Comment 8 | fields = ['name', 'email', 'url', 'text'] 9 | -------------------------------------------------------------------------------- /comments/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class Comment(models.Model): 5 | name = models.CharField(max_length=100) 6 | email = models.EmailField(max_length=255) 7 | url = models.URLField(blank=True) 8 | text = models.TextField() 9 | created_time = models.DateTimeField(auto_now_add=True) 10 | 11 | post = models.ForeignKey('blog.Blog', on_delete=models.DO_NOTHING) 12 | 13 | def __str__(self): 14 | return self.text[:20] 15 | -------------------------------------------------------------------------------- /comments/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /comments/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | app_name = 'comments' 6 | urlpatterns = [ 7 | url(r'^comment/post/(?P[0-9]+)/$', views.post_comment, name='post_comment'), 8 | ] 9 | -------------------------------------------------------------------------------- /comments/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render, get_object_or_404, redirect 2 | from blog.models import Blog 3 | from .models import Comment 4 | from .forms import CommentForm 5 | 6 | 7 | def post_comment(request, pk): 8 | # 先获取被评论的文章,因为后面需要把评论和被评论的文章关联起来。 9 | # 这里我们使用了 Django 提供的一个快捷函数 get_object_or_404, 10 | # 这个函数的作用是当获取的文章(Post)存在时,则获取;否则返回 404 页面给用户。 11 | post = get_object_or_404(Blog, pk=pk) 12 | 13 | # HTTP 请求有 get 和 post 两种,一般用户通过表单提交数据都是通过 post 请求, 14 | # 因此只有当用户的请求为 post 时才需要处理表单数据。 15 | if request.method == 'POST': 16 | # 用户提交的数据存在 request.POST 中,这是一个类字典对象。 17 | # 我们利用这些数据构造了 CommentForm 的实例,这样 Django 的表单就生成了。 18 | form = CommentForm(request.POST) 19 | 20 | # 当调用 form.is_valid() 方法时,Django 自动帮我们检查表单的数据是否符合格式要求。 21 | if form.is_valid(): 22 | # 检查到数据是合法的,调用表单的 save 方法保存数据到数据库, 23 | # commit=False 的作用是仅仅利用表单的数据生成 Comment 模型类的实例,但还不保存评论数据到数据库。 24 | comment = form.save(commit=False) 25 | 26 | # 将评论和被评论的文章关联起来。 27 | comment.post = post 28 | 29 | # 最终将评论数据保存进数据库,调用模型实例的 save 方法 30 | comment.save() 31 | 32 | # 重定向到 post 的详情页,实际上当 redirect 函数接收一个模型的实例时,它会调用这个模型实例的 get_absolute_url 方法, 33 | # 然后重定向到 get_absolute_url 方法返回的 URL。 34 | return redirect(post) 35 | 36 | else: 37 | # 检查到数据不合法,重新渲染详情页,并且渲染表单的错误。 38 | # 因此我们传了三个模板变量给 detail.html, 39 | # 一个是文章(Post),一个是评论列表,一个是表单 form 40 | # 注意这里我们用到了 post.comment_set.all() 方法, 41 | # 这个用法有点类似于 Post.objects.all() 42 | # 其作用是获取这篇 post 下的的全部评论, 43 | # 因为 Post 和 Comment 是 ForeignKey 关联的, 44 | # 因此使用 post.comment_set.all() 反向查询全部评论。 45 | # 具体请看下面的讲解。 46 | comment_list = post.comment_set.all() 47 | context = {'post': post, 48 | 'form': form, 49 | 'comment_list': comment_list 50 | } 51 | return render(request, 'blog.html', context=context) 52 | # 不是 post 请求,说明用户没有提交数据,重定向到文章详情页。 53 | return redirect(post) 54 | -------------------------------------------------------------------------------- /github_pic/admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/github_pic/admin.png -------------------------------------------------------------------------------- /github_pic/detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/github_pic/detail.png -------------------------------------------------------------------------------- /github_pic/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/github_pic/editor.png -------------------------------------------------------------------------------- /github_pic/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/github_pic/index.png -------------------------------------------------------------------------------- /github_pic/pic_upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/github_pic/pic_upload.png -------------------------------------------------------------------------------- /github_pic/tag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/real-qiuxingxiang/Django-Blog/8a032632704cb264ef7f96b3e58dfadd976e551e/github_pic/tag.png -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == '__main__': 6 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'QsBlog.settings') 7 | try: 8 | from django.core.management import execute_from_command_line 9 | except ImportError as exc: 10 | raise ImportError( 11 | "Couldn't import Django. Are you sure it's installed and " 12 | "available on your PYTHONPATH environment variable? Did you " 13 | "forget to activate a virtual environment?" 14 | ) from exc 15 | execute_from_command_line(sys.argv) 16 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | asn1crypto==0.24.0 2 | cffi==1.11.5 3 | cryptography==2.4.2 4 | defusedxml==0.5.0 5 | diff-match-patch==20181111 6 | Django==2.1.5 7 | django-appconf==1.0.2 8 | django-compressor==2.2 9 | django-crispy-forms==1.7.2 10 | django-formtools==2.1 11 | django-haystack==2.8.1 12 | django-import-export==1.2.0 13 | django-js-asset==1.1.0 14 | django-reversion==3.0.2 15 | django-simditor==0.0.15 16 | et-xmlfile==1.0.1 17 | future==0.17.1 18 | httplib2==0.12.0 19 | idna==2.8 20 | jdcal==1.4 21 | jieba==0.39 22 | mysqlclient==1.3.14 23 | odfpy==1.4.0 24 | openpyxl==2.5.12 25 | Pillow==5.4.1 26 | pycparser==2.19 27 | Pygments==2.3.1 28 | PyMySQL==0.9.3 29 | pytz==2018.9 30 | PyYAML==3.13 31 | rcssmin==1.0.6 32 | rjsmin==1.0.12 33 | six==1.12.0 34 | tablib==0.12.1 35 | unicodecsv==0.14.1 36 | Whoosh==2.7.4 37 | -e git+https://github.com/sshwsfc/xadmin@0751cd91bcb16735f83f77458d463f047803e7b0#egg=xadmin 38 | xlrd==1.2.0 39 | xlwt==1.3.0 40 | -------------------------------------------------------------------------------- /templates/about.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Tag - Q's Blog{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 |

Building……

9 |
10 | 11 | 12 | {% endblock %} 13 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {% load static %} 2 | {% load compress %} 3 | 4 | 5 | 6 | 7 | 8 | {% compress css %} 9 | 10 | 11 | {% endcompress %} 12 | 13 | 14 | {% block title %}{% endblock %} 15 | 16 | 17 | {% include 'header.html' %} 18 | {% block content %}{% endblock %} 19 | {% include 'footer.html' %} 20 | 21 | -------------------------------------------------------------------------------- /templates/blog.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ blog.title }} - Q's Blog{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 |
9 |
10 |

{{ blog.title }}

11 | 28 |
29 |
30 | {{ blog.content|safe }} 31 |
32 | {% if pre_blog %} 33 | 上一篇:{{ pre_blog.title }} 34 | {% else %} 35 | 上一篇:没有啦 36 | {% endif %} 37 | {% if next_blog %} 38 | 39 | {% else %} 40 | 41 | {% endif %} 42 |
43 | {% include 'valine.html' %} 44 |
45 |
46 |
47 | 48 | {% endblock %} 49 | -------------------------------------------------------------------------------- /templates/comment.html: -------------------------------------------------------------------------------- 1 |
2 |

Comments

3 |
    4 | {% for comment in comment_list %} 5 |
  1. 6 | {{ comment.name }} 7 | 8 |
    9 | {{ comment.text }} 10 |
    11 |
  2. 12 | {% empty %} 13 | 暂无评论 14 | {% endfor %} 15 |
16 |
17 |
18 |

Leave a Comment

19 |
20 | {% csrf_token %} 21 |

22 | {{ form.name }} 23 | {{ form.name.errors }}

24 |

25 | {{ form.email }} 26 | {{ form.email.errors }}

27 |

28 | {{ form.url }} 29 | {{ form.url.errors }}

30 |

31 | {{ form.text }} 32 | {{ form.text.errors }}

33 | 34 |
35 |
36 | -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

© Copyright 2013 - 2018 by Q.

4 |
5 |
-------------------------------------------------------------------------------- /templates/header.html: -------------------------------------------------------------------------------- 1 | 20 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Q's Blog{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 | {% regroup blogs by created_time.year as year_sorted_blogs %} 9 |
    10 | {% for year in year_sorted_blogs %} 11 |
  • {{ year.grouper }} 年
  • 12 | {% for blog in year.list %} 13 |
  • 14 | 18 |
    19 | 20 |
    21 |
  • 22 | {% endfor %} 23 | {% endfor %} 24 |
25 |
26 | 27 | {% endblock %} -------------------------------------------------------------------------------- /templates/search/indexes/blog/blog_text.txt: -------------------------------------------------------------------------------- 1 | {{ object.title }} 2 | {{ object.body }} -------------------------------------------------------------------------------- /templates/search/search.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block title %}搜索结果 - Q's Blog{% endblock %} 3 | 4 | {% block content %} 5 | 6 |
7 |

搜索结果:

8 | 36 |
37 | 38 | {% endblock %} 39 | -------------------------------------------------------------------------------- /templates/tag.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Tag - Q's Blog{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 |
    9 | {% for tag, blogs in tags_dic.items %} 10 |
  • {{ tag }}
  • 11 | {% for blog in blogs %} 12 |
  • 13 | 17 |
    18 | 19 |
    20 |
  • 21 | {% endfor %} 22 |
    23 | {% endfor %} 24 |
25 |
26 | 27 | 28 | {% endblock %} 29 | -------------------------------------------------------------------------------- /templates/tag_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{{ tag_name }} - Q's Blog{% endblock %} 4 | 5 | {% block content %} 6 | 7 |
8 |
    9 |

    Tag: {{ tag_name }}

    10 | {% for blog in blogs %} 11 |
  • 12 | 15 |
    16 |

    17 |
    18 |
  • 19 | {% endfor %} 20 |
21 |
22 | 23 | {% endblock %} -------------------------------------------------------------------------------- /templates/valine.html: -------------------------------------------------------------------------------- 1 |
2 |

Comments

3 |
4 | 5 | --------------------------------------------------------------------------------