├── .gitignore ├── CHANGELOG.md ├── CODE_README.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── conf ├── dev_supervisord.conf ├── dev_uwsgi.ini ├── nginx │ ├── apps │ │ └── typeidea.conf │ └── nginx.conf ├── supervisord.conf └── uwsgi.ini ├── fabfile.py ├── fabfile2.py ├── requirements.txt ├── setup.cfg ├── setup.py ├── ssh_config └── typeidea ├── blog ├── __init__.py ├── adminforms.py ├── adminx.py ├── apis.py ├── apps.py ├── middleware │ ├── __init__.py │ └── user_id.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180310_1505.py │ ├── 0003_auto_20180325_1737.py │ ├── 0004_post_content_html.py │ ├── 0005_auto_20180502_1730.py │ └── __init__.py ├── models.py ├── rss.py ├── serializers.py ├── sitemap.py ├── tests.py └── views.py ├── comment ├── __init__.py ├── adminx.py ├── apps.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180410_1922.py │ └── __init__.py ├── models.py ├── templatetags │ ├── __init__.py │ └── comment_block.py ├── tests.py └── views.py ├── config ├── __init__.py ├── adminx.py ├── apps.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20180410_1922.py │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── manage.py └── typeidea ├── __init__.py ├── autocomplete.py ├── base_admin.py ├── settings ├── __init__.py ├── base.py ├── develop.py └── product.py ├── storage.py ├── templates └── static_page │ ├── demo.html │ └── list.html ├── themes ├── bootstrap │ ├── static │ │ ├── css │ │ │ ├── bootstrap.css │ │ │ └── bootstrap.min.css │ │ └── js │ │ │ └── post_editor.js │ └── templates │ │ ├── 404.html │ │ ├── 50x.html │ │ ├── blog │ │ ├── base.html │ │ ├── detail.html │ │ └── list.html │ │ ├── comment │ │ ├── block.html │ │ └── result.html │ │ ├── config │ │ ├── blocks │ │ │ ├── sidebar_comments.html │ │ │ └── sidebar_posts.html │ │ └── links.html │ │ └── sitemap.xml └── default │ └── templates │ ├── blog │ ├── base.html │ ├── detail.html │ └── list.html │ └── config │ ├── blocks │ ├── sidebar_comments.html │ └── sidebar_posts.html │ └── links.html ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite3 2 | # Byte-compiled / optimized / DLL files 3 | __pycache__/ 4 | *.py[cod] 5 | *$py.class 6 | 7 | # C extensions 8 | *.so 9 | 10 | # Distribution / packaging 11 | .Python 12 | env/ 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/CHANGELOG.md -------------------------------------------------------------------------------- /CODE_README.md: -------------------------------------------------------------------------------- 1 | # typeidea 2 | Django企业开发实战对应项目代码 3 | 4 | 说明: 5 | 6 | 视频对应的分支是:chapter7,chapter8,这样的。 7 | 8 | 书对应的分支是:book/*开头的。 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/LICENSE -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | recursive-include typeidea *.css *.js *.jpg *.gif *.png *.html *.md 3 | prune typeidea/typeidea/static_files 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Typeidea 介绍 2 | 3 | 这个是完整的博客系统,master代码是基于 Python3.6 和 Django2.0 开发。 4 | 5 | 也是[《Django企业开发实战》](http://django-practice-book.com/)图书和对应的一套[视频](http://django-practice-book.com/course.html)的代码。 6 | 7 | 不同的载体和章节的内容分别在不同的分支中,你可以通过分支名来区分是视频还是图书,以及对应的章节,比如: 8 | 9 | 分支 ``book/05-initproject`` 就是对应的图书的第五章的代码,``book/06-admin`` 就是对应的第六章的代码。 10 | 而对应的 ``chapter7``、``chapter8``这样的是视频章节对应的代码。 11 | -------------------------------------------------------------------------------- /conf/dev_supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file=/tmp/supervisor.sock ; the path to the socket file 3 | ;chmod=0700 ; socket file mode (default 0700) 4 | ;chown=nobody:nogroup ; socket file uid:gid owner 5 | ;username=user ; default is no username (open server) 6 | ;password=123 ; default is no password (open server) 7 | 8 | [inet_http_server] ; Web Server的部分 9 | port=127.0.0.1:9001 ; 10 | username=user ; 登录用户名 11 | password=123 ; 登录密码 12 | 13 | [supervisord] ; 全局配置部分 14 | logfile=supervisord.log ; 主log文件 15 | logfile_maxbytes=50MB ; rotation配置 16 | logfile_backups=10 ; 备份数量 17 | loglevel=info ; 日志级别,默认 info; 其他选项: debug,warn,trace 18 | pidfile=/tmp/supervisord.pid ; pid文件 19 | nodaemon=false ; 默认是后台运行,如果需要前台运行可以设置为true 20 | ;user=chrism ; 默认是当前用户,如果是root用户的话需要配置为root 21 | ;directory=/tmp ; 配置所有涉及到目录的根目录,默认是当前运行目录 22 | ;environment=KEY="value" ; 全局(所有program)环境变量配置 23 | 24 | [rpcinterface:supervisor] ; 必须启用,supervisorctl通过它来管理进程 25 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 26 | 27 | [supervisorctl] ; 前几项配置必须跟[unix_http_server]保持相同 28 | serverurl=unix:///tmp/supervisor.sock 29 | ;username=chris 30 | ;password=123 31 | ;prompt=mysupervisor ; 进入交互模式时的提示文字,默认是"supervisor" 32 | ;history_file=~/.sc_history ; 用户的历史记录,跟bash上的配置类似,开启后可以查看和使用历史命令 33 | 34 | [program:typeidea] ;程序配置的部分,一份supervisord.conf可以配置多个程序 35 | command=gunicorn typeidea.wsgi:application -w 4 -b 127.0.0.1:800%(process_num)1d ; 启动命令,需要注意路径,最后的%(process_num)1d是获取当前进程号 36 | process_name=%(program_name)s_%(process_num)s ; 进程名,当下面的numprocs大于1时,必须配置%(process_num)s 37 | numprocs=2 ; 要启动进程数 38 | directory=typeidea ; 同上面配置,启动是所处的目录 39 | priority=999 ; 程序权重,多个程序时不同权重的程序启动先后顺序不同 40 | autostart=true ; supervisord启动是是否自动启动 41 | environment=TYPEIDEA_PROFILE="develop" ; 环境变量配置 42 | ;startsecs=1 ; 进程启动多长时间后视为正常运行 43 | ;startretries=3 ; 启动失败时重试次数,默认3 44 | ;autorestart=unexpected ; 何时重启进程如果程序在正常运行后退出,默认是unexpected也就是异常(不属于下面配置的exitcode时) 45 | ;exitcodes=0,2 ; 正常退出的exitcode 46 | ;stopsignal=QUIT ; kill进程的信号,默认是TERM,这是Linux中断信号:有如下选项 47 | ; TERM, HUP, INT, QUIT, KILL, USR1, or USR2 48 | ;stopwaitsecs=10 ; 当执行shutdown后多久关闭进程 49 | ;stopasgroup=false ; 停止进程组,比如Flask的Debug模式下,它不会传播信号给子进程,这会导致出现孤儿进程。 50 | ;killasgroup=false ; 同上,如果运行程序使用了multiprocessing的话需要用到 51 | ;user=chrism ; 使用其他用户身份运行程序 52 | ;redirect_stderr=true ; 重定向错误到stdout中,默认关闭 53 | stdout_logfile=stdout.log ;同 [supervisord] 54 | ;stdout_logfile_maxbytes=1MB ; 55 | ;stdout_logfile_backups=10 ; 56 | 57 | -------------------------------------------------------------------------------- /conf/dev_uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http = :9090 3 | chdir = ./typeidea 4 | home=/Users/the5fire/.virtualenvs/typeidea-env3 5 | PYTHONHOME = /Users/the5fire/.virtualenvs/typeidea-env3/bin/ 6 | 7 | env = TYPEIDEA_PROFILE=develop 8 | wsgi-file = typeidea/wsgi.py 9 | static-safe=typeidea/static_files 10 | route = /static/(.*) static:typeidea/static_files/$1 11 | route = /media/(.*) static:./typeidea/media/$1 12 | processes = 4 13 | threads = 2 14 | logger = file:errlog.log 15 | greenlet=1 16 | asyncio=10 17 | -------------------------------------------------------------------------------- /conf/nginx/apps/typeidea.conf: -------------------------------------------------------------------------------- 1 | upstream backend { 2 | # ip_hash/least_conn或者不填(默认round-robin) 3 | # 需要配合nginx upstream check module才能用 4 | # check interval=10000 rise=2 fall=3 timeout=3000 type=http default_down=false; 5 | 6 | # max_failes 最大失败次数, 失败后的等待时间,权重(越高被请求的频率越高) 7 | server 127.0.0.1:9090 max_fails=3 fail_timeout=30s weight=5; 8 | server 127.0.0.1:9091 max_fails=3 fail_timeout=30s weight=5; 9 | } 10 | 11 | server { 12 | listen 80 backlog=10000 default; 13 | server_name default; 14 | client_body_in_single_buffer on; # 是否把request body放到一个buffer中。 15 | client_max_body_size 2m; # request body最大限制,2M,如果是上传文件需求可以调整配置。 16 | client_body_buffer_size 50m; # request body buffer大小,超过设置会写入临时文件. 17 | proxy_buffering off; # 关闭proxy buffering,具体可查看本节参考链接 18 | 19 | access_log /tmp/access_log_typeidea.log main; 20 | 21 | location / { 22 | proxy_pass http://backend; 23 | proxy_http_version 1.1; 24 | proxy_connect_timeout 30; 25 | proxy_set_header Host $host; 26 | proxy_set_header X-Real-IP $http_x_forwarded_for; 27 | proxy_set_header X-Forwarded-For $remote_addr; 28 | proxy_set_header X-Forwarded-Host $http_host; 29 | } 30 | location /static/ { 31 | expires 1d; # 缓存1天 32 | alias /home/the5fire/venvs/typeidea-env/static_files/; # 需要跟最终项目部署后配置的STATIC_ROOT保持一致。 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /conf/nginx/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; # 以什么用户身份运行子进程 2 | worker_processes 1; # 一般设置为核数,通过命令:cat /proc/cpuinfo|grep "processor"|wc -l 查看核数 3 | # worker_cpu_affinity 01 10; # 多核情况下启用,设置亲和度,每个worker绑定到一个核上。如果是4核,则: 0001 0010 0100 1000 以此类推。 4 | 5 | error_log /var/log/nginx/error.log warn; 6 | pid /var/run/nginx.pid; 7 | 8 | 9 | events { 10 | use epoll; # 使用epoll提升并发能力,仅Linux系统可用 11 | worker_connections 1024; # 单个worker的同时最大连接数 12 | } 13 | 14 | 15 | http { 16 | include /etc/nginx/mime.types; 17 | default_type application/octet-stream; 18 | 19 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | '$status $body_bytes_sent "$http_referer" ' 21 | '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | access_log /var/log/nginx/access.log main; 24 | 25 | sendfile on; # 开启sendfile调用 —— 零拷贝技术,提供文件传输效率 26 | tcp_nopush on; # 配置TCP_CORK,配置sendfile后才会有效 27 | 28 | keepalive_timeout 65; # keepalive超时时间 29 | 30 | gzip on; # 开启gzip压缩 31 | gzip_types text/plain application/javascript application/x-javascript text/javascript text/css application/xml; # gzip要处理的文件类型 32 | 33 | # include /etc/nginx/conf.d/*.conf; # include 其他文件进来 34 | 35 | include apps/typeidea.conf; # 我们的配置文件 36 | } 37 | -------------------------------------------------------------------------------- /conf/supervisord.conf: -------------------------------------------------------------------------------- 1 | [unix_http_server] 2 | file={{ deploy_path }}/tmp/supervisor.sock 3 | 4 | [inet_http_server] 5 | port=127.0.0.1:9001 6 | username=user 7 | password=123 8 | 9 | [supervisord] 10 | logfile={{ deploy_path }}supervisord.log 11 | logfile_maxbytes=50MB 12 | logfile_backups=10 13 | loglevel=info 14 | pidfile={{ deploy_path }}/tmp/supervisord.pid 15 | nodaemon=false 16 | 17 | [rpcinterface:supervisor] 18 | supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface 19 | 20 | [supervisorctl] 21 | serverurl=unix://{{ deploy_path }}/tmp/supervisor.sock 22 | 23 | [program:typeidea] 24 | command=gunicorn typeidea.wsgi:application -w 1 -b 0.0.0.0:{{ port_prefix }}%(process_num)1d 25 | process_name=%(program_name)s_%(process_num)s 26 | numprocs={{ process_count }} 27 | directory={{ deploy_path }} 28 | priority=999 29 | autostart=true 30 | environment=TYPEIDEA_PROFILE="{{ profile }}" 31 | startsecs=5 32 | autorestart=unexpected 33 | exitcodes=0,2 34 | stopsignal=TERM 35 | stopwaitsecs=1 36 | redirect_stderr=true 37 | stdout_logfile={{ deploy_path }}/tmp/stdout.log 38 | stdout_logfile_maxbytes=1MB 39 | stdout_logfile_backups=10 40 | -------------------------------------------------------------------------------- /conf/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | http = :9090 3 | chdir = /home/the5fire/venvs/typeidea-env 4 | home=/home/the5fire/venvs/typeidea-env 5 | PYTHONHOME = /home/the5fire/venvs/typeidea-env/bin/ 6 | 7 | env = TYPEIDEA_PROFILE=product 8 | wsgi-file = bin/wsgi.py 9 | static-safe=/home/the5fire/venvs/typeidea-env/static_files/ ; 配置目录为安全目录,跳过uwsgi的secure检查 10 | route = /static/(.*) static:/home/the5fire/venvs/typeidea-env/static_files/$1 11 | processes = 4 12 | threads = 2 13 | ; deamonize=1 ; 用来配置background运行的程序 14 | -------------------------------------------------------------------------------- /fabfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime 3 | 4 | from fabric.api import ( 5 | env, run, prefix, local, settings, 6 | roles, 7 | ) 8 | from fabric.contrib.files import exists, upload_template 9 | from fabric.decorators import task 10 | 11 | 12 | env.roledefs = { 13 | 'myserver': ['the5fire@127.0.0.1:11022'], 14 | } 15 | env.PROJECT_NAME = 'typeidea' 16 | env.SETTINGS_BASE = 'typeidea/typeidea/settings/base.py' 17 | env.DEPLOY_PATH = '/home/the5fire/venvs/typeidea-env' 18 | env.VENV_ACTIVATE = os.path.join(env.DEPLOY_PATH, 'bin', 'activate') 19 | env.PYPI_HOST = '127.0.0.1' 20 | env.PYPI_INDEX = 'http://127.0.0.1:8080/simple' 21 | env.PROCESS_COUNT = 2 22 | env.PORT_PREFIX = 909 23 | 24 | 25 | class _Version: 26 | origin_record = {} 27 | 28 | def replace(self, f, version): 29 | with open(f, 'r') as fd: 30 | origin_content = fd.read() 31 | content = origin_content.replace('${version}', version) 32 | 33 | with open(f, 'w') as fd: 34 | fd.write(content) 35 | 36 | self.origin_record[f] = origin_content 37 | 38 | def set(self, file_list, version): 39 | for f in file_list: 40 | self.replace(f, version) 41 | 42 | def revert(self): 43 | for f, content in self.origin_record.items(): 44 | with open(f, 'w') as fd: 45 | fd.write(content) 46 | 47 | 48 | @task 49 | def build(version=None): 50 | """ 本地打包并且上传包到pypi上 51 | 1. 配置版本号 52 | 2. 打包并上传 53 | """ 54 | if not version: 55 | version = datetime.now().strftime('%m%d%H%M%S') # 当前时间,月日时分秒 56 | 57 | _version = _Version() 58 | _version.set(['setup.py', env.SETTINGS_BASE], version) 59 | 60 | with settings(warn_only=True): 61 | local('python setup.py bdist_wheel upload -r internal') 62 | 63 | _version.revert() 64 | 65 | 66 | def _ensure_virtualenv(): 67 | if exists(env.VENV_ACTIVATE): 68 | return True 69 | 70 | if not exists(env.DEPLOY_PATH): 71 | run('mkdir -p %s' % env.DEPLOY_PATH) 72 | 73 | run('python3.6 -m venv %s' % env.DEPLOY_PATH) 74 | 75 | 76 | def _reload_supervisoird(deploy_path, profile): 77 | template_dir = 'conf' 78 | filename = 'supervisord.conf' 79 | destination = env.DEPLOY_PATH 80 | context = { 81 | 'process_count': env.PROCESS_COUNT, 82 | 'port_prefix': env.PORT_PREFIX, 83 | 'profile': profile, 84 | 'deploy_path': deploy_path, 85 | } 86 | upload_template(filename, destination, context=context, use_jinja=True, template_dir=template_dir) 87 | with settings(warn_only=True): 88 | result = run('supervisorctl -c %s/supervisord.conf shutdown' % deploy_path) 89 | if result: 90 | run('supervisord -c %s/supervisord.conf' % deploy_path) 91 | 92 | 93 | @task 94 | @roles('myserver') 95 | def deploy(version, profile): 96 | """ 部署指定版本 97 | 1. 确认虚拟环境已经配置 98 | 2. 激活虚拟环境 99 | 3. 安装软件包 100 | 4. 启动 101 | """ 102 | _ensure_virtualenv() 103 | package_name = env.PROJECT_NAME + '==' + version 104 | with prefix('source %s' % env.VENV_ACTIVATE): 105 | run('pip install %s -i %s --trusted-host %s' % ( 106 | package_name, 107 | env.PYPI_INDEX, 108 | env.PYPI_HOST, 109 | )) 110 | _reload_supervisoird(env.DEPLOY_PATH, profile) 111 | -------------------------------------------------------------------------------- /fabfile2.py: -------------------------------------------------------------------------------- 1 | """ 2 | 这是fabric2版本的代码,fabfile1.py中是针对fabric3写的 3 | """ 4 | import os 5 | from datetime import datetime 6 | 7 | from jinja2 import Environment, FileSystemLoader, select_autoescape 8 | from invoke import task 9 | 10 | 11 | PROJECT_NAME = 'typeidea' 12 | SETTINGS_BASE = 'typeidea/typeidea/settings/base.py' 13 | DEPLOY_PATH = '/home/the5fire/venvs/typeidea-env' 14 | VENV_ACTIVATE = os.path.join(DEPLOY_PATH, 'bin', 'activate') 15 | PYPI_HOST = '127.0.0.1' 16 | PYPI_INDEX = 'http://127.0.0.1:8080/simple' 17 | PROCESS_COUNT = 2 18 | PORT_PREFIX = 909 19 | 20 | 21 | @task 22 | def build(c, version=None, bytescode=False): 23 | """ 本地打包并且上传包到pypi上 24 | 1. 配置版本号 25 | 2. 打包并上传 26 | Usage: 27 | fab build --version 1.4 28 | """ 29 | if not version: 30 | version = datetime.now().strftime('%m%d%H%M%S') # 当前时间,月日时分秒 31 | 32 | _version = _Version() 33 | _version.set(['setup.py', SETTINGS_BASE], version) 34 | 35 | result = c.run('echo $SHELL', hide=True) 36 | user_shell = result.stdout.strip('\n') 37 | c.run('python setup.py bdist_wheel upload -r internal', warn=True, shell=user_shell) 38 | 39 | _version.revert() 40 | 41 | 42 | @task 43 | def deploy(c, version, profile): 44 | """ 部署指定版本 45 | 1. 确认虚拟环境已经配置 46 | 2. 激活虚拟环境 47 | 3. 安装软件包 48 | 4. 启动 49 | 50 | Usage: 51 | fab -H myserver -S ssh_config deploy 1.4 product 52 | """ 53 | _ensure_virtualenv(c) 54 | package_name = PROJECT_NAME + '==' + version 55 | with c.prefix('source %s' % VENV_ACTIVATE): 56 | c.run('pip install %s -i %s --trusted-host %s' % ( 57 | package_name, 58 | PYPI_INDEX, 59 | PYPI_HOST, 60 | )) 61 | _reload_supervisoird(c, DEPLOY_PATH, profile) 62 | 63 | 64 | class _Version: 65 | origin_record = {} 66 | 67 | def replace(self, f, version): 68 | with open(f, 'r') as fd: 69 | origin_content = fd.read() 70 | content = origin_content.replace('${version}', version) 71 | 72 | with open(f, 'w') as fd: 73 | fd.write(content) 74 | 75 | self.origin_record[f] = origin_content 76 | 77 | def set(self, file_list, version): 78 | for f in file_list: 79 | self.replace(f, version) 80 | 81 | def revert(self): 82 | for f, content in self.origin_record.items(): 83 | with open(f, 'w') as fd: 84 | fd.write(content) 85 | 86 | 87 | def _ensure_virtualenv(c): 88 | if c.run('test -f %s' % VENV_ACTIVATE, warn=True).ok: 89 | return True 90 | 91 | if c.run('test -f %s' % DEPLOY_PATH, warn=True).failed: 92 | c.run('mkdir -p %s' % DEPLOY_PATH) 93 | 94 | c.run('python3.6 -m venv %s' % DEPLOY_PATH) 95 | c.run('mkdir -p %s/tmp' % DEPLOY_PATH) # 创建tmp目录存放pid和log 96 | 97 | 98 | def _upload_conf(c, deploy_path, profile): 99 | env = Environment( 100 | loader=FileSystemLoader('conf'), 101 | autoescape=select_autoescape(['.conf']) 102 | ) 103 | template = env.get_template('supervisord.conf') 104 | context = { 105 | 'process_count': PROCESS_COUNT, 106 | 'port_prefix': PORT_PREFIX, 107 | 'profile': profile, 108 | 'deploy_path': deploy_path, 109 | } 110 | content = template.render(**context) 111 | tmp_file = '/tmp/supervisord.conf' 112 | with open(tmp_file, 'wb') as f: 113 | f.write(content.encode('utf-8')) 114 | 115 | destination = os.path.join(deploy_path, 'supervisord.conf') 116 | c.put(tmp_file, destination) 117 | 118 | 119 | def _reload_supervisoird(c, deploy_path, profile): 120 | _upload_conf(c, deploy_path, profile) 121 | c.run('supervisorctl -c %s/supervisord.conf shutdown' % deploy_path, warn=True) 122 | c.run('supervisord -c %s/supervisord.conf' % deploy_path) 123 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i http://127.0.0.1:18080/simple 2 | ipython 3 | -e . 4 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | python-tag = py36 3 | #universal=0 # 仅限当前运行的Python版本2或者3 4 | #universal=1 # 2和3通用 5 | # 参考:https://wheel.readthedocs.io/en/stable/ 6 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | from setuptools import setup, find_packages 3 | 4 | 5 | setup( 6 | name='typeidea', 7 | version='${version}', 8 | description='Blog System base on Django', 9 | author='the5fire', 10 | author_email='thefivefire@gmail.com', 11 | url='https://www.the5fire.com', 12 | license='MIT', 13 | packages=find_packages('typeidea'), 14 | package_dir={'': 'typeidea'}, 15 | # package_data={'': [ # 打包数据文件,方法一 16 | # 'themes/*/*/*/*', # 需要按目录层级匹配 17 | # ]}, 18 | include_package_data=True, # 方法二 配合 MANIFEST.in文件 19 | install_requires=[ 20 | 'django~=2.0', 21 | 'gunicorn==19.8.1', 22 | 'supervisor==4.0.0dev0', 23 | 'xadmin==2.0.1', 24 | 'mysqlclient==1.3.12', 25 | 'django-ckeditor==5.4.0', 26 | 'django-rest-framework==0.1.0', 27 | 'django-redis==4.8.0', 28 | 'django-autocomplete-light==3.2.10', 29 | 'mistune==0.8.3', 30 | 'Pillow==4.3.0', 31 | 'coreapi==2.3.3', 32 | 'django-redis==4.8.0', 33 | 'hiredis==0.2.0', 34 | # debug 35 | 'django-debug-toolbar==1.9.1', 36 | 'django-silk==2.0.0', 37 | ], 38 | scripts=[ 39 | 'typeidea/manage.py', 40 | 'typeidea/typeidea/wsgi.py', 41 | ], 42 | entry_points={ 43 | 'console_scripts': [ 44 | 'typeidea_manage = manage:main', 45 | ] 46 | }, 47 | classifiers=[ # Optional 48 | # How mature is this project? Common values are 49 | # 3 - Alpha 50 | # 4 - Beta 51 | # 5 - Production/Stable 52 | 'Development Status :: 3 - Alpha', 53 | 54 | # Indicate who your project is intended for 55 | 'Intended Audience :: Developers', 56 | 'Topic :: Blog :: Django Blog', 57 | 58 | # Pick your license as you wish 59 | 'License :: OSI Approved :: MIT License', 60 | 61 | # Specify the Python versions you support here. In particular, ensure 62 | # that you indicate whether you support Python 2, Python 3 or both. 63 | 'Programming Language :: Python :: 3.6', 64 | ], 65 | 66 | ) 67 | -------------------------------------------------------------------------------- /ssh_config: -------------------------------------------------------------------------------- 1 | Host myserver 2 | HostName 127.0.0.1 3 | User the5fire 4 | Port 11022 5 | -------------------------------------------------------------------------------- /typeidea/blog/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/blog/__init__.py -------------------------------------------------------------------------------- /typeidea/blog/adminforms.py: -------------------------------------------------------------------------------- 1 | from dal import autocomplete 2 | from ckeditor_uploader.widgets import CKEditorUploadingWidget 3 | 4 | from django import forms 5 | 6 | from .models import Category, Tag, Post 7 | 8 | 9 | class PostAdminForm(forms.ModelForm): 10 | desc = forms.CharField(widget=forms.Textarea, label='摘要', required=False) 11 | category = forms.ModelChoiceField( 12 | queryset=Category.objects.all(), 13 | widget=autocomplete.ModelSelect2(url='category-autocomplete'), 14 | label='分类', 15 | ) 16 | tag = forms.ModelMultipleChoiceField( 17 | queryset=Tag.objects.all(), 18 | widget=autocomplete.ModelSelect2Multiple(url='tag-autocomplete'), 19 | label='标签', 20 | ) 21 | content_ck = forms.CharField(widget=CKEditorUploadingWidget(), label='正文', required=False) 22 | content_md = forms.CharField(widget=forms.Textarea(), label='正文', required=False) 23 | content = forms.CharField(widget=forms.HiddenInput(), required=False) 24 | 25 | class Meta: 26 | model = Post 27 | fields = ( 28 | 'category', 'tag', 'desc', 'title', 29 | 'is_md', 'content', 'content_md', 'content_ck', 30 | 'status' 31 | ) 32 | 33 | def __init__(self, instance=None, initial=None, **kwargs): 34 | initial = initial or {} 35 | if instance: 36 | if instance.is_md: 37 | initial['content_md'] = instance.content 38 | else: 39 | initial['content_ck'] = instance.content 40 | 41 | super().__init__(instance=instance, initial=initial, **kwargs) 42 | 43 | def clean(self): 44 | is_md = self.cleaned_data.get('is_md') 45 | if is_md: 46 | content_field_name = 'content_md' 47 | else: 48 | content_field_name = 'content_ck' 49 | content = self.cleaned_data.get(content_field_name) 50 | if not content: 51 | self.add_error(content_field_name, '必填项!') 52 | return 53 | self.cleaned_data['content'] = content 54 | return super().clean() 55 | 56 | class Media: 57 | js = ('js/post_editor.js', ) 58 | -------------------------------------------------------------------------------- /typeidea/blog/adminx.py: -------------------------------------------------------------------------------- 1 | import xadmin 2 | from xadmin.filters import RelatedFieldListFilter 3 | from xadmin.filters import manager 4 | from xadmin.layout import Row, Fieldset, Container 5 | 6 | from django.urls import reverse 7 | from django.utils.html import format_html 8 | 9 | from .adminforms import PostAdminForm 10 | from .models import Post, Category, Tag 11 | from typeidea.base_admin import BaseOwnerAdmin 12 | 13 | 14 | class PostInline: 15 | form_layout = ( 16 | Container( 17 | Row("title", "desc"), 18 | ) 19 | ) 20 | extra = 1 # 控制额外多几个 21 | model = Post 22 | 23 | 24 | @xadmin.sites.register(Category) 25 | class CategoryAdmin(BaseOwnerAdmin): 26 | # inlines = [PostInline, ] 27 | list_display = ('name', 'status', 'is_nav', 'created_time', 'post_count') 28 | fields = ('name', 'status', 'is_nav') 29 | 30 | def post_count(self, obj): 31 | return obj.post_set.count() 32 | 33 | post_count.short_description = '文章数量' 34 | 35 | 36 | @xadmin.sites.register(Tag) 37 | class TagAdmin(BaseOwnerAdmin): 38 | list_display = ('name', 'status', 'created_time') 39 | fields = ('name', 'status') 40 | 41 | 42 | class CategoryOwnerFilter(RelatedFieldListFilter): 43 | 44 | @classmethod 45 | def test(cls, field, request, params, model, admin_view, field_path): 46 | return field.name == 'category' 47 | 48 | def __init__(self, field, request, params, model, model_admin, field_path): 49 | super().__init__(field, request, params, model, model_admin, field_path) 50 | # 重新获取lookup_choices,根据owner过滤 51 | self.lookup_choices = Category.objects.filter(owner=request.user).values_list('id', 'name') 52 | 53 | 54 | manager.register(CategoryOwnerFilter, take_priority=True) 55 | 56 | 57 | @xadmin.sites.register(Post) 58 | class PostAdmin(BaseOwnerAdmin): 59 | form = PostAdminForm 60 | list_display = [ 61 | 'title', 'category', 'status', 62 | 'created_time', 'owner', 'operator' 63 | ] 64 | list_display_links = [] 65 | 66 | list_filter = ['category', ] 67 | search_fields = ['title', 'category__name'] 68 | save_on_top = True 69 | 70 | actions_on_top = True 71 | actions_on_bottom = True 72 | 73 | # 编辑页面 74 | save_on_top = True 75 | 76 | exclude = ['owner'] 77 | form_layout = ( 78 | Fieldset( 79 | '基础信息', 80 | Row("title", "category"), 81 | 'status', 82 | 'tag', 83 | ), 84 | Fieldset( 85 | '内容信息', 86 | 'desc', 87 | 'is_md', 88 | 'content_ck', 89 | 'content_md', 90 | 'content', 91 | ) 92 | ) 93 | 94 | def operator(self, obj): 95 | return format_html( 96 | '编辑', 97 | reverse('xadmin:blog_post_change', args=(obj.id,)) 98 | ) 99 | operator.short_description = '操作' 100 | 101 | # def get_media(self): 102 | # # xadmin基于bootstrap,引入会页面样式冲突,仅供参考, 故注释。 103 | # media = super().get_media() 104 | # media.add_js(['https://cdn.bootcss.com/bootstrap/4.0.0-beta.2/js/bootstrap.bundle.js']) 105 | # media.add_css({ 106 | # 'all': ("https://cdn.bootcss.com/bootstrap/4.0.0-beta.2/css/bootstrap.min.css", ), 107 | # }) 108 | # return media 109 | -------------------------------------------------------------------------------- /typeidea/blog/apis.py: -------------------------------------------------------------------------------- 1 | from rest_framework import viewsets 2 | 3 | from .models import Post, Category 4 | from .serializers import ( 5 | PostSerializer, PostDetailSerializer, 6 | CategorySerializer, CategoryDetailSerializer 7 | ) 8 | 9 | 10 | class PostViewSet(viewsets.ReadOnlyModelViewSet): 11 | """ 提供文章接口 """ 12 | serializer_class = PostSerializer 13 | queryset = Post.objects.filter(status=Post.STATUS_NORMAL) 14 | 15 | def retrieve(self, request, *args, **kwargs): 16 | self.serializer_class = PostDetailSerializer 17 | return super().retrieve(request, *args, **kwargs) 18 | 19 | def filter_queryset(self, queryset): 20 | category_id = self.request.query_params.get('category') 21 | if category_id: 22 | queryset = queryset.filter(category_id=category_id) 23 | return queryset 24 | 25 | 26 | class CategoryViewSet(viewsets.ReadOnlyModelViewSet): 27 | serializer_class = CategorySerializer 28 | queryset = Category.objects.filter(status=Category.STATUS_NORMAL) 29 | 30 | def retrieve(self, request, *args, **kwargs): 31 | self.serializer_class = CategoryDetailSerializer 32 | return super().retrieve(request, *args, **kwargs) 33 | -------------------------------------------------------------------------------- /typeidea/blog/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class BlogConfig(AppConfig): 8 | name = 'blog' 9 | -------------------------------------------------------------------------------- /typeidea/blog/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/blog/middleware/__init__.py -------------------------------------------------------------------------------- /typeidea/blog/middleware/user_id.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | 4 | USER_KEY = 'uid' 5 | TEN_YEARS = 60 * 60 * 24 * 365 * 10 6 | 7 | 8 | class UserIDMiddleware: 9 | def __init__(self, get_response): 10 | self.get_response = get_response 11 | 12 | def __call__(self, request): 13 | uid = self.generate_uid(request) 14 | request.uid = uid 15 | response = self.get_response(request) 16 | response.set_cookie(USER_KEY, uid, max_age=TEN_YEARS, httponly=True) 17 | return response 18 | 19 | def generate_uid(self, request): 20 | try: 21 | uid = request.COOKIES[USER_KEY] 22 | except KeyError: 23 | uid = uuid.uuid4().hex 24 | return uid 25 | -------------------------------------------------------------------------------- /typeidea/blog/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2018-03-10 03:49 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Category', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('name', models.CharField(max_length=50, verbose_name='\u540d\u79f0')), 24 | ('status', models.PositiveIntegerField(choices=[(1, '\u6b63\u5e38'), (0, '\u5220\u9664')], default=1, verbose_name='\u72b6\u6001')), 25 | ('is_nav', models.BooleanField(default=False, verbose_name='\u662f\u5426\u4e3a\u5bfc\u822a')), 26 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), 27 | ('author', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='\u4f5c\u8005')), 28 | ], 29 | options={ 30 | 'verbose_name': '\u5206\u7c7b', 31 | 'verbose_name_plural': '\u5206\u7c7b', 32 | }, 33 | ), 34 | migrations.CreateModel( 35 | name='Post', 36 | fields=[ 37 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 38 | ('title', models.CharField(max_length=255, verbose_name='\u6807\u9898')), 39 | ('desc', models.CharField(blank=True, max_length=1024, verbose_name='\u6458\u8981')), 40 | ('content', models.TextField(help_text='\u6b63\u6587\u5fc5\u987b\u4e3aMarkDown\u683c\u5f0f', verbose_name='\u6b63\u6587')), 41 | ('status', models.PositiveIntegerField(choices=[(1, '\u6b63\u5e38'), (0, '\u5220\u9664'), (2, '\u8349\u7a3f')], default=1, verbose_name='\u72b6\u6001')), 42 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), 43 | ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.Category', verbose_name='\u5206\u7c7b')), 44 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='\u4f5c\u8005')), 45 | ], 46 | options={ 47 | 'verbose_name': '\u6587\u7ae0', 48 | 'verbose_name_plural': '\u6587\u7ae0', 49 | }, 50 | ), 51 | migrations.CreateModel( 52 | name='Tag', 53 | fields=[ 54 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 55 | ('name', models.CharField(max_length=10, verbose_name='\u540d\u79f0')), 56 | ('status', models.PositiveIntegerField(choices=[(1, '\u6b63\u5e38'), (0, '\u5220\u9664')], default=1, verbose_name='\u72b6\u6001')), 57 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), 58 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='\u4f5c\u8005')), 59 | ], 60 | options={ 61 | 'verbose_name': '\u6807\u7b7e', 62 | 'verbose_name_plural': '\u6807\u7b7e', 63 | }, 64 | ), 65 | migrations.AddField( 66 | model_name='post', 67 | name='tag', 68 | field=models.ManyToManyField(to='blog.Tag', verbose_name='\u6807\u7b7e'), 69 | ), 70 | ] 71 | -------------------------------------------------------------------------------- /typeidea/blog/migrations/0002_auto_20180310_1505.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2018-03-10 07:05 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='category', 17 | old_name='author', 18 | new_name='owner', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /typeidea/blog/migrations/0003_auto_20180325_1737.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.11 on 2018-03-25 09:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0002_auto_20180310_1505'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='post', 17 | name='pv', 18 | field=models.PositiveIntegerField(default=1), 19 | ), 20 | migrations.AddField( 21 | model_name='post', 22 | name='uv', 23 | field=models.PositiveIntegerField(default=1), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /typeidea/blog/migrations/0004_post_content_html.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.11 on 2018-04-10 15:10 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0003_auto_20180325_1737'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='post', 17 | name='content_html', 18 | field=models.TextField(blank=True, editable=False, verbose_name='正文html代码'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /typeidea/blog/migrations/0005_auto_20180502_1730.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.13 on 2018-05-02 09:30 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('blog', '0004_post_content_html'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='post', 17 | options={'ordering': ['-id'], 'verbose_name': '文章', 'verbose_name_plural': '文章'}, 18 | ), 19 | migrations.AlterModelOptions( 20 | name='tag', 21 | options={'ordering': ['-id'], 'verbose_name': '标签', 'verbose_name_plural': '标签'}, 22 | ), 23 | migrations.AddField( 24 | model_name='post', 25 | name='is_md', 26 | field=models.BooleanField(default=False, verbose_name='markdown语法'), 27 | ), 28 | ] 29 | -------------------------------------------------------------------------------- /typeidea/blog/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/blog/migrations/__init__.py -------------------------------------------------------------------------------- /typeidea/blog/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import mistune 5 | 6 | from django.contrib.auth.models import User 7 | from django.core.cache import cache 8 | from django.db import models 9 | 10 | 11 | class Category(models.Model): 12 | STATUS_NORMAL = 1 13 | STATUS_DELETE = 0 14 | STATUS_ITEMS = ( 15 | (STATUS_NORMAL, '正常'), 16 | (STATUS_DELETE, '删除'), 17 | ) 18 | 19 | name = models.CharField(max_length=50, verbose_name="名称") 20 | status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name="状态") 21 | is_nav = models.BooleanField(default=False, verbose_name="是否为导航") 22 | owner = models.ForeignKey(User, verbose_name="作者", on_delete=models.DO_NOTHING) 23 | created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") 24 | 25 | class Meta: 26 | verbose_name = verbose_name_plural = '分类' 27 | 28 | def __str__(self): 29 | return self.name 30 | 31 | 32 | class Tag(models.Model): 33 | STATUS_NORMAL = 1 34 | STATUS_DELETE = 0 35 | STATUS_ITEMS = ( 36 | (STATUS_NORMAL, '正常'), 37 | (STATUS_DELETE, '删除'), 38 | ) 39 | 40 | name = models.CharField(max_length=10, verbose_name="名称") 41 | status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name="状态") 42 | owner = models.ForeignKey(User, verbose_name="作者", on_delete=models.DO_NOTHING) 43 | created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") 44 | 45 | class Meta: 46 | verbose_name = verbose_name_plural = '标签' 47 | ordering = ['-id'] 48 | 49 | def __str__(self): 50 | return self.name 51 | 52 | 53 | class Post(models.Model): 54 | STATUS_NORMAL = 1 55 | STATUS_DELETE = 0 56 | STATUS_DRAFT = 2 57 | STATUS_ITEMS = ( 58 | (STATUS_NORMAL, '正常'), 59 | (STATUS_DELETE, '删除'), 60 | (STATUS_DRAFT, '草稿'), 61 | ) 62 | 63 | title = models.CharField(max_length=255, verbose_name="标题") 64 | desc = models.CharField(max_length=1024, blank=True, verbose_name="摘要") 65 | content = models.TextField(verbose_name="正文", help_text="正文必须为MarkDown格式") 66 | content_html = models.TextField(verbose_name="正文html代码", blank=True, editable=False) 67 | status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name="状态") 68 | is_md = models.BooleanField(default=False, verbose_name="markdown语法") 69 | category = models.ForeignKey(Category, verbose_name="分类", on_delete=models.DO_NOTHING) 70 | tag = models.ManyToManyField(Tag, verbose_name="标签") 71 | owner = models.ForeignKey(User, verbose_name="作者", on_delete=models.DO_NOTHING) 72 | created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") 73 | 74 | pv = models.PositiveIntegerField(default=1) 75 | uv = models.PositiveIntegerField(default=1) 76 | 77 | class Meta: 78 | verbose_name = verbose_name_plural = "文章" 79 | ordering = ['-id'] 80 | 81 | def __str__(self): 82 | return self.title 83 | 84 | def save(self, *args, **kwargs): 85 | if self.is_md: 86 | self.content_html = mistune.markdown(self.content) 87 | else: 88 | self.content_html = self.content 89 | super().save(*args, **kwargs) 90 | 91 | @staticmethod 92 | def get_by_tag(tag_id): 93 | try: 94 | tag = Tag.objects.get(id=tag_id) 95 | except Tag.DoesNotExist: 96 | tag = None 97 | post_list = [] 98 | else: 99 | post_list = tag.post_set.filter(status=Post.STATUS_NORMAL)\ 100 | .select_related('owner', 'category') 101 | return post_list, tag 102 | 103 | @staticmethod 104 | def get_by_category(category_id): 105 | try: 106 | category = Category.objects.get(id=category_id) 107 | except Category.DoesNotExist: 108 | category = None 109 | post_list = [] 110 | else: 111 | post_list = category.post_set.filter(status=Post.STATUS_NORMAL)\ 112 | .select_related('owner', 'category') 113 | return post_list, category 114 | 115 | @classmethod 116 | def latest_posts(cls): 117 | return cls.objects.filter(status=cls.STATUS_NORMAL) 118 | 119 | @classmethod 120 | def hot_posts(cls): 121 | result = cache.get('hot_posts') 122 | if not result: 123 | result = cls.objects.filter(status=cls.STATUS_NORMAL).order_by('-pv') 124 | cache.set('hot_posts', result, 10 * 60) 125 | return result 126 | -------------------------------------------------------------------------------- /typeidea/blog/rss.py: -------------------------------------------------------------------------------- 1 | from django.contrib.syndication.views import Feed 2 | from django.urls import reverse 3 | from django.utils.feedgenerator import Rss201rev2Feed 4 | 5 | 6 | from .models import Post 7 | 8 | 9 | class ExtendedRSSFeed(Rss201rev2Feed): 10 | def add_item_elements(self, handler, item): 11 | super(ExtendedRSSFeed, self).add_item_elements(handler, item) 12 | handler.addQuickElement('content:html', item['content_html']) 13 | 14 | 15 | class LatestPostFeed(Feed): 16 | feed_type = ExtendedRSSFeed 17 | title = "Typeidea Blog System" 18 | link = "/rss/" 19 | description = "typeidea is a blog system power by django" 20 | 21 | def items(self): 22 | return Post.objects.filter(status=Post.STATUS_NORMAL)[:5] 23 | 24 | def item_title(self, item): 25 | return item.title 26 | 27 | def item_description(self, item): 28 | return item.desc 29 | 30 | def item_link(self, item): 31 | return reverse('post-detail', args=[item.pk]) 32 | 33 | def item_extra_kwargs(self, item): 34 | return {'content_html': self.item_content_html(item)} 35 | 36 | def item_content_html(self, item): 37 | return item.content_html 38 | -------------------------------------------------------------------------------- /typeidea/blog/serializers.py: -------------------------------------------------------------------------------- 1 | from rest_framework import serializers, pagination 2 | 3 | from .models import Post, Category 4 | 5 | 6 | class PostSerializer(serializers.HyperlinkedModelSerializer): 7 | category = serializers.SlugRelatedField( 8 | read_only=True, 9 | slug_field='name' 10 | ) 11 | tag = serializers.SlugRelatedField( 12 | many=True, 13 | read_only=True, 14 | slug_field='name' 15 | ) 16 | owner = serializers.SlugRelatedField( 17 | read_only=True, 18 | slug_field='username' 19 | ) 20 | created_time = serializers.DateTimeField(format="%Y-%m-%d %H:%M:%S") 21 | # url = serializers.HyperlinkedIdentityField(view_name='api-post-detail') 22 | 23 | class Meta: 24 | model = Post 25 | fields = ['url', 'id', 'title', 'category', 'tag', 'owner', 'created_time'] 26 | extra_kwargs = { 27 | 'url': {'view_name': 'api-post-detail'} 28 | } 29 | 30 | 31 | class PostDetailSerializer(PostSerializer): 32 | class Meta: 33 | model = Post 34 | fields = ['id', 'title', 'category', 'tag', 'owner', 'content_html', 'created_time'] 35 | 36 | 37 | class CategorySerializer(serializers.ModelSerializer): 38 | class Meta: 39 | model = Category 40 | fields = ( 41 | 'id', 'name', 'created_time' 42 | ) 43 | 44 | 45 | class CategoryDetailSerializer(CategorySerializer): 46 | posts = serializers.SerializerMethodField('paginated_posts') 47 | 48 | def paginated_posts(self, obj): 49 | posts = obj.post_set.filter(status=Post.STATUS_NORMAL) 50 | paginator = pagination.PageNumberPagination() 51 | page = paginator.paginate_queryset(posts, self.context['request']) 52 | serializer = PostSerializer(page, many=True, context={'request': self.context['request']}) 53 | return { 54 | 'count': posts.count(), 55 | 'results': serializer.data, 56 | 'previous': paginator.get_previous_link(), 57 | 'next': paginator.get_next_link(), 58 | } 59 | 60 | class Meta: 61 | model = Category 62 | fields = ( 63 | 'id', 'name', 'created_time', 'posts' 64 | ) 65 | -------------------------------------------------------------------------------- /typeidea/blog/sitemap.py: -------------------------------------------------------------------------------- 1 | from django.contrib.sitemaps import Sitemap 2 | from django.urls import reverse 3 | 4 | from .models import Post 5 | 6 | 7 | class PostSitemap(Sitemap): 8 | changefreq = "always" 9 | priority = 1.0 10 | protocol = 'https' 11 | 12 | def items(self): 13 | return Post.objects.filter(status=Post.STATUS_NORMAL) 14 | 15 | def lastmod(self, obj): 16 | return obj.created_time 17 | 18 | def location(self, obj): 19 | return reverse('post-detail', args=[obj.pk]) 20 | -------------------------------------------------------------------------------- /typeidea/blog/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /typeidea/blog/views.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import date 3 | 4 | from django.core.cache import cache 5 | from django.db.models import Q, F 6 | from django.views.generic import ListView, DetailView, TemplateView 7 | from django.shortcuts import get_object_or_404 8 | 9 | 10 | from config.models import SideBar 11 | from .models import Post, Category, Tag 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | class CommonViewMixin: 17 | def get_context_data(self, **kwargs): 18 | context = super().get_context_data(**kwargs) 19 | context.update({ 20 | 'sidebars': self.get_sidebars(), 21 | }) 22 | context.update(self.get_navs()) 23 | return context 24 | 25 | def get_sidebars(self): 26 | return SideBar.objects.filter(status=SideBar.STATUS_SHOW) 27 | 28 | def get_navs(self): 29 | categories = Category.objects.filter(status=Category.STATUS_NORMAL) 30 | nav_categories = [] 31 | normal_categories = [] 32 | for cate in categories: 33 | if cate.is_nav: 34 | nav_categories.append(cate) 35 | else: 36 | normal_categories.append(cate) 37 | 38 | return { 39 | 'navs': nav_categories, 40 | 'categories': normal_categories, 41 | } 42 | 43 | 44 | class IndexView(CommonViewMixin, ListView): 45 | queryset = Post.objects.filter(status=Post.STATUS_NORMAL)\ 46 | .select_related('owner')\ 47 | .select_related('category') 48 | paginate_by = 5 49 | context_object_name = 'post_list' 50 | template_name = 'blog/list.html' 51 | 52 | 53 | class CategoryView(IndexView): 54 | def get_context_data(self, **kwargs): 55 | context = super().get_context_data(**kwargs) 56 | category_id = self.kwargs.get('category_id') 57 | category = get_object_or_404(Category, pk=category_id) 58 | context.update({ 59 | 'category': category, 60 | }) 61 | return context 62 | 63 | def get_queryset(self): 64 | """ 重写querset,根据分类过滤 """ 65 | queryset = super().get_queryset() 66 | category_id = self.kwargs.get('category_id') 67 | return queryset.filter(category_id=category_id) 68 | 69 | 70 | class TagView(IndexView): 71 | def get_context_data(self, **kwargs): 72 | context = super().get_context_data(**kwargs) 73 | tag_id = self.kwargs.get('tag_id') 74 | tag = get_object_or_404(Tag, pk=tag_id) 75 | context.update({ 76 | 'tag': tag, 77 | }) 78 | return context 79 | 80 | def get_queryset(self): 81 | """ 重写querset,根据标签过滤 """ 82 | queryset = super().get_queryset() 83 | tag_id = self.kwargs.get('tag_id') 84 | return queryset.filter(tag__id=tag_id) 85 | 86 | 87 | class PostDetailView(CommonViewMixin, DetailView): 88 | queryset = Post.objects.filter(status=Post.STATUS_NORMAL) 89 | template_name = 'blog/detail.html' 90 | context_object_name = 'post' 91 | pk_url_kwarg = 'post_id' 92 | 93 | def get(self, request, *args, **kwargs): 94 | response = super().get(request, *args, **kwargs) 95 | self.handle_visited() 96 | return response 97 | 98 | def handle_visited(self): 99 | increase_pv = False 100 | increase_uv = False 101 | uid = self.request.uid 102 | pv_key = 'pv:%s:%s' % (uid, self.request.path) 103 | if not cache.get(pv_key): 104 | increase_pv = True 105 | cache.set(pv_key, 1, 1*60) # 1分钟有效 106 | 107 | uv_key = 'uv:%s:%s:%s' % (uid, str(date.today()), self.request.path) 108 | if not cache.get(uv_key): 109 | increase_uv = True 110 | cache.set(uv_key, 1, 24*60*60) # 24小时有效 111 | 112 | if increase_pv and increase_uv: 113 | Post.objects.filter(pk=self.object.id).update(pv=F('pv') + 1, uv=F('uv') + 1) 114 | elif increase_pv: 115 | Post.objects.filter(pk=self.object.id).update(pv=F('pv') + 1) 116 | elif increase_uv: 117 | Post.objects.filter(pk=self.object.id).update(uv=F('uv') + 1) 118 | 119 | 120 | class SearchView(IndexView): 121 | def get_context_data(self): 122 | context = super().get_context_data() 123 | context.update({ 124 | 'keyword': self.request.GET.get('keyword', '') 125 | }) 126 | return context 127 | 128 | def get_queryset(self): 129 | queryset = super().get_queryset() 130 | keyword = self.request.GET.get('keyword') 131 | if not keyword: 132 | return queryset 133 | return queryset.filter(Q(title__icontains=keyword) | Q(desc__icontains=keyword)) 134 | 135 | 136 | class AuthorView(IndexView): 137 | def get_queryset(self): 138 | queryset = super().get_queryset() 139 | author_id = self.kwargs.get('owner_id') 140 | return queryset.filter(owner_id=author_id) 141 | 142 | 143 | class Handler404(CommonViewMixin, TemplateView): 144 | template_name = '404.html' 145 | 146 | def get(self, request, *args, **kwargs): 147 | context = self.get_context_data(**kwargs) 148 | return self.render_to_response(context, status=404) 149 | 150 | 151 | class Handler50x(CommonViewMixin, TemplateView): 152 | template_name = '50x.html' 153 | 154 | def get(self, request, *args, **kwargs): 155 | context = self.get_context_data(**kwargs) 156 | return self.render_to_response(context, status=500) 157 | -------------------------------------------------------------------------------- /typeidea/comment/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/comment/__init__.py -------------------------------------------------------------------------------- /typeidea/comment/adminx.py: -------------------------------------------------------------------------------- 1 | import xadmin 2 | 3 | from .models import Comment 4 | 5 | 6 | @xadmin.sites.register(Comment) 7 | class CommentAdmin: 8 | list_display = ('target', 'nickname', 'content', 'website', 'created_time') 9 | -------------------------------------------------------------------------------- /typeidea/comment/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class CommentConfig(AppConfig): 8 | name = 'comment' 9 | -------------------------------------------------------------------------------- /typeidea/comment/forms.py: -------------------------------------------------------------------------------- 1 | import mistune 2 | 3 | from django import forms 4 | 5 | from .models import Comment 6 | 7 | 8 | class CommentForm(forms.ModelForm): 9 | nickname = forms.CharField( 10 | label='昵称', 11 | max_length=50, 12 | widget=forms.widgets.Input( 13 | attrs={'class': 'form-control', 'style': "width: 60%;"} 14 | ) 15 | ) 16 | email = forms.CharField( 17 | label='Email', 18 | max_length=50, 19 | widget=forms.widgets.EmailInput( 20 | attrs={'class': 'form-control', 'style': "width: 60%;"} 21 | ) 22 | ) 23 | website = forms.CharField( 24 | label='网站', 25 | max_length=100, 26 | widget=forms.widgets.URLInput( 27 | attrs={'class': 'form-control', 'style': "width: 60%;"} 28 | ) 29 | ) 30 | 31 | content = forms.CharField( 32 | label="内容", 33 | max_length=500, 34 | widget=forms.widgets.Textarea( 35 | attrs={'rows': 6, 'cols': 60, 'class': 'form-control'} 36 | ) 37 | ) 38 | 39 | def clean_content(self): 40 | content = self.cleaned_data.get('content') 41 | if len(content) < 10: 42 | raise forms.ValidationError('内容长度怎么能这么短呢!!') 43 | content = mistune.markdown(content) 44 | return content 45 | 46 | class Meta: 47 | model = Comment 48 | fields = ['nickname', 'email', 'website', 'content'] 49 | -------------------------------------------------------------------------------- /typeidea/comment/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2018-03-10 03:49 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ('blog', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Comment', 20 | fields=[ 21 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 22 | ('content', models.CharField(max_length=2000, verbose_name='\u5185\u5bb9')), 23 | ('nickname', models.CharField(max_length=50, verbose_name='\u6635\u79f0')), 24 | ('website', models.URLField(verbose_name='\u7f51\u7ad9')), 25 | ('email', models.EmailField(max_length=254, verbose_name='\u90ae\u7bb1')), 26 | ('status', models.PositiveIntegerField(choices=[(1, '\u6b63\u5e38'), (0, '\u5220\u9664')], default=1, verbose_name='\u72b6\u6001')), 27 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), 28 | ('target', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='blog.Post', verbose_name='\u8bc4\u8bba\u76ee\u6807')), 29 | ], 30 | options={ 31 | 'verbose_name': '\u8bc4\u8bba', 32 | 'verbose_name_plural': '\u8bc4\u8bba', 33 | }, 34 | ), 35 | ] 36 | -------------------------------------------------------------------------------- /typeidea/comment/migrations/0002_auto_20180410_1922.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.11 on 2018-04-10 11:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('comment', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='comment', 17 | name='target', 18 | field=models.CharField(max_length=100, verbose_name='评论目标'), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /typeidea/comment/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/comment/migrations/__init__.py -------------------------------------------------------------------------------- /typeidea/comment/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models 5 | 6 | 7 | class Comment(models.Model): 8 | STATUS_NORMAL = 1 9 | STATUS_DELETE = 0 10 | STATUS_ITEMS = ( 11 | (STATUS_NORMAL, '正常'), 12 | (STATUS_DELETE, '删除'), 13 | ) 14 | target = models.CharField(max_length=100, verbose_name="评论目标") 15 | content = models.CharField(max_length=2000, verbose_name="内容") 16 | nickname = models.CharField(max_length=50, verbose_name="昵称") 17 | website = models.URLField(verbose_name="网站") 18 | email = models.EmailField(verbose_name="邮箱") 19 | status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name="状态") 20 | created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") 21 | 22 | class Meta: 23 | verbose_name = verbose_name_plural = "评论" 24 | 25 | @classmethod 26 | def get_by_target(cls, target): 27 | return cls.objects.filter(target=target, status=cls.STATUS_NORMAL) 28 | -------------------------------------------------------------------------------- /typeidea/comment/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/comment/templatetags/__init__.py -------------------------------------------------------------------------------- /typeidea/comment/templatetags/comment_block.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from comment.forms import CommentForm 4 | from comment.models import Comment 5 | 6 | register = template.Library() 7 | 8 | 9 | @register.inclusion_tag('comment/block.html') 10 | def comment_block(target): 11 | return { 12 | 'target': target, 13 | 'comment_form': CommentForm(), 14 | 'comment_list': Comment.get_by_target(target), 15 | } 16 | -------------------------------------------------------------------------------- /typeidea/comment/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /typeidea/comment/views.py: -------------------------------------------------------------------------------- 1 | from captcha.models import CaptchaStore 2 | from captcha.helpers import captcha_image_url 3 | from django.http import JsonResponse 4 | from django.shortcuts import redirect 5 | from django.utils import timezone 6 | from django.views.generic import TemplateView, View 7 | 8 | from .forms import CommentForm 9 | 10 | 11 | class CommentView(TemplateView): 12 | http_method_names = ['post'] 13 | template_name = 'comment/result.html' 14 | 15 | def post(self, request, *args, **kwargs): 16 | comment_form = CommentForm(request.POST) 17 | target = request.POST.get('target') 18 | 19 | if comment_form.is_valid(): 20 | instance = comment_form.save(commit=False) 21 | instance.target = target 22 | instance.save() 23 | succeed = True 24 | return redirect(target) 25 | else: 26 | succeed = False 27 | 28 | context = { 29 | 'succeed': succeed, 30 | 'form': comment_form, 31 | 'target': target, 32 | } 33 | return self.render_to_response(context) 34 | 35 | 36 | class VerifyCaptcha(View): 37 | def get(self, request): 38 | captcha_id = CaptchaStore.generate_key() 39 | return JsonResponse({ 40 | 'captcha_id': captcha_id, 41 | 'image_src': captcha_image_url(captcha_id), 42 | }) 43 | 44 | def post(self, request): 45 | captcha_id = request.POST.get('captcha_id') 46 | captcha = request.POST.get('captcha') 47 | captcha = captcha.lower() 48 | 49 | try: 50 | CaptchaStore.objects.get(response=captcha, hashkey=captcha_id, expiration__gt=timezone.now()).delete() 51 | except CaptchaStore.DoesNotExist: 52 | return JsonResponse({'msg': '验证码错误'}, status=400) 53 | 54 | return JsonResponse({}) 55 | -------------------------------------------------------------------------------- /typeidea/config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/config/__init__.py -------------------------------------------------------------------------------- /typeidea/config/adminx.py: -------------------------------------------------------------------------------- 1 | import xadmin 2 | 3 | from .models import Link, SideBar 4 | from typeidea.base_admin import BaseOwnerAdmin 5 | 6 | 7 | @xadmin.sites.register(Link) 8 | class LinkAdmin(BaseOwnerAdmin): 9 | list_display = ('title', 'href', 'status', 'weight', 'created_time') 10 | fields = ('title', 'href', 'status', 'weight') 11 | 12 | def save_model(self, request, obj, form, change): 13 | obj.owner = request.user 14 | return super(LinkAdmin, self).save_model(request, obj, form, change) 15 | 16 | 17 | @xadmin.sites.register(SideBar) 18 | class SideBarAdmin(BaseOwnerAdmin): 19 | list_display = ('title', 'display_type', 'content', 'created_time') 20 | fields = ('title', 'display_type', 'content') 21 | 22 | def save_model(self, request, obj, form, change): 23 | obj.owner = request.user 24 | return super(SideBarAdmin, self).save_model(request, obj, form, change) 25 | -------------------------------------------------------------------------------- /typeidea/config/apps.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.apps import AppConfig 5 | 6 | 7 | class ConfigConfig(AppConfig): 8 | name = 'config' 9 | -------------------------------------------------------------------------------- /typeidea/config/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.8 on 2018-03-10 03:49 3 | from __future__ import unicode_literals 4 | 5 | from django.conf import settings 6 | from django.db import migrations, models 7 | import django.db.models.deletion 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | initial = True 13 | 14 | dependencies = [ 15 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name='Link', 21 | fields=[ 22 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 23 | ('title', models.CharField(max_length=50, verbose_name='\u6807\u9898')), 24 | ('href', models.URLField(verbose_name='\u94fe\u63a5')), 25 | ('status', models.PositiveIntegerField(choices=[(1, '\u6b63\u5e38'), (0, '\u5220\u9664')], default=1, verbose_name='\u72b6\u6001')), 26 | ('weight', models.PositiveIntegerField(choices=[(1, 1), (2, 2), (3, 3), (4, 4), (5, 5)], default=1, help_text='\u6743\u91cd\u9ad8\u5c55\u793a\u987a\u5e8f\u9760\u524d', verbose_name='\u6743\u91cd')), 27 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), 28 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='\u4f5c\u8005')), 29 | ], 30 | options={ 31 | 'verbose_name': '\u53cb\u94fe', 32 | 'verbose_name_plural': '\u53cb\u94fe', 33 | }, 34 | ), 35 | migrations.CreateModel( 36 | name='SideBar', 37 | fields=[ 38 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 39 | ('title', models.CharField(max_length=50, verbose_name='\u6807\u9898')), 40 | ('display_type', models.PositiveIntegerField(choices=[(1, 'HTML'), (2, '\u6700\u65b0\u6587\u7ae0'), (3, '\u6700\u70ed\u6587\u7ae0'), (4, '\u6700\u8fd1\u8bc4\u8bba')], default=1, verbose_name='\u5c55\u793a\u7c7b\u578b')), 41 | ('content', models.CharField(blank=True, help_text='\u5982\u679c\u8bbe\u7f6e\u7684\u4e0d\u662fHTML\u7c7b\u578b\uff0c\u53ef\u4e3a\u7a7a', max_length=500, verbose_name='\u5185\u5bb9')), 42 | ('status', models.PositiveIntegerField(choices=[(1, '\u5c55\u793a'), (0, '\u9690\u85cf')], default=1, verbose_name='\u72b6\u6001')), 43 | ('created_time', models.DateTimeField(auto_now_add=True, verbose_name='\u521b\u5efa\u65f6\u95f4')), 44 | ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='\u4f5c\u8005')), 45 | ], 46 | options={ 47 | 'verbose_name': '\u4fa7\u8fb9\u680f', 48 | 'verbose_name_plural': '\u4fa7\u8fb9\u680f', 49 | }, 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /typeidea/config/migrations/0002_auto_20180410_1922.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.11 on 2018-04-10 11:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('config', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='link', 17 | options={'ordering': ['-weight'], 'verbose_name': '友链', 'verbose_name_plural': '友链'}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /typeidea/config/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/config/migrations/__init__.py -------------------------------------------------------------------------------- /typeidea/config/models.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.contrib.auth.models import User 5 | from django.db import models 6 | from django.template.loader import render_to_string 7 | 8 | 9 | class Link(models.Model): 10 | STATUS_NORMAL = 1 11 | STATUS_DELETE = 0 12 | STATUS_ITEMS = ( 13 | (STATUS_NORMAL, '正常'), 14 | (STATUS_DELETE, '删除'), 15 | ) 16 | title = models.CharField(max_length=50, verbose_name="标题") 17 | href = models.URLField(verbose_name="链接") # 默认长度200 18 | status = models.PositiveIntegerField(default=STATUS_NORMAL, choices=STATUS_ITEMS, verbose_name="状态") 19 | weight = models.PositiveIntegerField(default=1, choices=zip(range(1, 6), range(1, 6)), 20 | verbose_name="权重", 21 | help_text="权重高展示顺序靠前") 22 | 23 | owner = models.ForeignKey(User, verbose_name="作者", on_delete=models.DO_NOTHING) 24 | created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") 25 | 26 | class Meta: 27 | verbose_name = verbose_name_plural = "友链" 28 | ordering = ['-weight', ] 29 | 30 | def __str__(self): 31 | return self.title 32 | 33 | 34 | class SideBar(models.Model): 35 | STATUS_SHOW = 1 36 | STATUS_HIDE = 0 37 | STATUS_ITEMS = ( 38 | (STATUS_SHOW, '展示'), 39 | (STATUS_HIDE, '隐藏'), 40 | ) 41 | DISPLAY_HTML = 1 42 | DISPLAY_LATEST = 2 43 | DISPLAY_HOT = 3 44 | DISPLAY_COMMENT = 4 45 | SIDE_TYPE = ( 46 | (DISPLAY_HTML, 'HTML'), 47 | (DISPLAY_LATEST, '最新文章'), 48 | (DISPLAY_HOT, '最热文章'), 49 | (DISPLAY_COMMENT, '最近评论'), 50 | ) 51 | title = models.CharField(max_length=50, verbose_name="标题") 52 | display_type = models.PositiveIntegerField(default=1, choices=SIDE_TYPE, 53 | verbose_name="展示类型") 54 | content = models.CharField(max_length=500, blank=True, verbose_name="内容", 55 | help_text="如果设置的不是HTML类型,可为空") 56 | 57 | status = models.PositiveIntegerField(default=STATUS_SHOW, choices=STATUS_ITEMS, verbose_name="状态") 58 | owner = models.ForeignKey(User, verbose_name="作者", on_delete=models.DO_NOTHING) 59 | created_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") 60 | 61 | class Meta: 62 | verbose_name = verbose_name_plural = "侧边栏" 63 | 64 | def __str__(self): 65 | return self.title 66 | 67 | def _render_latest(self): 68 | pass 69 | 70 | def content_html(self): 71 | """ 通过直接渲染模板 """ 72 | from blog.models import Post # 避免循环引用 73 | from comment.models import Comment 74 | 75 | result = '' 76 | if self.display_type == self.DISPLAY_HTML: 77 | result = self.content 78 | elif self.display_type == self.DISPLAY_LATEST: 79 | context = { 80 | 'posts': Post.latest_posts() 81 | } 82 | result = render_to_string('config/blocks/sidebar_posts.html', context) 83 | elif self.display_type == self.DISPLAY_HOT: 84 | context = { 85 | 'posts': Post.hot_posts() 86 | } 87 | result = render_to_string('config/blocks/sidebar_posts.html', context) 88 | elif self.display_type == self.DISPLAY_COMMENT: 89 | context = { 90 | 'comments': Comment.objects.filter(status=Comment.STATUS_NORMAL) 91 | } 92 | result = render_to_string('config/blocks/sidebar_comments.html', context) 93 | return result 94 | -------------------------------------------------------------------------------- /typeidea/config/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.test import TestCase 5 | 6 | # Create your tests here. 7 | -------------------------------------------------------------------------------- /typeidea/config/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import ListView 2 | 3 | from blog.views import CommonViewMixin 4 | from .models import Link 5 | 6 | 7 | class LinkListView(CommonViewMixin, ListView): 8 | queryset = Link.objects.filter(status=Link.STATUS_NORMAL) 9 | template_name = 'config/links.html' 10 | context_object_name = 'link_list' 11 | -------------------------------------------------------------------------------- /typeidea/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | 6 | def main(): 7 | profile = os.environ.get('TYPEIDEA_PROFILE', 'develop') 8 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "typeidea.settings.%s" % profile) 9 | 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError: 13 | # The above import may fail for some other reason. Ensure that the 14 | # issue is really that Django is missing to avoid masking other 15 | # exceptions on Python 2. 16 | try: 17 | import django 18 | except ImportError: 19 | raise ImportError( 20 | "Couldn't import Django. Are you sure it's installed and " 21 | "available on your PYTHONPATH environment variable? Did you " 22 | "forget to activate a virtual environment?" 23 | ) 24 | raise 25 | execute_from_command_line(sys.argv) 26 | 27 | if __name__ == "__main__": 28 | main() 29 | -------------------------------------------------------------------------------- /typeidea/typeidea/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/typeidea/__init__.py -------------------------------------------------------------------------------- /typeidea/typeidea/autocomplete.py: -------------------------------------------------------------------------------- 1 | from dal import autocomplete 2 | 3 | from blog.models import Category, Tag 4 | 5 | 6 | class CategoryAutocomplete(autocomplete.Select2QuerySetView): 7 | def get_queryset(self): 8 | if not self.request.user.is_authenticated: 9 | return Category.objects.none() 10 | 11 | qs = Category.objects.all() 12 | 13 | if self.q: 14 | qs = qs.filter(name__istartswith=self.q) 15 | return qs 16 | 17 | 18 | class TagAutocomplete(autocomplete.Select2QuerySetView): 19 | def get_queryset(self): 20 | if not self.request.user.is_authenticated: 21 | return Tag.objects.none() 22 | 23 | qs = Tag.objects.all() 24 | 25 | if self.q: 26 | qs = qs.filter(name__istartswith=self.q) 27 | return qs 28 | -------------------------------------------------------------------------------- /typeidea/typeidea/base_admin.py: -------------------------------------------------------------------------------- 1 | class BaseOwnerAdmin: 2 | """ 3 | 1. 用来处理文章、分类、标签、侧边栏、友链这些model的owner字段自动补充 4 | 2. 用来针对queryset过滤当前用户的数据 5 | """ 6 | exclude = ('owner', ) 7 | 8 | def get_list_queryset(self): 9 | request = self.request 10 | qs = super().get_list_queryset() 11 | return qs.filter(owner=request.user) 12 | 13 | def save_models(self): 14 | self.new_obj.owner = self.request.user 15 | return super().save_models() 16 | -------------------------------------------------------------------------------- /typeidea/typeidea/settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/the5fire/typeidea/336a9d961059ec7eab1b0b387fb9c20d3dd35cb5/typeidea/typeidea/settings/__init__.py -------------------------------------------------------------------------------- /typeidea/typeidea/settings/base.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for typeidea project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.11.8. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.11/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.11/ref/settings/ 11 | """ 12 | 13 | import os 14 | VERSION = '${version}' 15 | 16 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 17 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 18 | 19 | 20 | # Quick-start development settings - unsuitable for production 21 | # See https://docs.djangoproject.com/en/1.11/howto/deployment/checklist/ 22 | 23 | # SECURITY WARNING: keep the secret key used in production secret! 24 | SECRET_KEY = 'mspj07-bob3&2(*6h+)30iat$mnnd6g05m4ap&-eu0d!774c%%' 25 | 26 | # SECURITY WARNING: don't run with debug turned on in production! 27 | DEBUG = True 28 | 29 | ALLOWED_HOSTS = ['*'] 30 | 31 | 32 | # Application definition 33 | 34 | INSTALLED_APPS = [ 35 | 'typeidea', 36 | 'blog', 37 | 'config', 38 | 'comment', 39 | 'captcha', 40 | 41 | 'ckeditor', 42 | 'ckeditor_uploader', 43 | 'dal', 44 | 'dal_select2', 45 | 'xadmin', 46 | 'crispy_forms', 47 | 'rest_framework', 48 | 49 | 'django.contrib.admin', 50 | 'django.contrib.auth', 51 | 'django.contrib.contenttypes', 52 | 'django.contrib.sessions', 53 | 'django.contrib.messages', 54 | 'django.contrib.staticfiles', 55 | ] 56 | 57 | MIDDLEWARE = [ 58 | 'blog.middleware.user_id.UserIDMiddleware', 59 | 'django.middleware.security.SecurityMiddleware', 60 | 'django.contrib.sessions.middleware.SessionMiddleware', 61 | 'django.middleware.common.CommonMiddleware', 62 | 'django.middleware.csrf.CsrfViewMiddleware', 63 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 64 | 'django.contrib.messages.middleware.MessageMiddleware', 65 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 66 | ] 67 | 68 | ROOT_URLCONF = 'typeidea.urls' 69 | 70 | THEME = 'bootstrap' 71 | 72 | TEMPLATES = [ 73 | { 74 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 75 | 'DIRS': [os.path.join(BASE_DIR, 'themes', THEME, 'templates')], 76 | 'APP_DIRS': True, 77 | 'OPTIONS': { 78 | 'context_processors': [ 79 | 'django.template.context_processors.debug', 80 | 'django.template.context_processors.request', 81 | 'django.contrib.auth.context_processors.auth', 82 | 'django.contrib.messages.context_processors.messages', 83 | ], 84 | }, 85 | }, 86 | ] 87 | 88 | WSGI_APPLICATION = 'typeidea.wsgi.application' 89 | 90 | 91 | # Database 92 | # https://docs.djangoproject.com/en/1.11/ref/settings/#databases 93 | 94 | DATABASES = { 95 | 'default': { 96 | 'ENGINE': 'django.db.backends.sqlite3', 97 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 98 | } 99 | } 100 | 101 | 102 | # Password validation 103 | # https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators 104 | 105 | AUTH_PASSWORD_VALIDATORS = [ 106 | { 107 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 108 | }, 109 | { 110 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 111 | }, 112 | { 113 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 114 | }, 115 | { 116 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 117 | }, 118 | ] 119 | 120 | 121 | # Internationalization 122 | # https://docs.djangoproject.com/en/1.11/topics/i18n/ 123 | 124 | LANGUAGE_CODE = 'zh-hans' 125 | 126 | TIME_ZONE = 'Asia/Shanghai' 127 | 128 | USE_I18N = True 129 | 130 | USE_L10N = True 131 | 132 | USE_TZ = True 133 | 134 | 135 | # Static files (CSS, JavaScript, Images) 136 | # https://docs.djangoproject.com/en/1.11/howto/static-files/ 137 | 138 | STATIC_URL = '/static/' 139 | STATICFILES_DIRS = [ 140 | os.path.join(BASE_DIR, 'themes', THEME, "static"), 141 | ] 142 | 143 | XADMIN_TITLE = 'Typeidea管理后台' 144 | XADMIN_FOOTER_TITLE = 'power by the5fire.com' 145 | 146 | CKEDITOR_CONFIGS = { 147 | 'default': { 148 | 'toolbar': 'full', 149 | 'height': 300, 150 | 'width': 800, 151 | 'tabSpaces': 4, 152 | 'extraPlugins': 'codesnippet', 153 | }, 154 | } 155 | 156 | MEDIA_URL = "/media/" 157 | MEDIA_ROOT = os.path.join(BASE_DIR, "media") 158 | CKEDITOR_UPLOAD_PATH = "article_images" 159 | 160 | DEFAULT_FILE_STORAGE = 'typeidea.storage.WatermarkStorage' 161 | REST_FRAMEWORK = { 162 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 163 | 'PAGE_SIZE': 2, 164 | } 165 | -------------------------------------------------------------------------------- /typeidea/typeidea/settings/develop.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | import os 3 | import raven 4 | 5 | from .base import * # NOQA 6 | 7 | 8 | DEBUG = True 9 | 10 | DATABASES = { 11 | 'default': { 12 | 'ENGINE': 'django.db.backends.mysql', 13 | 'NAME': 'typeidea_db', 14 | 'USER': 'root', 15 | 'PASSWORD': '', 16 | 'HOST': '127.0.0.1', 17 | 'PORT': 3306, 18 | # 'OPTIONS': {'charset': 'utf8mb4'} 19 | } 20 | } 21 | INSTALLED_APPS += [ 22 | 'debug_toolbar', 23 | # 'raven.contrib.django.raven_compat', 24 | ] 25 | 26 | MIDDLEWARE += [ 27 | 'debug_toolbar.middleware.DebugToolbarMiddleware', 28 | ] 29 | 30 | INTERNAL_IPS = ['127.0.0.1'] 31 | STATIC_ROOT = os.path.join(BASE_DIR, 'static_files/') 32 | 33 | LOGGING = { 34 | 'version': 1, 35 | 'disable_existing_loggers': False, 36 | 'formatters': { 37 | 'default': { 38 | 'format': '%(levelname)s %(asctime)s %(module)s:' 39 | '%(funcName)s:%(lineno)d %(message)s' 40 | }, 41 | }, 42 | 'handlers': { 43 | 'console': { 44 | 'level': 'INFO', 45 | 'class': 'logging.StreamHandler', 46 | 'formatter': 'default', 47 | }, 48 | 'file': { 49 | 'level': 'INFO', 50 | 'class': 'logging.handlers.RotatingFileHandler', 51 | 'filename': 'typeidea.log', 52 | 'formatter': 'default', 53 | 'maxBytes': 1024 * 1024, # 1M 54 | 'backupCount': 5, 55 | }, 56 | 57 | }, 58 | 'loggers': { 59 | '': { 60 | 'handlers': ['console'], 61 | 'level': 'INFO', 62 | 'propagate': True, 63 | }, 64 | } 65 | } 66 | 67 | 68 | RAVEN_CONFIG = { 69 | 'dsn': 'http://ac72ba920a864100b375f3f626d54835:bd5aae601a234b5ca02a5441a88b7814@127.0.0.1:19000//3', 70 | 'release': VERSION, # 默认的配置是从git项目读取最新的commit,我们这里使用已经base中配置的VERSEION。 71 | } 72 | -------------------------------------------------------------------------------- /typeidea/typeidea/settings/product.py: -------------------------------------------------------------------------------- 1 | from .base import * # NOQA 2 | 3 | DEBUG = False 4 | 5 | DATABASES = { 6 | 'default': { 7 | 'ENGINE': 'django.db.backends.mysql', 8 | 'NAME': 'typeidea_db', 9 | 'USER': 'root', 10 | 'PASSWORD': '', 11 | 'HOST': '127.0.0.1', 12 | 'PORT': 3306, 13 | 'CONN_MAX_AGE': 60, 14 | 'OPTIONS': {'charset': 'utf8mb4'} 15 | } 16 | } 17 | 18 | ADMINS = MANAGERS = ( 19 | ('the5fire', 'thefivefire@gmail.com'), # 你的邮件地址 20 | ) 21 | 22 | # EMAIL_HOST = '' 23 | # EMAIL_HOST_USER = 'the5fire' 24 | # EMAIL_HOST_PASSWORD = '' 25 | # EMAIL_SUBJECT_PREFIX = '' 26 | # DEFAULT_FROM_EMAIL = '' 27 | # SERVER_EMAIL = '' 28 | 29 | STATIC_ROOT = '/home/the5fire/venvs/typeidea-env/static_files/' 30 | 31 | REDIS_URL = '127.0.0.1:6379:1' 32 | 33 | CACHES = { 34 | 'default': { 35 | 'BACKEND': 'django_redis.cache.RedisCache', 36 | 'LOCATION': REDIS_URL, 37 | 'TIMEOUT': 300, 38 | 'OPTIONS': { 39 | # 'PASSWORD': '<对应密码>', 40 | 'CLIENT_CLASS': 'django_redis.client.DefaultClient', 41 | 'PARSER_CLASS': 'redis.connection.HiredisParser', 42 | }, 43 | 'CONNECTION_POOL_CLASS': 'redis.connection.BlockingConnectionPool', 44 | } 45 | } 46 | 47 | LOGGING = { 48 | 'version': 1, 49 | 'disable_existing_loggers': False, 50 | 'formatters': { 51 | 'default': { 52 | 'format': '%(levelname)s %(asctime)s %(module)s:' 53 | '%(funcName)s:%(lineno)d %(message)s' 54 | }, 55 | }, 56 | 'handlers': { 57 | 'console': { 58 | 'level': 'INFO', 59 | 'class': 'logging.StreamHandler', 60 | 'formatter': 'default', 61 | }, 62 | 'file': { 63 | 'level': 'INFO', 64 | 'class': 'logging.handlers.RotatingFileHandler', 65 | 'filename': 'typeidea.log', 66 | 'formatter': 'default', 67 | 'maxBytes': 1024 * 1024, # 1M 68 | 'backupCount': 5, 69 | }, 70 | 71 | }, 72 | 'loggers': { 73 | '': { 74 | 'handlers': ['console'], 75 | 'level': 'INFO', 76 | 'propagate': True, 77 | }, 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /typeidea/typeidea/storage.py: -------------------------------------------------------------------------------- 1 | from io import BytesIO 2 | 3 | from django.core.files.storage import FileSystemStorage 4 | from django.core.files.uploadedfile import InMemoryUploadedFile 5 | 6 | from PIL import Image, ImageDraw, ImageFont 7 | 8 | 9 | class WatermarkStorage(FileSystemStorage): 10 | def save(self, name, content, max_length=None): 11 | # 处理逻辑 12 | if 'image' in content.content_type: 13 | # 加水印 14 | image = self.watermark_with_text(content, 'the5fire.com', 'red') 15 | content = self.convert_image_to_file(image, name) 16 | 17 | return super().save(name, content, max_length=max_length) 18 | 19 | def convert_image_to_file(self, image, name): 20 | temp = BytesIO() 21 | image.save(temp, format='PNG') 22 | file_size = temp.tell() 23 | return InMemoryUploadedFile(temp, None, name, 'image/png', file_size, None) 24 | 25 | def watermark_with_text(self, file_obj, text, color, fontfamily=None): 26 | image = Image.open(file_obj).convert('RGBA') 27 | draw = ImageDraw.Draw(image) 28 | width, height = image.size 29 | margin = 10 30 | if fontfamily: 31 | font = ImageFont.truetype(fontfamily, int(height / 20)) 32 | else: 33 | font = None 34 | textWidth, textHeight = draw.textsize(text, font) 35 | x = (width - textWidth - margin) / 2 # 计算横轴位置 36 | y = height - textHeight - margin # 计算纵轴位置 37 | draw.text((x, y), text, color, font) 38 | 39 | return image 40 | -------------------------------------------------------------------------------- /typeidea/typeidea/templates/static_page/demo.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |基于Django的多人博客系统
39 |基于Django的多人博客系统
40 |22 | {% autoescape off %} 23 | {{ post.content_html }} 24 | {% endautoescape %} 25 |
26 | {% endif %} 27 | 28 | {% comment_block request.path %} 29 | {% endblock %} 30 | -------------------------------------------------------------------------------- /typeidea/typeidea/themes/bootstrap/templates/blog/list.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %} 4 | {% if tag %} 5 | 标签页: {{ tag.name }} 6 | {% elif category %} 7 | 分类页: {{ category.name }} 8 | {% else %} 9 | 首页 10 | {% endif %} 11 | {% endblock %} 12 | 13 | 14 | {% block main %} 15 | {% for post in post_list %} 16 |{{ post.desc }}完整内容
28 |# | 10 |名称 | 11 |网址 | 12 |
---|---|---|
{{ forloop.counter }} | 18 |{{ link.title }} | 19 |{{ link.href }} | 20 |
13 | {{ post.content }} 14 |
15 | {% endif %} 16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /typeidea/typeidea/themes/default/templates/blog/list.html: -------------------------------------------------------------------------------- 1 | {% extends "./base.html" %} 2 | 3 | {% block title %} 4 | {% if tag %} 5 | 标签页: {{ tag.name }} 6 | {% elif category %} 7 | 分类页: {{ category.name }} 8 | {% else %} 9 | 首页 10 | {% endif %} 11 | {% endblock %} 12 | 13 | 14 | {% block main %} 15 |{{ post.desc }}
24 |
14 | {% for comment in comment_list %} 15 |-
16 |
17 | {{ comment.nickname }} {{ comment.created_time }}
18 |
19 |
20 | {% autoescape off %}
21 | {{ comment.content }}
22 | {% endautoescape %}
23 |
24 |
25 | {% endfor %}
26 |
27 |