├── .coveragerc ├── .gitignore ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.md ├── README.rst ├── blog.json ├── blog.service ├── docker ├── Python-3.6.0.tgz ├── Shanghai ├── blog.df ├── deploy.py ├── phantomjs-2.1.1-linux-x86_64.tar.bz2 ├── requirements.txt ├── settings.py ├── start.sh ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ ├── font-awesome │ │ │ ├── css │ │ │ │ ├── font-awesome.css │ │ │ │ └── font-awesome.min.css │ │ │ └── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ └── fontawesome-webfont.woff2 │ │ └── style.css │ ├── img │ │ ├── avatar.png │ │ ├── logo.png │ │ └── single-post.jpg │ └── js │ │ ├── JSXTransformer.js │ │ ├── bootstrap.min.js │ │ ├── browser.min.js │ │ ├── elasticsearch.jquery.min.js │ │ ├── jquery-1.12.4.min.js │ │ ├── modernizr.js │ │ ├── moment.min.js │ │ ├── react-0.13.4.js │ │ ├── react-0.14.0.js │ │ ├── react-dom-0.14.0.js │ │ ├── react-dom.js │ │ ├── react-with-addons.js │ │ ├── react.js │ │ └── zh-cn.js ├── templetes │ ├── delete.html │ ├── edit.html │ ├── imports.html │ ├── index.html │ └── login.html └── timezone ├── docs ├── blog │ ├── __init__.py │ ├── article.py │ └── welcome.py └── 我的博客API文档 │ ├── article │ ├── 文章相关.md │ ├── 文章相关.md.html │ ├── 文章相关的api.md │ └── 文章相关的api.md.html │ ├── github-markdown.css │ ├── index.md │ ├── index.md.html │ └── welcome │ ├── 欢迎页.md │ └── 欢迎页.md.html ├── gulpfile.js ├── package.json ├── requirements.txt ├── resources ├── 1.jpg ├── 10.png ├── 2.jpg ├── 3.jpg ├── 4.jpg ├── 5.jpg ├── 6.jpg ├── 7.jpg ├── 8.jpg ├── 8.png └── 9.png ├── setup.cfg.tpl ├── setup.py ├── src └── blog │ ├── __init__.py │ ├── blog.sql │ ├── blog │ ├── __init__.py │ ├── article │ │ ├── __init__.py │ │ ├── article.py │ │ ├── article_exporter.py │ │ ├── controller.py │ │ ├── format.py │ │ └── service.py │ ├── exchange │ │ ├── __init__.py │ │ └── solo.py │ ├── import_ │ │ └── __init__.py │ ├── lib │ │ └── __init__.py │ ├── utils.py │ └── welcome │ │ ├── __init__.py │ │ └── controller.py │ ├── cut_html.js │ ├── db │ └── blog │ ├── settings.py │ ├── solo_app.py │ ├── static │ ├── css │ │ ├── bootstrap.min.css │ │ ├── font-awesome │ │ │ ├── css │ │ │ │ ├── font-awesome.css │ │ │ │ └── font-awesome.min.css │ │ │ └── fonts │ │ │ │ ├── FontAwesome.otf │ │ │ │ ├── fontawesome-webfont.eot │ │ │ │ ├── fontawesome-webfont.svg │ │ │ │ ├── fontawesome-webfont.ttf │ │ │ │ ├── fontawesome-webfont.woff │ │ │ │ └── fontawesome-webfont.woff2 │ │ ├── github.css │ │ ├── pdf.css │ │ └── style.css │ ├── editor │ │ ├── css │ │ │ ├── iconfont.5ce067d.eot │ │ │ ├── iconfont.7d3d0c4.ttf │ │ │ ├── iconfont.9fadafc.svg │ │ │ └── iconfont.b300b13.woff │ │ ├── index.css │ │ ├── index.js │ │ ├── preview.css │ │ └── preview.js │ ├── img │ │ ├── favicon.ico │ │ ├── logo.png │ │ └── pretty │ │ │ └── 0.jpg │ └── js │ │ ├── JSXTransformer.js │ │ ├── bootstrap.min.js │ │ ├── browser.min.js │ │ ├── elasticsearch.jquery.min.js │ │ ├── jqpage.js │ │ ├── jquery-1.12.4.min.js │ │ ├── modernizr.js │ │ ├── moment.min.js │ │ ├── react-0.13.4.js │ │ ├── react-0.14.0.js │ │ ├── react-dom-0.14.0.js │ │ ├── react-dom.js │ │ ├── react-with-addons.js │ │ ├── react.js │ │ └── zh-cn.js │ ├── tasks.py │ ├── templates │ ├── comments.html │ ├── delete.html │ ├── edit.html │ ├── import.html │ ├── index.html │ └── login.html │ └── web_app.py ├── tests ├── conftest.py ├── factories.py ├── test_article │ ├── test_article.py │ ├── test_article_exporter.py │ └── test_service.py ├── test_data │ └── 一键生成API文档.md ├── test_import │ └── test_import.py └── test_utils.py └── tools ├── auto_commit.py ├── build.sh ├── change_profile.py ├── cut.js ├── ip_change.py ├── one-key-ikev2.sh └── stop.py /.coveragerc: -------------------------------------------------------------------------------- 1 | # .coveragerc to control coverage.py 2 | [run] 3 | branch = True 4 | omit = 5 | *tests* 6 | *controller* 7 | 8 | [report] 9 | # Regexes for lines to exclude from consideration 10 | exclude_lines = 11 | # Have to re-enable the standard pragma 12 | pragma: no cover 13 | 14 | # Don't complain about missing debug-only code: 15 | # if self\.debug 16 | 17 | # Don't complain if tests don't hit defensive assertion code: 18 | raise AssertionError 19 | raise NotImplementedError 20 | return NotImplemented 21 | 22 | # Don't complain if non-runnable code isn't run: 23 | if 0: 24 | if __name__ == .__main__.: 25 | 26 | ignore_errors = True 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | *.pyc 3 | *egg-info 4 | dist/ 5 | .pytest_cache 6 | .eggs/ 7 | .idea/ 8 | htmlcov/ 9 | *.log 10 | *.logs 11 | *lock.json 12 | node_modules/ 13 | setup.cfg 14 | code 15 | temp -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | recursive-include blog/templates * -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | pytest = "*" 8 | pytest-cov = "*" 9 | pytest-asyncio = "*" 10 | aiohttp = "*" 11 | pytest-apistellar = ">=0.1.10" 12 | 13 | [packages] 14 | pytz = "*" 15 | html2text = "*" 16 | aiofiles = "*" 17 | Markdown = "*" 18 | WeasyPrint = "*" 19 | apistellar = "~=1.3.0" 20 | 21 | [requires] 22 | python_version = "3.6" 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 基于apistellar的个人博客 2 | 3 | ## Badge 4 | 5 | ### GitHub 6 | 7 | [![GitHub followers](https://img.shields.io/github/followers/shichaoma.svg?label=github%20follow)](https://github.com/shichao.ma) 8 | [![GitHub repo size in bytes](https://img.shields.io/github/repo-size/shichaoma/blog.svg)](https://github.com/shichaoma/blog) 9 | [![GitHub stars](https://img.shields.io/github/stars/shichaoma/blog.svg?label=github%20stars)](https://github.com/shichaoma/blog) 10 | [![GitHub release](https://img.shields.io/github/release/shichaoma/blog.svg)](https://github.com/shichaoma/blog/releases) 11 | [![Github commits (since latest release)](https://img.shields.io/github/commits-since/shichaoma/blog/latest.svg)](https://github.com/shichaoma/blog) 12 | 13 | [![Github All Releases](https://img.shields.io/github/downloads/shichaoma/blog/total.svg)](https://github.com/shichaoma/blog/releases) 14 | [![GitHub Release Date](https://img.shields.io/github/release-date/shichaoma/blog.svg)](https://github.com/shichaoma/blog/releases) 15 | 16 | ## Desc 17 | 博文支持markdown格式的上传和编辑,支持全文搜索。业务代码单元测试代码覆盖率100%。 18 | 19 | 通过环境变量来指定相关配置信息 20 | 21 | - AUTHOR: 这个博客的所有者, eg: 夏洛之枫(默认)。博客名就会变成 夏洛之枫的个人博客,默认导入的博文作者也会是 夏洛之枫 22 | - USERNAME: 登陆用户名。eg: test(默认)。个人博客只支持单用户登录以对博文进行上传,修改和删除。 23 | - PASSWORD: 登陆密码。eg: 12345(默认)。 24 | 25 | 其它配置信息参见[settings.py](https://github.com/ShichaoMa/blog/edit/master/settings.py) 26 | ## Start 27 | ``` 28 | # 自行安装python3.6+ 29 | git clone https://github.com/ShichaoMa/blog.git 30 | cd blog 31 | pip install -e . 32 | uvicorn blog.web_app:app --log-level debug 33 | 34 | ``` 35 | 36 | ### 首页 37 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/1.jpg) 38 | ### 文章正文 39 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/2.jpg) 40 | ### 全部文章 41 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/3.jpg) 42 | ### 登录 43 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/4.jpg) 44 | ### 上传文章 45 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/5.jpg) 46 | ### 个人介绍 47 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/6.jpg) 48 | ### 联系方式 49 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/7.jpg) 50 | ### 修改文章 51 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/10.png) 52 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/8.png) 53 | ### 新增少量实时修改功能 54 | ![](https://github.com/ShichaoMa/blog/blob/master/resources/9.png) -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/README.rst -------------------------------------------------------------------------------- /blog.json: -------------------------------------------------------------------------------- 1 | { 2 | "mappings": { 3 | "articles": { 4 | "properties": { 5 | "id": { 6 | "type": "keyword" 7 | }, 8 | "tags": { 9 | "type": "text" 10 | }, 11 | "description": { 12 | "type": "text" 13 | }, 14 | "title": { 15 | "type": "text" 16 | }, 17 | "article": { 18 | "type": "text" 19 | }, 20 | "author": { 21 | "type": "keyword" 22 | }, 23 | "created_at": { 24 | "type": "date" 25 | }, 26 | "updated_at": { 27 | "type": "date" 28 | }, 29 | "feature": { 30 | "type": "boolean" 31 | }, 32 | "show": { 33 | "type": "integer" 34 | } 35 | } 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /blog.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | # 描述 3 | Description=blog server 4 | # 在哪个程序之后启动 5 | After=network.target 6 | # 依赖哪个程序 7 | Wants=network.target 8 | [Service] 9 | Environment=PASSWORD=12345 10 | Environment=USERNAME=test 11 | Type=simple 12 | # 执行命令 13 | ExecStart=/root/.pyenv/shims/uvicorn blog.web_app:app --log-level debug --host 0.0.0.0 --port 80 14 | StandardOutput=syslog 15 | StandardError=syslog 16 | [Install] 17 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /docker/Python-3.6.0.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/Python-3.6.0.tgz -------------------------------------------------------------------------------- /docker/Shanghai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/Shanghai -------------------------------------------------------------------------------- /docker/blog.df: -------------------------------------------------------------------------------- 1 | FROM ubuntu 2 | MAINTAINER Shichao Ma 3 | COPY Python-3.6.0.tgz / 4 | RUN apt-get update 5 | RUN apt-get install -y locales 6 | RUN locale-gen en_US.UTF-8 7 | RUN update-locale LANG=en_US.UTF-8 8 | ENV LANG en_US.UTF-8 9 | COPY Shanghai /etc/localtime 10 | COPY timezone /etc/timezone 11 | RUN apt-get install -y --no-install-recommends libc6-dev gcc make ca-certificates 12 | RUN apt-get install -y --no-install-recommends build-essential \ 13 | libssl-dev zlib1g-dev libbz2-dev libreadline-dev libsqlite3-dev libffi-dev 14 | # 安装python3.6 15 | RUN tar zxvf Python-3.6.0.tgz 16 | RUN cd Python-3.6.0 && ./configure --bindir=/bin/ 17 | RUN cd Python-3.6.0 && make && make install 18 | RUN apt-get install -y --no-install-recommends libcairo2-dev pango1.0-tests 19 | RUN apt-get install -y --no-install-recommends xfonts-intl-chinese xfonts-wqy ttf-wqy-zenhei 20 | RUN apt-get install -y --no-install-recommends ttf-wqy-microhei xfonts-intl-chinese-big 21 | COPY requirements.txt /tmp/ 22 | RUN mkdir -p /app/articles 23 | RUN pip3.6 install -r /tmp/requirements.txt 24 | COPY static /app/static 25 | COPY start.py /app/start.py 26 | COPY settings.py /app/settings.py 27 | COPY blog.json /app/blog.json 28 | COPY blog.sql /app/blog.sql 29 | COPY templetes /app/templetes 30 | COPY blog /app/blog 31 | WORKDIR /app 32 | -------------------------------------------------------------------------------- /docker/deploy.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import sys 4 | 5 | os.system("rm -rf templetes") 6 | os.system("rm -rf static") 7 | os.system("rm -rf blog") 8 | os.system("cp -r ../templetes ./templetes") 9 | os.system("cp -r ../static ./static") 10 | os.system("cp ../start.py start.py") 11 | os.system("cp ../blog.sql blog.sql") 12 | os.system("cp ../blog.json blog.json") 13 | os.system("cp -r ../blog blog") 14 | os.system("cp ../settings.py settings.py") 15 | os.system("sudo docker build -f blog.df -t cnaafhvk/blog:latest .") 16 | os.system("sudo docker push cnaafhvk/blog") 17 | -------------------------------------------------------------------------------- /docker/phantomjs-2.1.1-linux-x86_64.tar.bz2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/phantomjs-2.1.1-linux-x86_64.tar.bz2 -------------------------------------------------------------------------------- /docker/requirements.txt: -------------------------------------------------------------------------------- 1 | elasticsearch 2 | flask 3 | markdown 4 | pytz 5 | html2text 6 | weasyprint -------------------------------------------------------------------------------- /docker/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | 4 | STATIC_FOLDER = os.environ.get("STATIC_FOLDER", "static") 5 | 6 | STATIC_URL_PATH = os.environ.get("STATIC_URL_PATH", "/static") 7 | 8 | TEMPLATE_FOLDER = os.environ.get("TEMPLATE_FOLDER", "templetes") 9 | 10 | ES_HOST = os.environ.get("ES_HOST", "127.0.0.1:9200") 11 | 12 | ES_USERNAME = os.environ.get("ES_USER", "elastic") 13 | 14 | ES_PASSWORD = os.environ.get("ES_PASSWORD", "changeme") 15 | 16 | INDEX = os.environ.get("INDEX", "blog") 17 | 18 | DOC_TYPE = os.environ.get("DOC_TYPE", "articles") 19 | 20 | USERNAME = os.environ.get("USERNAME", "test") 21 | 22 | PASSWORD = os.environ.get("PASSWORD", "12345") 23 | 24 | SECRET_KEY = os.urandom(24) 25 | 26 | TIME_ZONE = os.environ.get("TIME_ZONE", 'Asia/Shanghai') 27 | 28 | AUTHOR = os.environ.get("AUTHOR", "夏洛之枫") 29 | 30 | DB = os.environ.get("DB", "sqlite") 31 | -------------------------------------------------------------------------------- /docker/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | nohup /kibana-5.0.1-linux-x86_64/bin/kibana & 4 | ./start.py -------------------------------------------------------------------------------- /docker/static/css/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/css/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /docker/static/css/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/css/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /docker/static/css/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/css/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /docker/static/css/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/css/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /docker/static/css/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/css/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /docker/static/img/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/img/avatar.png -------------------------------------------------------------------------------- /docker/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/img/logo.png -------------------------------------------------------------------------------- /docker/static/img/single-post.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/docker/static/img/single-post.jpg -------------------------------------------------------------------------------- /docker/static/js/modernizr.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.7.1 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-inlinesvg-shiv-cssclasses-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes 3 | */ 4 | ;window.Modernizr=function(a,b,c){function B(a){j.cssText=a}function C(a,b){return B(m.join(a+";")+(b||""))}function D(a,b){return typeof a===b}function E(a,b){return!!~(""+a).indexOf(b)}function F(a,b){for(var d in a){var e=a[d];if(!E(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function G(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:D(f,"function")?f.bind(d||b):f}return!1}function H(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+o.join(d+" ")+d).split(" ");return D(b,"string")||D(b,"undefined")?F(e,b):(e=(a+" "+p.join(d+" ")+d).split(" "),G(e,b,c))}var d="2.7.1",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n="Webkit Moz O ms",o=n.split(" "),p=n.toLowerCase().split(" "),q={svg:"http://www.w3.org/2000/svg"},r={},s={},t={},u=[],v=u.slice,w,x=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},y=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=D(e[d],"function"),D(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),z={}.hasOwnProperty,A;!D(z,"undefined")&&!D(z.call,"undefined")?A=function(a,b){return z.call(a,b)}:A=function(a,b){return b in a&&D(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=v.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(v.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(v.call(arguments)))};return e}),r.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==q.svg};for(var I in r)A(r,I)&&(w=I.toLowerCase(),e[w]=r[I](),u.push((e[w]?"":"no-")+w));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)A(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},B(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=m,e._domPrefixes=p,e._cssomPrefixes=o,e.hasEvent=y,e.testProp=function(a){return F([a])},e.testAllProps=H,e.testStyles=x,e.prefixed=function(a,b,c){return b?H(a,b,c):H(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+u.join(" "):""),e}(this,this.document); -------------------------------------------------------------------------------- /docker/static/js/react-dom-0.14.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOM v0.14.0 3 | * 4 | * Copyright 2013-2015, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | // Based off https://github.com/ForbesLindesay/umd/blob/master/template.js 13 | ;(function(f) { 14 | // CommonJS 15 | if (typeof exports === "object" && typeof module !== "undefined") { 16 | module.exports = f(require('react')); 17 | 18 | // RequireJS 19 | } else if (typeof define === "function" && define.amd) { 20 | define(['react'], f); 21 | 22 | // -------------------------------------------------------------------------------- /docker/templetes/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 更新 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 56 |
57 | 58 | 70 | -------------------------------------------------------------------------------- /docker/templetes/imports.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 导入 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 52 |
53 | 54 | -------------------------------------------------------------------------------- /docker/templetes/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 登陆 4 | 5 | 6 | 7 |
8 |

这里不是你该来的地方

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /docker/timezone: -------------------------------------------------------------------------------- 1 | Asia/Shanghai 2 | -------------------------------------------------------------------------------- /docs/blog/__init__.py: -------------------------------------------------------------------------------- 1 | # blog API 2 | from .welcome import Welcome 3 | from .article import Article 4 | 5 | 6 | class Blog(Welcome, Article): 7 | pass -------------------------------------------------------------------------------- /docs/blog/article.py: -------------------------------------------------------------------------------- 1 | # 文章相关 PRC调用 2 | import typing 3 | 4 | from apistellar.helper import register 5 | from aiohttp import ClientSession, FormData 6 | from apistellar.helper import RestfulApi 7 | 8 | 9 | class Article(RestfulApi): 10 | # 这个url会被连接上域名和注册的endpoint之后注入到方法中使用。 11 | url = None # type: str 12 | session = None # type: ClientSession 13 | 14 | @register("/import", conn_timeout=10) 15 | async def import_(self, cookies: dict=None): 16 | resp = await self.session.get(self.url) 17 | return await resp.read() 18 | 19 | @register("/check", conn_timeout=10) 20 | async def check(self, form_fields: typing.List[dict], cookies: dict=None): 21 | data = FormData() 22 | for meta in form_fields: 23 | data.add_field(meta["name"], 24 | meta["value"], 25 | filename=meta.get("filename"), 26 | content_type=meta.get("content_type")) 27 | resp = await self.session.post(self.url, data=data) 28 | return await resp.read() 29 | 30 | @register("/load", "data", error_check=lambda x: x["code"] != 0, conn_timeout=10) 31 | async def load(self, username: str, password: str, cookies: dict=None): 32 | params = dict() 33 | if username is not None: 34 | params["username"] = username 35 | if password is not None: 36 | params["password"] = password 37 | resp = await self.session.get(self.url, params=params) 38 | return await resp.json() 39 | 40 | @register("/upload", conn_timeout=10) 41 | async def upload(self, form_fields: typing.List[dict], cookies: dict=None): 42 | data = FormData() 43 | for meta in form_fields: 44 | data.add_field(meta["name"], 45 | meta["value"], 46 | filename=meta.get("filename"), 47 | content_type=meta.get("content_type")) 48 | resp = await self.session.post(self.url, data=data) 49 | return await resp.read() 50 | 51 | @register("/export", conn_timeout=10) 52 | async def export(self, ids: str, code: str=None, cookies: dict=None): 53 | params = dict() 54 | if ids is not None: 55 | params["ids"] = ids 56 | if code is not None: 57 | params["code"] = code 58 | resp = await self.session.get(self.url, params=params) 59 | return await resp.read() 60 | 61 | @register("/modify", "data", error_check=lambda x: x["code"] != 0, conn_timeout=10) 62 | async def modify(self, form_fields: typing.List[dict], cookies: dict=None): 63 | data = FormData() 64 | for meta in form_fields: 65 | data.add_field(meta["name"], 66 | meta["value"], 67 | filename=meta.get("filename"), 68 | content_type=meta.get("content_type")) 69 | resp = await self.session.post(self.url, data=data) 70 | return await resp.json() 71 | 72 | @register("/edit", conn_timeout=10) 73 | async def edit(self, id: str, cookies: dict=None): 74 | params = dict() 75 | if id is not None: 76 | params["id"] = id 77 | resp = await self.session.get(self.url, params=params) 78 | return await resp.read() 79 | 80 | @register("/update", conn_timeout=10) 81 | async def update(self, form_fields: typing.List[dict], cookies: dict=None): 82 | data = FormData() 83 | for meta in form_fields: 84 | data.add_field(meta["name"], 85 | meta["value"], 86 | filename=meta.get("filename"), 87 | content_type=meta.get("content_type")) 88 | resp = await self.session.post(self.url, data=data) 89 | return await resp.read() 90 | 91 | @register("/delete", conn_timeout=10) 92 | async def delete(self, id: str, cookies: dict=None): 93 | params = dict() 94 | if id is not None: 95 | params["id"] = id 96 | resp = await self.session.get(self.url, params=params) 97 | return await resp.read() 98 | 99 | @register("/article", conn_timeout=10) 100 | async def article(self, id: str, cookies: dict=None): 101 | params = dict() 102 | if id is not None: 103 | params["id"] = id 104 | resp = await self.session.get(self.url, params=params) 105 | return await resp.json() 106 | 107 | @register("/me", conn_timeout=10) 108 | async def me(self, code: str, cookies: dict=None): 109 | params = dict() 110 | if code is not None: 111 | params["code"] = code 112 | resp = await self.session.get(self.url, params=params) 113 | return await resp.json() 114 | 115 | @register("/contact", conn_timeout=10) 116 | async def contact(self, cookies: dict=None): 117 | resp = await self.session.get(self.url) 118 | return await resp.json() 119 | 120 | @register("/show", conn_timeout=10) 121 | async def show(self, searchField: str=None, fulltext: bool=None, _from: int=None, size: int=None, cookies: dict=None): 122 | params = dict() 123 | if searchField is not None: 124 | params["searchField"] = searchField 125 | if fulltext is not None: 126 | params["fulltext"] = fulltext 127 | if _from is not None: 128 | params["_from"] = _from 129 | if size is not None: 130 | params["size"] = size 131 | resp = await self.session.get(self.url, params=params) 132 | return await resp.json() 133 | 134 | @register("/cut", conn_timeout=10) 135 | async def cut(self, url: str, top: int=None, left: int=None, width: int=None, height: int=None, cookies: dict=None): 136 | params = dict() 137 | if url is not None: 138 | params["url"] = url 139 | if top is not None: 140 | params["top"] = top 141 | if left is not None: 142 | params["left"] = left 143 | if width is not None: 144 | params["width"] = width 145 | if height is not None: 146 | params["height"] = height 147 | resp = await self.session.get(self.url, params=params) 148 | return await resp.read() 149 | 150 | -------------------------------------------------------------------------------- /docs/blog/welcome.py: -------------------------------------------------------------------------------- 1 | # 欢迎页 PRC调用 2 | import typing 3 | 4 | from apistellar.helper import register 5 | from aiohttp import ClientSession, FormData 6 | from apistellar.helper import RestfulApi 7 | 8 | 9 | class Welcome(RestfulApi): 10 | # 这个url会被连接上域名和注册的endpoint之后注入到方法中使用。 11 | url = None # type: str 12 | session = None # type: ClientSession 13 | 14 | @register("/", conn_timeout=10) 15 | async def index(self, path: str=None, cookies: dict=None): 16 | params = dict() 17 | if path is not None: 18 | params["path"] = path 19 | resp = await self.session.get(self.url, params=params) 20 | return await resp.read() 21 | 22 | @register("/upload_image", conn_timeout=10) 23 | async def upload_image(self, form_fields: typing.List[dict], cookies: dict=None): 24 | data = FormData() 25 | for meta in form_fields: 26 | data.add_field(meta["name"], 27 | meta["value"], 28 | filename=meta.get("filename"), 29 | content_type=meta.get("content_type")) 30 | resp = await self.session.post(self.url, data=data) 31 | return await resp.read() 32 | 33 | @register("/a/{b}/{+path}", conn_timeout=10, have_path_param=True) 34 | async def a_b_path(self, path_params: dict, cookies: dict=None): 35 | resp = await self.session.post(self.url) 36 | return await resp.read() 37 | 38 | -------------------------------------------------------------------------------- /docs/我的博客API文档/article/文章相关.md: -------------------------------------------------------------------------------- 1 | # 文章相关 API文档 2 | 3 | ## 模型定义 4 | 5 | ### blog.blog.article.article.Article 6 | 字段名|类型|是否必须|是否可为空值|默认值|描述|示例 7 | :--|:--|:--|:--|:--|:--|:-- 8 | title|String|是|否||标题| 9 | id|String|否|否|20190210021343|每篇文章的唯一id,日期字符串的形式表示| 10 | tags|Tags|是|否||关键字|`["python", "apistellar"]` 11 | description|String|否|否||描述信息| 12 | author|String|否|否|夏洛之枫|作者信息| 13 | feature|Boolean|否|否|False|是否为精品| 14 | created_at|Timestamp|否|否|1549736023.159149|创建时间| 15 | updated_at|Timestamp|否|否|1549736023.160362|更新时间| 16 | show|Boolean|否|否|True|是否在文章列表中展示| 17 | article|String|否|否||文章正文| 18 | 19 | ## 接口定义 20 | 21 | 22 | ### 1. 导入文章 23 | #### URL: /import 24 | #### 方法: GET 25 | 26 | 27 | 28 | #### 返回信息 29 | 30 | 31 | ##### 返回描述 32 | 33 | 34 | 导入文章页面 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ### 2. 检查用户名和密码是否正确 43 | #### URL: /check 44 | #### 方法: POST 45 | 46 | #### 表单参数: 47 | 参数名|类型|是否必须|默认值|描述|示例 48 | :--|:--|:--|:--|:--|:-- 49 | title,id,tags,...|blog.blog.article.article.Article|是|||`{"title": "xxx"}` 50 | username|str|是||用户名|`test` 51 | password|str|是||密码|`12345` 52 | ref|str|是||从哪里跳过来的| 53 | 54 | 55 | 56 | #### 返回信息 57 | 58 | 59 | ##### 返回描述 60 | 61 | 62 | 返回网页 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | ### 3. 检查用户名密码是否正确,返回检查结果 71 | #### URL: /load 72 | #### 方法: GET 73 | 74 | #### 查询参数: 75 | 参数名|类型|是否必须|默认值|描述|示例 76 | :--|:--|:--|:--|:--|:-- 77 | username|str|是||用户名|`test` 78 | password|str|是||密码|`12345` 79 | 80 | 81 | 82 | #### 返回信息 83 | 84 | 85 | 86 | ##### 返回示例 87 | 88 | 89 | ###### 示例1 90 | 91 | ```json 92 | {"code": 0, "data": null} 93 | ``` 94 | 95 | 96 | ###### 示例2 97 | 98 | ```json 99 | {"code": 401, "message": "密码错误"} 100 | ``` 101 | 102 | 103 | 104 | #### 返回响应码 105 | 响应码|描述 106 | :--|:-- 107 | 0|返回成功 108 | 401|密码错误 109 | 110 | 111 | 112 | ### 4. 用于上传文章 113 | #### URL: /upload 114 | #### 方法: POST 115 | 116 | #### 表单参数: 117 | 参数名|类型|是否必须|默认值|描述|示例 118 | :--|:--|:--|:--|:--|:-- 119 | title,id,tags,...|blog.blog.article.article.Article|是||文章相关信息| 120 | 121 | 122 | 123 | #### 返回信息 124 | 125 | 126 | ##### 返回描述 127 | 128 | 129 | 返回上传页面继续上传 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | ### 5. 导出文章 138 | #### URL: /export 139 | #### 方法: GET 140 | 141 | #### 查询参数: 142 | 参数名|类型|是否必须|默认值|描述|示例 143 | :--|:--|:--|:--|:--|:-- 144 | ids|str|是||要导出的文章id,使用,连接成字符串|`20181010111111,20181020111111` 145 | code|str|否|None|后端生成的用于验证的code| 146 | 147 | 148 | 149 | #### 返回信息 150 | 151 | 152 | ##### 返回描述 153 | 154 | 155 | 导出生成的压缩包 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | ### 6. 这个接口用于直接在网页上修改文章内容 164 | #### URL: /modify 165 | #### 方法: POST 166 | 167 | #### 表单参数: 168 | 参数名|类型|是否必须|默认值|描述|示例 169 | :--|:--|:--|:--|:--|:-- 170 | img_url|str|是||首图地址|`http://www.csdn.....jpg` 171 | title,id,tags,...|blog.blog.article.article.Article|是||文章对象| 172 | 173 | 174 | 175 | #### 返回信息 176 | 177 | 178 | 179 | ##### 返回示例 180 | 181 | 182 | ###### 示例1 183 | 184 | ```json 185 | {"code": 0, "data": null} 186 | ``` 187 | 188 | 189 | ###### 示例2 190 | 191 | ```json 192 | {"code": 401, "message": "Login required!"} 193 | ``` 194 | 195 | 196 | 197 | #### 返回响应码 198 | 响应码|描述 199 | :--|:-- 200 | 0|返回成功 201 | 401|Login required! 202 | 203 | 204 | 205 | ### 7. 打开文章编辑页面 206 | #### URL: /edit 207 | #### 方法: GET 208 | 209 | #### 查询参数: 210 | 参数名|类型|是否必须|默认值|描述|示例 211 | :--|:--|:--|:--|:--|:-- 212 | id|str|是||文章的id|`20111111111111` 213 | 214 | 215 | 216 | #### 返回信息 217 | 218 | 219 | ##### 返回描述 220 | 221 | 222 | 如果登录了,跳转到编辑页面,否则,跳转到登录页。 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | ### 8. 编辑之后更新文章内容 231 | #### URL: /update 232 | #### 方法: POST 233 | 234 | #### 表单参数: 235 | 参数名|类型|是否必须|默认值|描述|示例 236 | :--|:--|:--|:--|:--|:-- 237 | title,id,tags,...|blog.blog.article.article.Article|是||文章对象| 238 | 239 | 240 | 241 | #### 返回信息 242 | 243 | 244 | ##### 返回描述 245 | 246 | 247 | 如果登录了,跳转到首页,否则,跳转到登录页 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | ### 9. 删除文章接口 256 | #### URL: /delete 257 | #### 方法: GET 258 | 259 | #### 查询参数: 260 | 参数名|类型|是否必须|默认值|描述|示例 261 | :--|:--|:--|:--|:--|:-- 262 | id|str|是||要删除的文章id|`19911111111111` 263 | 264 | 265 | 266 | #### 返回信息 267 | 268 | 269 | ##### 返回描述 270 | 271 | 272 | 如果登录了,跳转到首页,否则跳转到登录页。 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | ### 10. 获取文章 281 | #### URL: /article 282 | #### 方法: GET 283 | 284 | #### 查询参数: 285 | 参数名|类型|是否必须|默认值|描述|示例 286 | :--|:--|:--|:--|:--|:-- 287 | id|str|是||要获取的文章id| 288 | 289 | 290 | 291 | #### 返回信息 292 | ##### 返回类型 293 | blog.blog.article.article.Article 294 | 295 | 296 | ##### 返回描述 297 | 298 | 299 | 获取到的文章对象 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | ### 11. 获取关于我的文章 308 | #### URL: /me 309 | #### 方法: GET 310 | 311 | #### 查询参数: 312 | 参数名|类型|是否必须|默认值|描述|示例 313 | :--|:--|:--|:--|:--|:-- 314 | code|str|是||后端生成的用于验证的code| 315 | 316 | 317 | 318 | #### 返回信息 319 | ##### 返回类型 320 | blog.blog.article.article.Article 321 | 322 | 323 | ##### 返回描述 324 | 325 | 326 | 获取到的文章对象 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | ### 12. 获取我的联系方式 335 | #### URL: /contact 336 | #### 方法: GET 337 | 338 | 339 | 340 | #### 返回信息 341 | ##### 返回类型 342 | blog.blog.article.article.Article 343 | 344 | 345 | ##### 返回描述 346 | 347 | 348 | 获取到的文章对象 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | ### 13. 首页展示接口 357 | #### URL: /show 358 | #### 方法: GET 359 | 360 | #### 查询参数: 361 | 参数名|类型|是否必须|默认值|描述|示例 362 | :--|:--|:--|:--|:--|:-- 363 | searchField|str|否||搜索关键词|`python` 364 | fulltext|bool|否|True|是否全文搜索|`true/false` 365 | _from|int|否|0|从第几篇文章开始搜|`0` 366 | size|int|否|10|每页大小|`10` 367 | 368 | 369 | 370 | #### 返回信息 371 | ##### 返回类型 372 | dict 373 | 374 | 375 | 376 | ##### 返回示例 377 | 378 | 379 | ```json 380 | { 381 | "count": 10, 382 | "articles": [...], 383 | "feature_articles": [..], 384 | "tags": ["python", "ubuntu"...] 385 | } 386 | ``` 387 | 388 | 389 | 390 | 391 | 392 | 393 | ### 14. 截图api 394 | #### URL: /cut 395 | #### 方法: GET 396 | 397 | #### 查询参数: 398 | 参数名|类型|是否必须|默认值|描述|示例 399 | :--|:--|:--|:--|:--|:-- 400 | url|str|是||要截图的地址| 401 | top|int|否|0|截图区域的top| 402 | left|int|否|0|截图区域的left| 403 | width|int|否|1024|截图区域的width| 404 | height|int|否|768|截图区域的height| 405 | 406 | 407 | 408 | #### 返回信息 409 | 410 | 411 | ##### 返回描述 412 | 413 | 414 | 重定向到截图的静态地址 415 | 416 | 417 | 418 | 419 | 420 | 421 | -------------------------------------------------------------------------------- /docs/我的博客API文档/article/文章相关的api.md: -------------------------------------------------------------------------------- 1 | # 文章相关的api API文档 2 | 3 | ## 模型定义 4 | 5 | ### blog.blog.article.article.Article 6 | 字段名|类型|是否必须|是否可为空值|默认值|描述|示例 7 | :--|:--|:--|:--|:--|:--|:-- 8 | title|String|是|否||标题| 9 | id|String|否|否|20190126144023|每篇文章的唯一id,日期字符串的形式表示| 10 | tags|Tags|是|否||关键字|`["python", "apistellar"]` 11 | description|String|否|否||描述信息| 12 | author|String|否|否|夏洛之枫|作者信息| 13 | feature|Boolean|否|否|False|是否为精品| 14 | created_at|Timestamp|否|否|1548484823.313708|创建时间| 15 | updated_at|Timestamp|否|否|1548484823.313737|更新时间| 16 | show|Boolean|否|否|True|是否在文章列表中展示| 17 | article|String|否|否||文章正文| 18 | 19 | ## 接口定义 20 | 21 | 22 | ### 1. 导入文章 23 | #### URL: /import 24 | #### 方法: GET 25 | 26 | 27 | 28 | #### 返回信息 29 | 30 | 31 | ##### 返回描述 32 | 33 | 34 | 导入文章页面 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ### 2. 检查用户名和密码是否正确 43 | #### URL: /check 44 | #### 方法: POST 45 | 46 | #### 表单参数: 47 | 参数名|类型|是否必须|默认值|描述|示例 48 | :--|:--|:--|:--|:--|:-- 49 | article|blog.blog.article.article.Article|是|||`{"title": "xxx"}` 50 | username|str|是||用户名|`test` 51 | password|str|是||密码|`12345` 52 | ref|str|是||从哪里跳过来的| 53 | 54 | 55 | 56 | #### 返回信息 57 | ##### 返回类型 58 | str 59 | 60 | 61 | ##### 返回描述 62 | 63 | 64 | 返回网页 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ### 3. 检查用户名密码是否正确,返回检查结果 73 | #### URL: /load 74 | #### 方法: GET 75 | 76 | #### 查询参数: 77 | 参数名|类型|是否必须|默认值|描述|示例 78 | :--|:--|:--|:--|:--|:-- 79 | username|str|是||用户名|`test` 80 | password|str|是||密码|`12345` 81 | 82 | 83 | 84 | #### 返回信息 85 | 86 | 87 | 88 | ##### 返回示例 89 | 90 | 91 | ###### 示例1 92 | 93 | ```json 94 | {"code": 0, "data": null} 95 | ``` 96 | 97 | 98 | ###### 示例2 99 | 100 | ```json 101 | {"code": 401, "message": "密码错误"} 102 | ``` 103 | 104 | 105 | 106 | #### 返回响应码 107 | 响应码|描述 108 | :--|:-- 109 | 0|返回成功 110 | 401|密码错误 111 | 112 | 113 | 114 | ### 4. 用于上传文章 115 | #### URL: /upload 116 | #### 方法: POST 117 | 118 | #### json请求体 119 | 120 | ##### 请求描述 121 | 文章相关信息 122 | 123 | ##### 模型类型 124 | blog.blog.article.article.Article 125 | 126 | 127 | 128 | #### 返回信息 129 | 130 | 131 | ##### 返回描述 132 | 133 | 134 | 返回上传页面继续上传 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | ### 5. 导出文章 143 | #### URL: /export 144 | #### 方法: GET 145 | 146 | #### 查询参数: 147 | 参数名|类型|是否必须|默认值|描述|示例 148 | :--|:--|:--|:--|:--|:-- 149 | ids|str|是||要导出的文章id,使用,连接成字符串|`20181010111111,20181020111111` 150 | code|str|否|None|后端生成的用于验证的code| 151 | 152 | 153 | 154 | #### 返回信息 155 | 156 | 157 | ##### 返回描述 158 | 159 | 160 | 导出生成的压缩包 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | ### 6. 这个接口用于直接在网页上修改文章内容 169 | #### URL: /modify 170 | #### 方法: POST 171 | 172 | #### 表单参数: 173 | 参数名|类型|是否必须|默认值|描述|示例 174 | :--|:--|:--|:--|:--|:-- 175 | img_url|str|是||首图地址|`http://www.csdn.....jpg` 176 | article|blog.blog.article.article.Article|是||文章对象| 177 | 178 | 179 | 180 | #### 返回信息 181 | 182 | 183 | 184 | ##### 返回示例 185 | 186 | 187 | ###### 示例1 188 | 189 | ```json 190 | {"code": 0, "data": null} 191 | ``` 192 | 193 | 194 | ###### 示例2 195 | 196 | ```json 197 | {"code": 401, "message": "Login required!"} 198 | ``` 199 | 200 | 201 | 202 | #### 返回响应码 203 | 响应码|描述 204 | :--|:-- 205 | 0|返回成功 206 | 401|Login required! 207 | 208 | 209 | 210 | ### 7. 打开文章编辑页面 211 | #### URL: /edit 212 | #### 方法: GET 213 | 214 | #### 查询参数: 215 | 参数名|类型|是否必须|默认值|描述|示例 216 | :--|:--|:--|:--|:--|:-- 217 | id|str|是||文章的id|`20111111111111` 218 | 219 | 220 | 221 | #### 返回信息 222 | 223 | 224 | ##### 返回描述 225 | 226 | 227 | 如果登录了,跳转到编辑页面,否则,跳转到登录页。 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | ### 8. 编辑之后更新文章内容 236 | #### URL: /update 237 | #### 方法: POST 238 | 239 | #### 表单参数: 240 | 参数名|类型|是否必须|默认值|描述|示例 241 | :--|:--|:--|:--|:--|:-- 242 | article|blog.blog.article.article.Article|是||文章对象| 243 | 244 | 245 | 246 | #### 返回信息 247 | 248 | 249 | ##### 返回描述 250 | 251 | 252 | 如果登录了,跳转到首页,否则,跳转到登录页 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | ### 9. 删除文章接口 261 | #### URL: /delete 262 | #### 方法: GET 263 | 264 | #### 查询参数: 265 | 参数名|类型|是否必须|默认值|描述|示例 266 | :--|:--|:--|:--|:--|:-- 267 | id|str|是||要删除的文章id|`19911111111111` 268 | 269 | 270 | 271 | #### 返回信息 272 | 273 | 274 | ##### 返回描述 275 | 276 | 277 | 如果登录了,跳转到首页,否则跳转到登录页。 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | ### 10. 获取文章 286 | #### URL: /article 287 | #### 方法: GET 288 | 289 | #### 查询参数: 290 | 参数名|类型|是否必须|默认值|描述|示例 291 | :--|:--|:--|:--|:--|:-- 292 | id|str|是||要获取的文章id| 293 | 294 | 295 | 296 | #### 返回信息 297 | ##### 返回类型 298 | blog.blog.article.article.Article 299 | 300 | 301 | ##### 返回描述 302 | 303 | 304 | 获取到的文章对象 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | ### 11. 获取关于我的文章 313 | #### URL: /me 314 | #### 方法: GET 315 | 316 | #### 查询参数: 317 | 参数名|类型|是否必须|默认值|描述|示例 318 | :--|:--|:--|:--|:--|:-- 319 | code|str|是||后端生成的用于验证的code| 320 | 321 | 322 | 323 | #### 返回信息 324 | ##### 返回类型 325 | blog.blog.article.article.Article 326 | 327 | 328 | ##### 返回描述 329 | 330 | 331 | 获取到的文章对象 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | ### 12. 获取我的联系方式 340 | #### URL: /contact 341 | #### 方法: GET 342 | 343 | 344 | 345 | #### 返回信息 346 | ##### 返回类型 347 | blog.blog.article.article.Article 348 | 349 | 350 | ##### 返回描述 351 | 352 | 353 | 获取到的文章对象 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | ### 13. 首页展示接口 362 | #### URL: /show 363 | #### 方法: GET 364 | 365 | #### 查询参数: 366 | 参数名|类型|是否必须|默认值|描述|示例 367 | :--|:--|:--|:--|:--|:-- 368 | searchField|str|否||搜索关键词|`python` 369 | fulltext|bool|否|True|是否全文搜索|`true/false` 370 | _from|int|否|0|从第几篇文章开始搜|`0` 371 | size|int|否|10|每页大小|`10` 372 | 373 | 374 | 375 | #### 返回信息 376 | 377 | 378 | 379 | ##### 返回示例 380 | 381 | 382 | ```json 383 | { 384 | "count": 10, 385 | "articles": [...], 386 | "feature_articles": [..], 387 | "tags": ["python", "ubuntu"...] 388 | } 389 | ``` 390 | 391 | 392 | 393 | 394 | 395 | 396 | ### 14. 截图api 397 | #### URL: /cut 398 | #### 方法: GET 399 | 400 | #### 查询参数: 401 | 参数名|类型|是否必须|默认值|描述|示例 402 | :--|:--|:--|:--|:--|:-- 403 | url|str|是||要截图的地址| 404 | top|int|否|0|截图区域的top| 405 | left|int|否|0|截图区域的left| 406 | width|int|否|1024|截图区域的width| 407 | height|int|否|768|截图区域的height| 408 | 409 | 410 | 411 | #### 返回信息 412 | 413 | 414 | ##### 返回描述 415 | 416 | 417 | 重定向到截图的静态地址 418 | 419 | 420 | 421 | 422 | 423 | 424 | -------------------------------------------------------------------------------- /docs/我的博客API文档/article/文章相关的api.md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 |
21 |

文章相关的api API文档

22 |

模型定义

23 |

blog.blog.article.article.Article

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
字段名类型是否必须是否可为空值默认值描述示例
titleString标题
idString20190126144023每篇文章的唯一id,日期字符串的形式表示
tagsTags关键字["python", "apistellar"]
descriptionString描述信息
authorString夏洛之枫作者信息
featureBooleanFalse是否为精品
created_atTimestamp1548484823.313708创建时间
updated_atTimestamp1548484823.313737更新时间
showBooleanTrue是否在文章列表中展示
articleString文章正文
129 |

接口定义

130 |

1. 导入文章

131 |

URL: /import

132 |

方法: GET

133 |

返回信息

134 |
返回描述
135 |

导入文章页面

136 |

2. 检查用户名和密码是否正确

137 |

URL: /check

138 |

方法: POST

139 |

表单参数:

140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 |
参数名类型是否必须默认值描述示例
articleblog.blog.article.article.Article{"title": "xxx"}
usernamestr用户名test
passwordstr密码12345
refstr从哪里跳过来的
186 |

返回信息

187 |
返回类型
188 |

str

189 |
返回描述
190 |

返回网页

191 |

3. 检查用户名密码是否正确,返回检查结果

192 |

URL: /load

193 |

方法: GET

194 |

查询参数:

195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 |
参数名类型是否必须默认值描述示例
usernamestr用户名test
passwordstr密码12345
225 |

返回信息

226 |
返回示例
227 |
示例1
228 |
        {"code": 0, "data": null}
229 | 
230 | 231 |
示例2
232 |
        {"code": 401, "message": "密码错误"}
233 | 
234 | 235 |

返回响应码

236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 |
响应码描述
0返回成功
401密码错误
254 |

4. 用于上传文章

255 |

URL: /upload

256 |

方法: POST

257 |

json请求体

258 |
请求描述
259 |

文章相关信息

260 |
模型类型
261 |

blog.blog.article.article.Article

262 |

返回信息

263 |
返回描述
264 |

返回上传页面继续上传

265 |

5. 导出文章

266 |

URL: /export

267 |

方法: GET

268 |

查询参数:

269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 |
参数名类型是否必须默认值描述示例
idsstr要导出的文章id,使用,连接成字符串20181010111111,20181020111111
codestrNone后端生成的用于验证的code
299 |

返回信息

300 |
返回描述
301 |

导出生成的压缩包

302 |

6. 这个接口用于直接在网页上修改文章内容

303 |

URL: /modify

304 |

方法: POST

305 |

表单参数:

306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 |
参数名类型是否必须默认值描述示例
img_urlstr首图地址http://www.csdn.....jpg
articleblog.blog.article.article.Article文章对象
336 |

返回信息

337 |
返回示例
338 |
示例1
339 |
        {"code": 0, "data": null}
340 | 
341 | 342 |
示例2
343 |
        {"code": 401, "message": "Login required!"}
344 | 
345 | 346 |

返回响应码

347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 |
响应码描述
0返回成功
401Login required!
365 |

7. 打开文章编辑页面

366 |

URL: /edit

367 |

方法: GET

368 |

查询参数:

369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 |
参数名类型是否必须默认值描述示例
idstr文章的id20111111111111
391 |

返回信息

392 |
返回描述
393 |

如果登录了,跳转到编辑页面,否则,跳转到登录页。

394 |

8. 编辑之后更新文章内容

395 |

URL: /update

396 |

方法: POST

397 |

表单参数:

398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 |
参数名类型是否必须默认值描述示例
articleblog.blog.article.article.Article文章对象
420 |

返回信息

421 |
返回描述
422 |

如果登录了,跳转到首页,否则,跳转到登录页

423 |

9. 删除文章接口

424 |

URL: /delete

425 |

方法: GET

426 |

查询参数:

427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 |
参数名类型是否必须默认值描述示例
idstr要删除的文章id19911111111111
449 |

返回信息

450 |
返回描述
451 |

如果登录了,跳转到首页,否则跳转到登录页。

452 |

10. 获取文章

453 |

URL: /article

454 |

方法: GET

455 |

查询参数:

456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 |
参数名类型是否必须默认值描述示例
idstr要获取的文章id
478 |

返回信息

479 |
返回类型
480 |

blog.blog.article.article.Article

481 |
返回描述
482 |

获取到的文章对象

483 |

11. 获取关于我的文章

484 |

URL: /me

485 |

方法: GET

486 |

查询参数:

487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 |
参数名类型是否必须默认值描述示例
codestr后端生成的用于验证的code
509 |

返回信息

510 |
返回类型
511 |

blog.blog.article.article.Article

512 |
返回描述
513 |

获取到的文章对象

514 |

12. 获取我的联系方式

515 |

URL: /contact

516 |

方法: GET

517 |

返回信息

518 |
返回类型
519 |

blog.blog.article.article.Article

520 |
返回描述
521 |

获取到的文章对象

522 |

13. 首页展示接口

523 |

URL: /show

524 |

方法: GET

525 |

查询参数:

526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 556 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 |
参数名类型是否必须默认值描述示例
searchFieldstr搜索关键词python
fulltextboolTrue是否全文搜索true/false
_fromint0从第几篇文章开始搜0
sizeint10每页大小10
572 |

返回信息

573 |
返回示例
574 |
        {
575 |             "count": 10,
576 |             "articles": [...],
577 |             "feature_articles": [..],
578 |             "tags": ["python", "ubuntu"...]
579 |         }
580 | 
581 | 582 |

14. 截图api

583 |

URL: /cut

584 |

方法: GET

585 |

查询参数:

586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 |
参数名类型是否必须默认值描述示例
urlstr要截图的地址
topint0截图区域的top
leftint0截图区域的left
widthint1024截图区域的width
heightint768截图区域的height
640 |

返回信息

641 |
返回描述
642 |

重定向到截图的静态地址

643 |
644 | 645 | -------------------------------------------------------------------------------- /docs/我的博客API文档/index.md: -------------------------------------------------------------------------------- 1 | # 我的博客API文档 2 | 3 | 1. [欢迎页](welcome/欢迎页.md) 4 | 2. [文章相关](article/文章相关.md) 5 | -------------------------------------------------------------------------------- /docs/我的博客API文档/index.md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 |
21 |

我的博客API文档

22 |
    23 |
  1. 欢迎页
  2. 24 |
  3. 文章相关
  4. 25 |
26 |
27 | 28 | -------------------------------------------------------------------------------- /docs/我的博客API文档/welcome/欢迎页.md: -------------------------------------------------------------------------------- 1 | # 欢迎页 API文档 2 | 3 | ## 模型定义 4 | 5 | ## 接口定义 6 | 7 | 8 | ### 1. 首页 9 | #### URL: / 10 | #### 方法: GET 11 | 12 | #### 查询参数: 13 | 参数名|类型|是否必须|默认值|描述|示例 14 | :--|:--|:--|:--|:--|:-- 15 | path|str|否|None|子路径|`"/article?a=3"` 16 | 17 | 18 | 19 | #### 返回信息 20 | ##### 返回类型 21 | str 22 | 23 | 24 | 25 | ##### 返回示例 26 | 27 | 28 | ```html 29 | ... 30 | ``` 31 | 32 | 33 | 34 | 35 | 36 | 37 | ### 2. 38 | #### URL: /upload_image 39 | #### 方法: POST 40 | 41 | #### 表单参数: 42 | 参数名|类型|是否必须|默认值|描述|示例 43 | :--|:--|:--|:--|:--|:-- 44 | `file1`, `file2`, ...|file|是||| 45 | 46 | 47 | 48 | 49 | 50 | 51 | ### 3. 52 | #### URL: /a/{b}/{+path} 53 | #### 方法: POST 54 | 55 | #### 路径参数: 56 | 参数名|类型|是否必须|默认值|描述|示例 57 | :--|:--|:--|:--|:--|:-- 58 | b|int|是||| 59 | path|path|是||| 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /docs/我的博客API文档/welcome/欢迎页.md.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 20 |
21 |

欢迎页 API文档

22 |

模型定义

23 |

接口定义

24 |

1. 首页

25 |

URL: /

26 |

方法: GET

27 |

查询参数:

28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
参数名类型是否必须默认值描述示例
pathstrNone子路径"/article?a=3"
50 |

返回信息

51 |
返回类型
52 |

str

53 |
返回示例
54 |
        <html>...</html>
 55 | 
56 | 57 |

2.

58 |

URL: /upload_image

59 |

方法: POST

60 |

表单参数:

61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 |
参数名类型是否必须默认值描述示例
file1, file2, ...file
83 |

3.

84 |

URL: /a/{b}/{+path}

85 |

方法: POST

86 |

路径参数:

87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
参数名类型是否必须默认值描述示例
bint
pathpath
117 |
118 | 119 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserSync = require('browser-sync').create(); 3 | var reload = browserSync.reload; 4 | 5 | 6 | gulp.task('default', function () { 7 | browserSync.init({ 8 | browser: ["google chrome"], 9 | server: { 10 | baseDir: "./htmlcov" 11 | } 12 | }); 13 | 14 | gulp.watch('./htmlcov/**/*.{html,markdown,md,yml,json,txt,xml}') 15 | .on('change', reload); 16 | }); 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-dev", 3 | "version": "1.0.0", 4 | "description": "基于Gulp.js的开发调试工具", 5 | "main": "index.js", 6 | "directories": { 7 | "doc": "docs", 8 | "test": "tests" 9 | }, 10 | "dependencies": {}, 11 | "devDependencies": { 12 | "browser-sync": "^2.24.7", 13 | "gulp": "^4.0.0" 14 | }, 15 | "scripts": { 16 | "gulp": "node node_modules/gulp/bin/gulp.js" 17 | }, 18 | "author": "Moore.Huang", 19 | "license": "ISC" 20 | } 21 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiofiles==0.4.0 2 | aiohttp==3.5.4 3 | apistar==0.5.42 4 | apistellar==1.3.0 5 | appnope==0.1.0 ; sys_platform == 'darwin' 6 | async-timeout==3.0.1 7 | attrs==19.1.0 8 | backcall==0.1.0 9 | cairocffi==1.0.2 10 | cairosvg==2.3.1 11 | certifi==2019.3.9 12 | cffi==1.12.2 13 | chardet==3.0.4 14 | click==7.0 15 | cssselect2==0.2.1 16 | decorator==4.4.0 17 | defusedxml==0.6.0 18 | flask==1.0.2 19 | future==0.17.1 20 | gunicorn==19.9.0 21 | h11==0.8.1 22 | html2text==2018.1.9 23 | html5lib==1.0.1 24 | httptools==0.0.13 25 | idna-ssl==1.1.0 ; python_version < '3.7' 26 | idna==2.8 27 | ipdb==0.12 28 | ipython-genutils==0.2.0 29 | ipython==6.4.0 ; python_version >= '3.4' 30 | itsdangerous==1.1.0 31 | jedi==0.13.3 32 | jinja2==2.10.1 33 | kafka-python==1.4.6 34 | markdown==3.1 35 | markupsafe==1.1.1 36 | multidict==4.5.2 37 | parso==0.4.0 38 | pexpect==4.7.0 ; sys_platform != 'win32' 39 | pickleshare==0.7.5 40 | pillow==6.0.0 41 | prompt-toolkit==1.0.16 42 | ptyprocess==0.6.0 43 | pyaop==0.0.7 44 | pycparser==2.19 45 | pygments==2.3.1 46 | pyphen==0.9.5 47 | python-json-logger==0.1.11 48 | pytz==2019.1 49 | pyyaml==5.1 50 | redis==3.2.1 51 | requests==2.21.0 52 | simplegeneric==0.8.1 53 | six==1.12.0 54 | tinycss2==1.0.2 55 | toolkity==1.8.0 56 | traitlets==4.3.2 57 | typing-extensions==3.7.2 ; python_version < '3.7' 58 | urllib3==1.24.2 59 | uvicorn==0.2.2 60 | uvloop==0.12.2 61 | wcwidth==0.1.7 62 | weasyprint==47 63 | webencodings==0.5.1 64 | websockets==3.3 65 | werkzeug==0.15.2 66 | whitenoise==3.3.1 67 | yarl==1.3.0 -------------------------------------------------------------------------------- /resources/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/1.jpg -------------------------------------------------------------------------------- /resources/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/10.png -------------------------------------------------------------------------------- /resources/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/2.jpg -------------------------------------------------------------------------------- /resources/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/3.jpg -------------------------------------------------------------------------------- /resources/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/4.jpg -------------------------------------------------------------------------------- /resources/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/5.jpg -------------------------------------------------------------------------------- /resources/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/6.jpg -------------------------------------------------------------------------------- /resources/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/7.jpg -------------------------------------------------------------------------------- /resources/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/8.jpg -------------------------------------------------------------------------------- /resources/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/8.png -------------------------------------------------------------------------------- /resources/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/resources/9.png -------------------------------------------------------------------------------- /setup.cfg.tpl: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test=pytest 3 | 4 | [tool:pytest] 5 | ;when run pytest command, add --roodir here will be effective, but 6 | ;the printed message of rootdir is only can be changed by command 7 | ;line, so never mind! 8 | addopts = --rootdir=${pwd}/tests --cov-report=html:${pwd}/htmlcov --cov-branch --cov=${pwd}/src/blog/ -vv --disable-warnings 9 | usefixtures = 10 | mock 11 | ;`UNIT_TEST_MODE` used to ignore DriverMixin wrapper conn_manager, 12 | ;for it is a module of apistellar, to test it, UNIT_TEST_MODE need 13 | ;to be false. 14 | env = 15 | UNIT_TEST_MODE=true 16 | PROJECT_PATH=blog.__path__[0] -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import string 4 | 5 | from contextlib import contextmanager 6 | from setuptools import find_packages, setup 7 | 8 | 9 | def get_version(package): 10 | """ 11 | Return package version as listed in `__version__` in `__init__.py`. 12 | """ 13 | init_py = open(os.path.join(package, '__init__.py')).read() 14 | mth = re.search("__version__\s?=\s?['\"]([^'\"]+)['\"]", init_py) 15 | if mth: 16 | return mth.group(1) 17 | else: 18 | raise RuntimeError("Cannot find version!") 19 | 20 | 21 | def install_requires(): 22 | """ 23 | Return requires in requirements.txt 24 | :return: 25 | """ 26 | try: 27 | with open("requirements.txt") as f: 28 | return [line.strip() for line in f.readlines() if line.strip()] 29 | except OSError: 30 | return [] 31 | 32 | 33 | @contextmanager 34 | def cfg_manage(cfg_tpl_filename): 35 | if os.path.exists(cfg_tpl_filename): 36 | cfg_file_tpl = open(cfg_tpl_filename) 37 | buffer = cfg_file_tpl.read() 38 | try: 39 | with open(cfg_tpl_filename.rstrip(".tpl"), "w") as cfg_file: 40 | cfg_file.write(string.Template(buffer).substitute( 41 | pwd=os.path.abspath(os.path.dirname(__file__)))) 42 | yield 43 | finally: 44 | cfg_file_tpl.close() 45 | else: 46 | yield 47 | 48 | 49 | with cfg_manage(__file__.replace(".py", ".cfg.tpl")): 50 | setup( 51 | name="blog", 52 | version=get_version("src/blog"), 53 | packages=find_packages('src'), 54 | package_dir={'': 'src'}, 55 | include_package_data=True, 56 | install_requires=install_requires(), 57 | author="", 58 | author_email="", 59 | description="""package description here""", 60 | keywords="", 61 | setup_requires=["pytest-runner"], 62 | tests_require=["pytest-apistellar", "pytest-asyncio", "pytest-cov"] 63 | ) 64 | -------------------------------------------------------------------------------- /src/blog/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | __version__ = "0.1.0" 3 | -------------------------------------------------------------------------------- /src/blog/blog.sql: -------------------------------------------------------------------------------- 1 | 2 | CREATE TABLE articles( 3 | id CHAR(14) PRIMARY KEY NOT NULL, 4 | description TEXT NOT NULL, 5 | tags TEXT NOT NULL, 6 | article TEXT NOT NULL, 7 | author CHAR(20) NOT NULL, 8 | title CHAR(50) NOT NULL, 9 | feature INT NOT NULL, 10 | created_at INT NOT NULL, 11 | updated_at INT NOT NULL, 12 | show INT NOT NULL 13 | ); 14 | -------------------------------------------------------------------------------- /src/blog/blog/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /src/blog/blog/article/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/blog/article/__init__.py -------------------------------------------------------------------------------- /src/blog/blog/article/article.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | import typing 3 | import logging 4 | import datetime 5 | 6 | from itertools import repeat 7 | from collections import MutableSequence, MutableSet, defaultdict 8 | 9 | from apistellar.types import PersistentType 10 | from apistellar.persistence import conn_ignore 11 | from apistellar import validators, settings 12 | 13 | from blog.blog.lib import SqliteDriverMixin 14 | from blog.blog.utils import code_generator 15 | 16 | from .format import Tags, Timestamp 17 | 18 | 19 | logger = logging.getLogger("article") 20 | 21 | 22 | class Article(PersistentType, SqliteDriverMixin): 23 | """ 24 | 文章模型 25 | :param title: 标题 26 | :ex `我的主页` 27 | :param id: 每篇文章的唯一id,日期字符串的形式表示 28 | :param tags: 关键字 29 | :ex tags: 30 | `["python", "apistellar"]` 31 | :param description: 描述信息 32 | :param author: 作者信息 33 | :param feature: 是否为精品 34 | :ex feature: True/False 35 | :param updated_at: 更新时间 36 | :param created_at: 创建时间 37 | :param show: 是否在文章列表中展示 38 | :ex show: True/False 39 | :param article: 文章正文 40 | """ 41 | TABLE = "articles" 42 | 43 | title = validators.String() 44 | id = validators.String(default=lambda: datetime.datetime.now( 45 | pytz.timezone(settings["TIME_ZONE"])).strftime("%Y%m%d%H%M%S")) 46 | tags = Tags() 47 | description = validators.String(default="") 48 | author = validators.String(default=settings.get("AUTHOR")) 49 | feature = validators.Boolean(default=False) 50 | created_at = Timestamp(default=lambda: datetime.datetime.now().timestamp()) 51 | updated_at = Timestamp(default=lambda: datetime.datetime.now().timestamp()) 52 | show = validators.Boolean(default=True) 53 | article = validators.String(default=str) 54 | 55 | @classmethod 56 | def right_code(cls, code, code_gen=None): 57 | """ 58 | code是否正确 59 | :param code: 60 | :param code_gen: 61 | :return: 62 | """ 63 | if code_gen is None: 64 | code_gen = code_generator( 65 | settings.get_int("CODE_EXPIRE_INTERVAL")) 66 | 67 | if settings.get_bool("NEED_CODE"): 68 | return next(code_gen) == code 69 | else: 70 | return True 71 | 72 | @classmethod 73 | async def load(cls, **kwargs): 74 | """ 75 | 查找一篇文章 76 | :param kwargs: 77 | :return: 78 | """ 79 | vals, sql = cls._build_select_sql(kwargs) 80 | cls.store.execute(sql, vals) 81 | data = cls.store.fetchone() 82 | 83 | if data: 84 | return cls(dict(zip( 85 | (col[0] for col in cls.store.description), data))) 86 | else: 87 | return Article() 88 | 89 | @classmethod 90 | async def load_list(cls, _from=None, size=None, sub=None, 91 | vals=None, projection=None, **kwargs): 92 | """ 93 | 查询文章列表 94 | :param _from: 95 | :param size: 96 | :param sub: 97 | :param vals: 98 | :param kwargs: 99 | :param projection: 100 | :return: 101 | """ 102 | vals, sql = cls._build_select_sql( 103 | kwargs, _from, size, projection, [("updated_at", "desc")], sub, vals) 104 | cls.store.execute(sql, vals) 105 | data_list = cls.store.fetchall() 106 | desc = [col[0] for col in cls.store.description] 107 | return [Article(dict(zip(desc, data))) for data in data_list] 108 | 109 | @classmethod 110 | async def get_total_tags(cls): 111 | """ 112 | 获取全部文章数量及每个tag的数量 113 | :return: 114 | """ 115 | count = 0 116 | group_tags = defaultdict(int) 117 | for article in await cls.load_list(projection=["tags"], show=True): 118 | for tag in article.tags.split(","): 119 | group_tags[tag] += 1 120 | count += 1 121 | return count, group_tags 122 | 123 | async def save(self): 124 | """ 125 | 保存文章 126 | :return: 127 | """ 128 | self.format(allow_coerce=True) 129 | self.store.execute( 130 | f"INSERT INTO {self.TABLE} VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", 131 | (self.id, 132 | self.description, 133 | # 通过下标获取,可以调用其formatter的to_string方法返回 134 | self["tags"], 135 | self.article, 136 | self.author, 137 | self.title, 138 | self.feature, 139 | self.created_at, 140 | self.updated_at, 141 | self.show)) 142 | 143 | async def remove(self): 144 | """ 145 | 删除文章 146 | :return: 147 | """ 148 | self.store.execute( 149 | f"DELETE FROM {self.TABLE} WHERE show=1 and id=?;", (self.id, )) 150 | 151 | async def update(self): 152 | """ 153 | 更新文章 154 | :return: 155 | """ 156 | self.store.execute(*self._build_update_sql(self)) 157 | 158 | @classmethod 159 | def _build_update_sql(cls, article): 160 | sql = f"UPDATE {cls.TABLE} SET " 161 | args = list() 162 | for name in article: 163 | # 在调用update之前使用了format,目前format会创建字符串时间 164 | # 无法使用,在这里进行一次排除 165 | if name not in ("id", "updated_at", "created_at", "show"): 166 | article.reformat(name, allow_coerce=True) 167 | sql += f"{name}=?, " 168 | args.append(article[name]) 169 | 170 | sql += f"updated_at=? WHERE id=?;" 171 | args.append(int(datetime.datetime.now().timestamp())) 172 | args.append(article.id) 173 | return sql, args 174 | 175 | @classmethod 176 | @conn_ignore 177 | async def search(cls, search_field, _from, size, fulltext=False, **kwargs): 178 | """ 179 | 搜索文章 180 | :param search_field: 181 | :param _from: 182 | :param size: 183 | :param fulltext: 184 | :param kwargs: 185 | :return: 186 | """ 187 | sub, vals = cls._fuzzy_search_sub_sql(search_field, fulltext) 188 | return await cls.load_list( 189 | _from=_from, size=size, sub=sub, vals=vals, **kwargs) 190 | 191 | @staticmethod 192 | def _fuzzy_search_sub_sql(search_field, fulltext): 193 | """ 194 | 获取模糊搜索where子句 195 | :param search_field: 搜索词 196 | :param fulltext: 是否是全文搜索,如果是的话,则从article和tags中搜索 197 | :return: 198 | """ 199 | sub, vals = None, None 200 | if search_field: 201 | vals = list() 202 | if fulltext: 203 | sub = f" AND (article LIKE ? OR tags LIKE ?)" 204 | vals.append(f"%{search_field}%") 205 | else: 206 | sub = " AND tags LIKE ?" 207 | vals.append(f"%{search_field}%") 208 | return sub, vals 209 | 210 | @classmethod 211 | def _build_select_sql( 212 | cls, 213 | kwargs: dict=None, 214 | _from: int=None, 215 | size: int=None, 216 | projection: list=None, 217 | order_fields: typing.List[typing.Tuple[str, str]]=None, 218 | sub: str=None, 219 | vals: list=None): 220 | """ 221 | 创建查询sql 222 | :param kwargs: 查询参数 223 | :param _from: 从哪一个开始查 224 | :param size: 查几个 225 | :param projection: 需要查哪 226 | :param order_fields: 按哪个字段排序 227 | :ex order_fields: `[("id", "desc)..]` 228 | :param sub: where子句 229 | :param vals: 占位符实参 230 | :return: 231 | """ 232 | if vals is None: 233 | vals = list() 234 | 235 | if sub is None: 236 | sub = "" 237 | 238 | for k, v in (kwargs or dict()).items(): 239 | if isinstance(v, (MutableSequence, MutableSet, tuple)): 240 | sub += f' AND {k} IN ({", ".join(repeat("?", len(v)))})' 241 | vals.extend(v) 242 | else: 243 | sub += " AND {}=?".format(k) 244 | vals.append(v) 245 | 246 | if order_fields: 247 | sub += f" order by {''.join(f + ' ' + o for f, o in order_fields)}" 248 | 249 | if size is not None: 250 | assert _from is not None, "Both of size and _from cannot be None!" 251 | sub += f" limit {size} offset {_from}" 252 | 253 | if projection: 254 | fields = ", ".join(projection) 255 | else: 256 | fields = "*" 257 | return vals, f"SELECT {fields} FROM {cls.TABLE} WHERE 1=1{sub};" 258 | -------------------------------------------------------------------------------- /src/blog/blog/article/article_exporter.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import asyncio 4 | import markdown 5 | 6 | from functools import partial 7 | from urllib.parse import urljoin 8 | from collections import namedtuple 9 | from apistellar import settings 10 | 11 | 12 | ArticleFile = namedtuple("ArticleFile", "filename,buffer") 13 | 14 | 15 | class ArticleExporter(object): 16 | __slots__ = ("article", "code", "url") 17 | desc_fields = ("tags", "description", "title", "author") 18 | 19 | def __init__(self, article, code, url): 20 | self.article = article 21 | self.code = code 22 | self.url = url 23 | 24 | async def export_pdf(self, content): 25 | from html.parser import unescape 26 | html = unescape(markdown.markdown( 27 | content, extensions=['markdown.extensions.extra'])) 28 | return await self._get_pdf_buffer( 29 | f'
{self._replace_url(html)}
') 30 | 31 | async def export_me(self): 32 | """ 33 | 导出我的简历 34 | :return: 35 | """ 36 | assert self.article.right_code(self.code), f"Invalid code: {self.code}" 37 | buffer = await self.export_pdf(self.article.article) 38 | return ArticleFile(f"{self.article.title}.pdf", buffer) 39 | 40 | @classmethod 41 | async def save_as_pdf(cls, content, path): 42 | """ 43 | 将网页转换pdf 44 | :param html: 45 | :return: 46 | """ 47 | with open(path, "wb") as f: 48 | f.write(await cls._get_pdf_buffer(content)) 49 | 50 | @staticmethod 51 | async def _get_pdf_buffer(html): 52 | from weasyprint import HTML 53 | 54 | loop = asyncio.get_event_loop() 55 | return await loop.run_in_executor( 56 | None, partial(HTML(string=html).write_pdf, stylesheets=[ 57 | os.path.join(settings["PROJECT_PATH"], "static/css/pdf.css")])) 58 | 59 | async def export_other(self): 60 | """ 61 | 导出其它文章 62 | :return: 63 | """ 64 | buffer = self.article.article 65 | for field in self.desc_fields: 66 | buffer += "\n" 67 | buffer += f"[comment]: <{field}> ({getattr(self.article, field)})" 68 | 69 | return ArticleFile(f"{self.article.title}.md", buffer.encode()) 70 | 71 | async def export(self): 72 | return await (self._choice_function()()) 73 | 74 | def _choice_function(self): 75 | return getattr(self, f"export_{self.article.id}", self.export_other) 76 | 77 | def _replace_url(self, html): 78 | return re.sub(r'(?<=src=")(.+?)(?=")', self._repl, html) 79 | 80 | def _repl(self, mth): 81 | """ 82 | 由于weasyprint有bug会让svg失真,所以将svg的图片截一下。 83 | :param mth: 84 | :return: 85 | """ 86 | url = mth.group(1) 87 | if not url.count("img.shields.io"): 88 | return url 89 | 90 | return urljoin(self.url, "/cut") + f"?width=60&height=20&url={url}" 91 | -------------------------------------------------------------------------------- /src/blog/blog/article/controller.py: -------------------------------------------------------------------------------- 1 | from apistar import http, App 2 | from apistellar.helper import redirect, return_wrapped 3 | from apistellar import Controller, route, get, post, \ 4 | Session, FormParam, require, settings 5 | 6 | from .article import Article 7 | from .service import ArticleService 8 | 9 | 10 | @route("", name="article") 11 | class ArticleController(Controller): 12 | """ 13 | 文章相关 14 | """ 15 | def __init__(self): 16 | # 通过在controller中初始化service会失去service的注入功能, 17 | # service全局唯一,无法随请求的改变而改变。 18 | # 但好处是不用每次请求重新创建service对象了。 19 | # 对于不需要注入属性能service可以使用此方案。 20 | self.service = ArticleService() 21 | 22 | @get("/import") 23 | async def _import(self, app: App, session: Session): 24 | """ 25 | 导入文章 26 | :param app: 27 | :param session: 28 | :return: 导入文章页面 29 | """ 30 | if not session.get("login"): 31 | return app.render_template("login.html", ref="import") 32 | else: 33 | return app.render_template("import.html", success="") 34 | 35 | @post("/check") 36 | async def check(self, 37 | app: App, 38 | article: Article, 39 | username: FormParam, 40 | password: FormParam, 41 | ref: FormParam, 42 | session: Session): 43 | """ 44 | 检查用户名和密码是否正确 45 | :param app: 46 | :param article: 47 | :ex article: 48 | ```json 49 | {"title": "xxx"} 50 | ``` 51 | :type article: form 52 | :param username: 用户名 53 | :ex username: `test` 54 | :param password: 密码 55 | :ex password: `12345` 56 | :param ref: 从哪里跳过来的 57 | :param session: 58 | :return: 返回网页 59 | """ 60 | # article由于没有经过format会带有多余的信息 61 | if username == settings["USERNAME"] and password == settings["PASSWORD"]: 62 | session["login"] = f'{username}:{password}' 63 | if ref == "edit" and hasattr(article, "id"): 64 | article = await Article.load(id=article.id) 65 | if ref: 66 | return app.render_template( 67 | f"{ref}.html", success="", **article.to_dict()) 68 | else: 69 | return redirect(app.reverse_url("view:welcome:index")) 70 | else: 71 | return app.render_template( 72 | "login.html", **article.to_dict()) 73 | 74 | @get('/load') 75 | @return_wrapped(error_info={401: "密码错误"}) 76 | async def load(self, 77 | username: http.QueryParam, 78 | password: http.QueryParam, 79 | session: Session): 80 | """ 81 | 检查用户名密码是否正确,返回检查结果 82 | :param username: 用户名 83 | :ex username: `test` 84 | :param password: 密码 85 | :ex password: `12345` 86 | :param session: 87 | :return: 88 | ```json 89 | {"code": 0, "data": null} 90 | ``` 91 | :return: 92 | ```json 93 | {"code": 401, "message": "密码错误"} 94 | ``` 95 | """ 96 | assert username == settings["USERNAME"] and \ 97 | password == settings["PASSWORD"], (401, "密码错误") 98 | session["login"] = f'{username}:{password}' 99 | 100 | @post("/upload") 101 | async def upload(self, app: App, article: Article, session: Session): 102 | """ 103 | 用于上传文章 104 | :param app: 105 | :param article: 文章相关信息 106 | :type article: form 107 | :param session: 108 | :return: 返回上传页面继续上传 109 | """ 110 | if not session.get("login"): 111 | return app.render_template("login.html", ref="import") 112 | 113 | await self.service.upload(article) 114 | return app.render_template("import.html", success="success") 115 | 116 | @get("/export") 117 | async def export(self, 118 | url: http.URL, 119 | ids: http.QueryParam, 120 | code: http.QueryParam=None): 121 | """ 122 | 导出文章 123 | :param url: 当前访问的url地址 124 | :param code: 后端生成的用于验证的code 125 | :param ids: 要导出的文章id,使用,连接成字符串 126 | :ex ids: `20181010111111,20181020111111` 127 | :return: 导出生成的压缩包 128 | """ 129 | if ids: 130 | ids = ids.split(",") 131 | else: 132 | ids = [] 133 | article_list = await Article.load_list(id=ids) 134 | return await self.service.export(article_list, code, url) 135 | 136 | @post("/modify") 137 | @return_wrapped(error_info={401: "Login required!"}) 138 | @require(Session, judge=lambda x: x.get("login")) 139 | async def modify(self, img_url: FormParam, article: Article): 140 | """ 141 | 这个接口用于直接在网页上修改文章内容 142 | :param img_url: 首图地址 143 | :ex img_url: `http://www.csdn.....jpg` 144 | :param article: 文章对象 145 | :type article: form 146 | :param session: 147 | :return: 148 | ```json 149 | {"code": 0, "data": null} 150 | ``` 151 | :return: 152 | ```json 153 | {"code": 401, "message": "Login required!"} 154 | ``` 155 | """ 156 | return await self.service.modify(article, img_url) 157 | 158 | @get("/edit") 159 | async def edit(self, app: App, id: http.QueryParam, session: Session): 160 | """ 161 | 打开文章编辑页面 162 | :param app: 163 | :param id: 文章的id 164 | :ex id: `20111111111111` 165 | :param session: 166 | :return: 如果登录了,跳转到编辑页面,否则,跳转到登录页。 167 | """ 168 | article = await Article.load(id=id) 169 | if not session.get("login"): 170 | return app.render_template("login.html", ref="edit", **article) 171 | else: 172 | return app.render_template("edit.html", ref="update", **article) 173 | 174 | @post("/update") 175 | async def update(self, app: App, article: Article, session: Session): 176 | """ 177 | 编辑之后更新文章内容 178 | :param app: 179 | :param article: 文章对象 180 | :type article: form 181 | :param session: 182 | :return: 如果登录了,跳转到首页,否则,跳转到登录页 183 | """ 184 | if not session.get("login"): 185 | return app.render_template("login.html", ref="edit", **article) 186 | 187 | await self.service.update(article) 188 | url = f'{app.reverse_url("view:welcome:index")}?path=' \ 189 | f'{app.reverse_url("view:article:article")}?id={article.id}' 190 | return redirect(url) 191 | 192 | @get("/delete") 193 | async def delete(self, app: App, id: http.QueryParam, session: Session): 194 | """ 195 | 删除文章接口 196 | :param app: 197 | :param id: 要删除的文章id 198 | :ex id: `19911111111111` 199 | :param session: 200 | :return: 如果登录了,跳转到首页,否则跳转到登录页。 201 | """ 202 | article = Article(id=id) 203 | if not session.get("login"): 204 | return app.render_template( 205 | "login.html", ref="delete", id=article.id) 206 | 207 | await self.service.delete(article) 208 | return redirect(app.reverse_url("view:welcome:index")) 209 | 210 | @get("/article", name="article") 211 | async def get_article(self, id: http.QueryParam) -> Article: 212 | """ 213 | 获取文章 214 | :param id: 要获取的文章id 215 | :return: 获取到的文章对象 216 | """ 217 | return await self.service.get(id) 218 | 219 | @get("/me") 220 | async def me(self, code: http.QueryParam) -> Article: 221 | """ 222 | 获取关于我的文章 223 | :param code: 后端生成的用于验证的code 224 | :return: 获取到的文章对象 225 | """ 226 | assert Article.right_code(code), f"Invalid code: {code}" 227 | return await self.service.about("me") 228 | 229 | @get("/contact") 230 | async def contact(self) -> Article: 231 | """ 232 | 获取我的联系方式 233 | :param code: 后端生成的用于验证的code 234 | :return: 获取到的文章对象 235 | """ 236 | return await self.service.about("contact") 237 | 238 | @get("/show") 239 | async def show(self, 240 | searchField: http.QueryParam="", 241 | fulltext: bool=True, 242 | _from: int=0, 243 | size: int=10) -> dict: 244 | """ 245 | 首页展示接口 246 | :param searchField: 搜索关键词 247 | :ex searchField: `python` 248 | :param fulltext: 是否全文搜索 249 | :ex fulltext: `true/false` 250 | :param _from: 从第几篇文章开始搜 251 | :ex _from: `0` 252 | :param size: 每页大小 253 | :ex size: `10` 254 | :return: 255 | ```json 256 | { 257 | "count": 10, 258 | "articles": [...], 259 | "feature_articles": [..], 260 | "tags": ["python", "ubuntu"...] 261 | } 262 | ``` 263 | """ 264 | return await self.service.show(searchField, _from, size, fulltext) 265 | 266 | @get("/cut") 267 | async def cut(self, 268 | url: http.QueryParam, 269 | top: int=0, 270 | left: int=0, 271 | width: int=1024, 272 | height: int= 768): 273 | """ 274 | 截图api 275 | :param url: 要截图的地址 276 | :param top: 截图区域的top 277 | :param left: 截图区域的left 278 | :param width: 截图区域的width 279 | :param height: 截图区域的height 280 | :return: 重定向到截图的静态地址 281 | """ 282 | save_name = await self.service.cut(url, top, left, width, height) 283 | return redirect(save_name.replace(settings["PROJECT_PATH"], "")) 284 | -------------------------------------------------------------------------------- /src/blog/blog/article/format.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | import numbers 3 | import warnings 4 | import datetime 5 | 6 | from collections import Sequence 7 | from apistellar import validators, settings 8 | from apistellar.types.formats import BaseFormat, DATETIME_REGEX, ValidationError 9 | 10 | 11 | class TsFormat(BaseFormat): 12 | 13 | type = numbers.Number 14 | name = "ts" 15 | 16 | @property 17 | def default_tz(self): 18 | return pytz.timezone(settings["TIME_ZONE"]) 19 | 20 | def is_native_type(self, value): 21 | return isinstance(value, self.type) 22 | 23 | def validate(self, value): 24 | """ 25 | 赋值和format会调用valiate,转普通类型转换成self.type类型 26 | :param value: 27 | :return: 28 | """ 29 | if isinstance(value, (str, bytes)): 30 | match = DATETIME_REGEX.match(value) 31 | if match: 32 | kwargs = {k: int(v) for k, v in 33 | match.groupdict().items() if v is not None} 34 | return datetime.datetime( 35 | **kwargs, tzinfo=self.default_tz).timestamp() 36 | raise ValidationError('Must be a valid timestamp.') 37 | 38 | def to_string(self, value): 39 | """ 40 | 所有最终会调用__getitem__的方法,会调用这个方法来反序列化,__getattr__则不会。 41 | :param value: 42 | :return: 43 | """ 44 | try: 45 | return datetime.datetime.fromtimestamp( 46 | value, self.default_tz).strftime("%Y-%m-%d %H:%M:%S") 47 | except AttributeError: 48 | return str(value) 49 | 50 | 51 | class TagsFormat(BaseFormat): 52 | 53 | type = list 54 | name = "tags" 55 | 56 | def is_native_type(self, value): 57 | return isinstance(value, self.type) 58 | 59 | def validate(self, value): 60 | if isinstance(value, str): 61 | return value.split(",") 62 | if isinstance(value, bytes): 63 | return value.decode().split(",") 64 | if isinstance(value, Sequence): 65 | return list(value) 66 | raise ValidationError('Must be a valid tags.') 67 | 68 | def to_string(self, value): 69 | if isinstance(value, list): 70 | value = ",".join(value) 71 | return value 72 | 73 | 74 | class Timestamp(validators.String): 75 | 76 | def __init__(self, **kwargs): 77 | super().__init__(format='ts', **kwargs) 78 | 79 | 80 | class Tags(validators.String): 81 | def __init__(self, **kwargs): 82 | super().__init__(format='tags', **kwargs) 83 | -------------------------------------------------------------------------------- /src/blog/blog/article/service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import hashlib 4 | import asyncio 5 | import zipfile 6 | import markdown 7 | import datetime 8 | import html2text 9 | 10 | from io import BytesIO 11 | from toolkit import cache_method 12 | from concurrent.futures import ThreadPoolExecutor 13 | from apistellar import FileResponse, Service, settings 14 | 15 | from .article import Article 16 | from .article_exporter import ArticleExporter 17 | 18 | 19 | class ArticleService(Service): 20 | def __init__(self): 21 | self.phantomjs_path = settings.get("PHANTOMJS_PATH") 22 | self.js_path = os.path.join(settings["PROJECT_PATH"], "cut_html.js") 23 | self.executor = ThreadPoolExecutor() 24 | 25 | async def get(self, id): 26 | """ 27 | 获取文章对象,并渲染文章正文 28 | :param id: 29 | :return: 30 | """ 31 | article = await Article.load(id=id) 32 | format_article_body = markdown.markdown( 33 | article.article, 34 | extensions=['markdown.extensions.extra']) 35 | article = article.to_dict() 36 | article["first_img"] = self._get_image(article.pop("article")) 37 | article["article"] = format_article_body 38 | return article 39 | 40 | async def export(self, article_list, code, url): 41 | """ 42 | 导出文章或文章列表,生成压缩包 43 | :param article_list: 44 | :param code: 45 | :param url: 46 | :return: 47 | """ 48 | zip_file = BytesIO() 49 | zf = zipfile.ZipFile(zip_file, "w") 50 | for article in article_list: 51 | zf.writestr(*await ArticleExporter(article, code, url).export()) 52 | 53 | zf.close() 54 | zip_file.seek(0) 55 | body = zip_file.read() 56 | zip_file.close() 57 | return FileResponse( 58 | body, filename=f"{datetime.datetime.now().timestamp()}.zip") 59 | 60 | async def upload(self, article): 61 | article["title"] = article.title or \ 62 | article.article.filename.replace(".md", "") 63 | buffer = article.article.read() 64 | try: 65 | article["article"] = buffer.decode("utf-8") 66 | except UnicodeDecodeError: 67 | article["article"] = buffer.decode("gbk") 68 | 69 | return await article.save() 70 | 71 | async def modify(self, article, img_url): 72 | """ 73 | 修改文章 74 | :param article: 75 | :param img_url: 76 | :return: 77 | """ 78 | h2t = html2text.HTML2Text() 79 | h2t.ignore_links = False 80 | h2t.ignore_images = False 81 | article.pop("img_url", None) 82 | article.article = "[comment]: (![](%s))\n%s" % ( 83 | img_url, h2t.handle(article.article) 84 | ) 85 | await article.update() 86 | 87 | async def update(self, article): 88 | """ 89 | 更新文章 90 | :param article: 91 | :return: 92 | """ 93 | await article.update() 94 | 95 | async def delete(self, article): 96 | """ 97 | 删除文章 98 | :param article: 99 | :return: 100 | """ 101 | await article.remove() 102 | 103 | async def about(self, id): 104 | """ 105 | 返回或者生成关于我和我的联系方式文章模板 106 | :param id: 107 | :return: 108 | """ 109 | article = await Article.load(id=id) 110 | if not article: 111 | article.id = id 112 | article.author = self.settings.get("AUTHOR") 113 | article.tags = [id] 114 | article.description = id 115 | article.feature = False 116 | article.article = id 117 | article.title = id 118 | article.show = False 119 | article.format() 120 | await article.save() 121 | 122 | article = article.to_dict() 123 | article["first_img"] = self._get_image(article["article"]) 124 | article["article"] = markdown.markdown( 125 | article["article"], extensions=['markdown.extensions.extra']) 126 | return article 127 | 128 | async def show(self, searchField, _from, size, fulltext): 129 | """ 130 | 首页展示 131 | :param searchField: 132 | :param _from: 133 | :param size: 134 | :param fulltext: 135 | :return: 136 | """ 137 | # 获取精品文档 138 | feature_articles = await Article.search( 139 | searchField, _from=_from, size=size, 140 | fulltext=fulltext, feature=True, show=True) 141 | self._enrich_first_img(feature_articles) 142 | 143 | # 获取首页文档 144 | articles = await Article.search( 145 | searchField, _from=_from, size=size, fulltext=fulltext, show=True) 146 | self._enrich_first_img(articles) 147 | # 获取全部tags 148 | count, tags = await Article.get_total_tags() 149 | 150 | return { 151 | "count": count, 152 | "articles": articles, 153 | "feature_articles": feature_articles, 154 | "tags": list(sorted(tags.items(), key=lambda x: x[1], reverse=True)) 155 | } 156 | 157 | async def cut(self, url, top, left, width, height): 158 | """ 159 | 按指定位置尺寸切网页 160 | :param url: 161 | :param top: 162 | :param left: 163 | :param width: 164 | :param height: 165 | :return: 166 | """ 167 | 168 | sh = hashlib.sha1(url.encode()) 169 | sh.update(bytes(str(top), encoding="utf-8")) 170 | sh.update(bytes(str(left), encoding="utf-8")) 171 | sh.update(bytes(str(width), encoding="utf-8")) 172 | sh.update(bytes(str(height), encoding="utf-8")) 173 | save_name = sh.hexdigest()[:10] + ".png" 174 | save_name = os.path.join( 175 | settings["PROJECT_PATH"], "static/temp/", save_name) 176 | loop = asyncio.get_event_loop() 177 | await loop.run_in_executor( 178 | self.executor, 179 | self._cut, 180 | url, save_name, top, left, width, height) 181 | return save_name 182 | 183 | @classmethod 184 | def _enrich_first_img(cls, articles): 185 | for index in range(len(articles)): 186 | article = articles[index].to_dict() 187 | article["first_img"] = cls._get_image(article.pop("article")) 188 | articles[index] = article 189 | 190 | @staticmethod 191 | def _get_image(body): 192 | try: 193 | image_part = body[:body.index("\n")] 194 | except ValueError: 195 | image_part = body 196 | mth = re.search(r"!\[.*?\]\((.*?)\)", image_part) 197 | return mth.group(1) if mth else "" 198 | 199 | @cache_method(3600) 200 | def _cut(self, url, save_name, top=0, left=0, width=1024, height=768): 201 | os.system(("%s " * 8) % ( 202 | self.phantomjs_path, self.js_path, url, 203 | save_name, top, left, width, height)) 204 | -------------------------------------------------------------------------------- /src/blog/blog/exchange/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/blog/blog/exchange/solo.py: -------------------------------------------------------------------------------- 1 | from apistellar import Solo 2 | 3 | from blog.blog.article.article_exporter import ArticleExporter 4 | 5 | 6 | class Exchange(Solo): 7 | """ 8 | 将网页转换pdf 9 | """ 10 | def __init__(self, input_path, output_path, **kwargs): 11 | self.input_path = input_path 12 | self.output_path = output_path 13 | super(Exchange, self).__init__(**kwargs) 14 | 15 | async def setup(self): 16 | """ 17 | 初始化 18 | :return: 19 | """ 20 | 21 | async def run(self): 22 | """ 23 | 业务逻辑 24 | :return: 25 | """ 26 | with open(self.input_path) as f: 27 | await ArticleExporter.save_as_pdf(f.read(), self.output_path) 28 | 29 | async def teardown(self): 30 | """ 31 | 回收资源 32 | :return: 33 | """ 34 | 35 | @classmethod 36 | def enrich_parser(cls, sub_parser): 37 | """ 38 | 自定义命令行参数,若定义了,则可通过__init__获取 39 | 注意在__init__中使用kwargs来保留其它参数,并调用父类的__init__ 40 | :param sub_parser: 41 | :return: 42 | """ 43 | sub_parser.add_argument("-i", "--input-path") 44 | sub_parser.add_argument("-o", "--output-path") 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/blog/blog/import_/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import glob 3 | import time 4 | from logging import getLogger 5 | 6 | from os.path import join, basename 7 | from apistellar import Solo, settings 8 | 9 | from ..article.article import Article 10 | 11 | 12 | logger = getLogger("import") 13 | 14 | 15 | class Import(Solo): 16 | 17 | def __init__(self, paths, **kwargs): 18 | self.paths = paths 19 | super(Import, self).__init__(**kwargs) 20 | 21 | async def setup(self): 22 | """ 23 | 初始化 24 | :return: 25 | """ 26 | 27 | async def run(self): 28 | """ 29 | 业务逻辑 30 | :return: 31 | """ 32 | for path in self.paths: 33 | for filename in glob.glob(join(path, "*")): 34 | await self.insert(filename) 35 | 36 | @classmethod 37 | async def insert(cls, filename): 38 | title = basename(filename).replace(".md", "") 39 | article = await Article.load(title=title) 40 | 41 | if not article: 42 | article.title = title 43 | lines = open(filename, encoding="utf-8").readlines() 44 | article.tags = cls.retrieve("tags", lines) or [] 45 | article.description = cls.retrieve("description", lines) 46 | article.title = cls.retrieve("title", lines) or title 47 | article.author = cls.retrieve("author", lines) or settings["AUTHOR"] 48 | article.article = "".join(lines) 49 | article.format() 50 | await article.save() 51 | time.sleep(1) 52 | logger.debug(f"Import {filename} to db. ") 53 | else: 54 | logger.info(f"Article {filename} exist. ") 55 | return article 56 | 57 | @staticmethod 58 | def retrieve(word, article): 59 | regex = re.compile(r"\[comment\]: <%s> \((.+?)\)" % word) 60 | for line in article[:]: 61 | mth = regex.search(line) 62 | if mth: 63 | article.remove(line) 64 | return mth.group(1) 65 | return "" 66 | 67 | async def teardown(self): 68 | """ 69 | 回收资源 70 | :return: 71 | """ 72 | 73 | @classmethod 74 | def enrich_parser(cls, sub_parser): 75 | """ 76 | 自定义命令行参数,若定义了,则可通过__init__获取 77 | 注意在__init__中使用kwargs来保留其它参数 78 | :param sub_parser: 79 | :return: 80 | """ 81 | sub_parser.add_argument("paths", nargs="+", help="目录地址") 82 | 83 | -------------------------------------------------------------------------------- /src/blog/blog/lib/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sqlite3 3 | import logging 4 | 5 | from pyaop import AOP, Proxy 6 | 7 | from apistellar import settings 8 | from apistellar.helper import cache_classproperty 9 | from apistellar.persistence import DriverMixin, proxy, contextmanager 10 | 11 | logger = logging.getLogger("sql") 12 | 13 | 14 | class SqliteProxy(Proxy): 15 | proxy_methods = ["execute"] 16 | 17 | 18 | def execute_before(self, *args, **kwargs): 19 | logger.debug(f"Execute sql: `{args[0]}` args: `{args[1]}`") 20 | 21 | 22 | class SqliteDriverMixin(DriverMixin): 23 | 24 | INIT_SQL_FILE = "blog.sql" 25 | DB_PATH = "db/blog" 26 | 27 | store = None # type: sqlite3.Cursor 28 | 29 | @cache_classproperty 30 | def init_sqlite(cls): 31 | project_path = settings["PROJECT_PATH"] 32 | os.makedirs(os.path.join( 33 | project_path, os.path.dirname(cls.DB_PATH)), exist_ok=True) 34 | table_initialize = open( 35 | os.path.join(project_path, cls.INIT_SQL_FILE)).read() 36 | conn = sqlite3.connect( 37 | os.path.join(project_path, cls.DB_PATH)) 38 | cur = conn.cursor() 39 | try: 40 | cur.execute(table_initialize) 41 | except sqlite3.OperationalError as e: 42 | pass 43 | return conn, cur 44 | 45 | @classmethod 46 | @contextmanager 47 | def get_store(cls, self_or_cls, **callargs): 48 | conn, cur = cls.init_sqlite 49 | with super(SqliteDriverMixin, cls).get_store( 50 | self_or_cls, **callargs) as self_or_cls: 51 | cur = conn.cursor() 52 | if hasattr(self_or_cls, "_need_proxy") \ 53 | and self_or_cls._need_proxy("store"): 54 | store = SqliteProxy( 55 | cur, before=[AOP.Hook(execute_before, ["execute"])]) 56 | self_or_cls = proxy(self_or_cls, prop_name="store", prop=store) 57 | try: 58 | yield self_or_cls 59 | finally: 60 | conn.commit() 61 | 62 | -------------------------------------------------------------------------------- /src/blog/blog/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | import os 3 | import time 4 | import random 5 | 6 | from apistellar import settings 7 | 8 | 9 | def code_generator(interval): 10 | project_path = settings["PROJECT_PATH"] 11 | code = None 12 | 13 | try: 14 | code, last_time = get_stored_code(project_path) 15 | except OSError: 16 | last_time = 0 17 | 18 | while True: 19 | pair = gen_code(last_time, interval, project_path) 20 | if pair: 21 | code = pair[0] 22 | last_time = pair[1] 23 | yield code 24 | 25 | 26 | def gen_code(last_time, interval, project_path): 27 | key = "ABCDEFGHIGKLMNOPQISTUVWXYZ0123456789" 28 | 29 | if time.time() - last_time > interval: 30 | code, last_time = "".join(random.choices(key, k=6)), time.time() 31 | with open(os.path.join(project_path, "code"), "w") as f: 32 | f.write(code) 33 | f.write("\n%d" % time.time()) 34 | return code, int(last_time) 35 | return None 36 | 37 | 38 | def get_stored_code(project_path): 39 | code, last_time = open(os.path.join(project_path, "code")).read().split("\n") 40 | last_time = int(last_time) 41 | return code, last_time 42 | -------------------------------------------------------------------------------- /src/blog/blog/welcome/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/blog/welcome/__init__.py -------------------------------------------------------------------------------- /src/blog/blog/welcome/controller.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from apistar import App 4 | from apistar.http import QueryParam 5 | 6 | from apistellar import Controller, route, get, post, require, Session, \ 7 | settings, MultiPartForm 8 | 9 | 10 | @route("/", name="welcome") 11 | class WelcomeController(Controller): 12 | """ 13 | 欢迎页 14 | """ 15 | @get("/") 16 | def index(self, app: App, path: QueryParam=None) -> str: 17 | """ 18 | 首页 19 | :param app: 20 | :param path: 子路径 21 | :ex path: 22 | `"/article?a=3"` 23 | :param settings: 配置信息 24 | :return: 25 | ```html 26 | ... 27 | ``` 28 | """ 29 | return app.render_template( 30 | 'index.html', 31 | author=settings["AUTHOR"], 32 | _path=path or "", 33 | page_size=settings["PAGE_SIZE"], 34 | url_for=app.reverse_url, 35 | code_swatch=str(settings.get_bool("NEED_CODE")).lower()) 36 | 37 | @post("/upload_image") 38 | @require(Session, judge=lambda x: x.get("login")) 39 | def upload(self, files: MultiPartForm): 40 | for name, file in files.items(): 41 | file.save(os.path.join( 42 | settings["PROJECT_PATH"], "static/img", file.filename)) 43 | return {"success": True} 44 | 45 | @post("/a/{b}/{+path}") 46 | async def test(self, b: int, path: str): 47 | print(b, path) 48 | -------------------------------------------------------------------------------- /src/blog/cut_html.js: -------------------------------------------------------------------------------- 1 | var page = require('webpage').create(); 2 | var system = require('system'); 3 | if(system.args.length != 7){ 4 | phantom.exit(); 5 | } 6 | 7 | // page.settings.javascriptEnabled = false; 8 | //viewportSize being the actual size of the headless browser 9 | page.viewportSize = { width: 1024, height: 768 }; 10 | //the clipRect is the portion of the page you are taking a screenshot of 11 | page.clipRect = { 12 | top: parseInt(system.args[3]) , 13 | left: parseInt(system.args[4]), 14 | width: parseInt(system.args[5]), 15 | height: parseInt(system.args[6])}; 16 | //the rest of the code is the same as the previous example 17 | page.open(system.args[1], function() { 18 | page.render(system.args[2]); 19 | phantom.exit(); 20 | }); -------------------------------------------------------------------------------- /src/blog/db/blog: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/db/blog -------------------------------------------------------------------------------- /src/blog/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding:utf-8 -*- 2 | STATIC_DIR = "static" 3 | 4 | STATIC_URL_PATH = "/static" 5 | 6 | TEMPLATE_DIR = "templates" 7 | 8 | USERNAME = "test" 9 | 10 | PASSWORD = "12345" 11 | 12 | TIME_ZONE = 'Asia/Shanghai' 13 | 14 | AUTHOR = "夏洛之枫" 15 | 16 | PAGE_SIZE = 40 17 | 18 | CODE_EXPIRE_INTERVAL = 30*24*3600 19 | 20 | PHANTOMJS_PATH = "phantomjs" 21 | 22 | NEED_CODE = False 23 | 24 | LOCAL_VARIABLE = {"session": "apistellar.bases.entities.Session"} 25 | -------------------------------------------------------------------------------- /src/blog/solo_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from apistellar import SoloManager 5 | 6 | app_name = "blog" 7 | 8 | 9 | def run(): 10 | logging.basicConfig( 11 | level=logging.DEBUG, 12 | format='%(asctime)s [%(name)s] %(levelname)s: %(message)s') 13 | SoloManager( 14 | app_name, current_dir=os.path.dirname(os.path.abspath(__file__))).start() 15 | 16 | 17 | if __name__ == "__main__": 18 | run() 19 | -------------------------------------------------------------------------------- /src/blog/static/css/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/blog/static/css/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/blog/static/css/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/css/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/blog/static/editor/css/iconfont.5ce067d.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/editor/css/iconfont.5ce067d.eot -------------------------------------------------------------------------------- /src/blog/static/editor/css/iconfont.7d3d0c4.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/editor/css/iconfont.7d3d0c4.ttf -------------------------------------------------------------------------------- /src/blog/static/editor/css/iconfont.9fadafc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Created by FontForge 20120731 at Wed Nov 30 13:57:20 2016 6 | By admin 7 | 8 | 9 | 10 | 24 | 26 | 28 | 30 | 32 | 34 | 38 | 43 | 48 | 51 | 54 | 57 | 60 | 63 | 66 | 69 | 72 | 75 | 80 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /src/blog/static/editor/css/iconfont.b300b13.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/editor/css/iconfont.b300b13.woff -------------------------------------------------------------------------------- /src/blog/static/editor/index.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:iconfont;src:url(css/iconfont.5ce067d.eot);src:url(css/iconfont.5ce067d.eot#iefix) format('embedded-opentype'),url(css/iconfont.b300b13.woff) format('woff'),url(css/iconfont.7d3d0c4.ttf) format('truetype'),url(css/iconfont.9fadafc.svg#iconfont) format('svg')}.iconfont{font-family:iconfont!important;font-size:16px;font-style:normal;-webkit-font-smoothing:antialiased;-webkit-text-stroke-width:.2px;-moz-osx-font-smoothing:grayscale}.icon-bold:before{content:"\EA09"}.icon-chain:before{content:"\EA36"}.icon-code:before{content:"\EA67"}.icon-compress:before{content:"\EA71"}.icon-ellipsish:before{content:"\EA95"}.icon-expand:before{content:"\EAA1"}.icon-image:before{content:"\EB26"}.icon-italic:before{content:"\EB31"}.icon-mailforward:before{content:"\EB52"}.icon-mailreply:before{content:"\EB53"}.icon-quoteleft:before{content:"\EBB8"}.icon-underline:before{content:"\EC4E"}.icon-shanchuxian2:before{content:"\E6F7"}@keyframes fadeIn{0%{opacity:0}50%{opacity:.5}to{opacity:1}}@keyframes fadeOut{0%{opacity:1}50%{opacity:.5}to{opacity:0}}.markdown__editor .fade{animation-duration:.2s}.markdown__editor .fade.in{animation-name:fadeIn}.markdown__editor .fade.out{animation-name:fadeOut}.markdown__editor-status{position:absolute;bottom:0;padding:8px;background:rgba(0,0,0,.1);width:100%;color:#333}.markdown__editor-status.info{background:rgba(130,232,255,.12)}.markdown__editor-status.success{background:rgba(101,255,177,.12)}.markdown__editor-status.error{background:hsla(0,100%,70%,.12)}.allow{height:15px;width:15px;border-left:2px solid rgba(0,0,0,.25);border-top:2px solid rgba(0,0,0,.25)}.allow.allow-left{transform:rotate(-45deg)}.allow.allow-right{transform:rotate(135deg)}.preview-tool{display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center;cursor:w-resize}.preview-tool .allow-wrapper{padding:20px 8px}.preview-tool .allow-wrapper:hover{cursor:pointer;background:rgba(0,0,0,.1)}.preview-tool .allow-wrapper:hover>.allow{border-color:#000}.markdown__editor-tool{display:-ms-flexbox;display:flex;overflow-x:auto;-ms-flex-pack:justify;justify-content:space-between;width:100%;padding:0 10px;background:#f7f7f7}.markdown__editor-tool .action-group{margin-right:25px;display:-ms-flexbox;display:flex}.markdown__editor-tool .action-group:last-child{margin:0}.markdown__editor-tool .iconfont{font-size:1.5em;padding:12px 15px}.markdown__editor-tool .iconfont:hover:not(.disabled){background:#fff;cursor:pointer}.markdown__editor-tool .iconfont.disabled{color:#ccc}.markdown__editor{height:100%;width:100%;border:1px solid #f5f5f5}.markdown__editor,.markdown__editor *{box-sizing:border-box}.markdown__editor.fullscreen{top:0;left:0;position:fixed}.markdown__editor .markdown__editor-wrapper{position:relative;height:100%;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column}.markdown__editor .markdown__editor-content{position:relative;-ms-flex:1;flex:1;background:#fff}.markdown__editor .markdown__editor-content .content-wrapper{position:absolute;height:100%;width:100%;display:-ms-flexbox;display:flex}.markdown__editor .markdown__editor-content .content-wrapper .markdown__editor-editor{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:16px;-ms-flex:1;flex:1;border:0;outline:0;resize:none;border-right:1px solid #f5f5f5;position:relative;box-sizing:border-box;padding:10px;background:#fff}.markdown__editor .hotkey-remind{position:fixed;padding:5px 8px;color:#fff;background:rgba(0,0,0,.5)}.markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#183691}.markdown-body .pl-v{color:#ed6a43}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{color:#f8f8f8;background-color:#b52a1d}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#63a35c}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#1d3e81}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{font-style:italic;color:#333}.markdown-body .pl-mb{font-weight:700;color:#333}.markdown-body .pl-md{color:#bd2c00;background-color:#ffecec}.markdown-body .pl-mi1{color:#55a532;background-color:#eaffea}.markdown-body .pl-mdr{font-weight:700;color:#795da3}.markdown-body .pl-mo{color:#1d3e81}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:inherit;font-weight:bolder}.markdown-body h1{font-size:2em;margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body code,.markdown-body kbd,.markdown-body pre{font-family:monospace,monospace;font-size:1em}.markdown-body hr{box-sizing:content-box;height:0;overflow:visible}.markdown-body input{font:inherit;margin:0;overflow:visible}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body *{box-sizing:border-box}.markdown-body input{font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body a{color:#4078c0;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{text-decoration:underline}.markdown-body strong{font-weight:600}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #ddd}.markdown-body hr:after,.markdown-body hr:before{display:table;content:""}.markdown-body hr:after{clear:both}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:0;margin-bottom:0}.markdown-body h1{font-size:32px;font-weight:600}.markdown-body h2{font-size:24px;font-weight:600}.markdown-body h3{font-size:20px;font-weight:600}.markdown-body h4{font-size:16px;font-weight:600}.markdown-body h5{font-size:14px;font-weight:600}.markdown-body h6{font-size:12px;font-weight:600}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0}.markdown-body ol,.markdown-body ul{padding-left:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:12px}.markdown-body pre{margin-top:0;margin-bottom:0;font:12px Consolas,Liberation Mono,Menlo,Courier,monospace}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body input{-webkit-font-feature-settings:"liga" 0;font-feature-settings:"liga" 0}.markdown-body:after,.markdown-body:before{display:table;content:""}.markdown-body:after{clear:both}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:none}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e7e7e7;border:0}.markdown-body blockquote{padding:0 1em;color:#777;border-left:.25em solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2em}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body h2{font-size:1.5em}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#777}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:0;padding-top:.2em;padding-bottom:.2em;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\A0"}.markdown-body pre{word-wrap:normal}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f7f7f7;border-radius:3px}.markdown-body pre code{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:3px!important}.markdown-body .pl-2{padding-left:6px!important}.markdown-body .pl-3{padding-left:12px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:36px!important}.markdown-body .pl-6{padding-left:48px!important}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#4078c0;border:1px solid #4078c0}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,Liberation Mono,Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#4078c0}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}.markdown__editor-preview{min-width:20%;padding:10px;font-size:16px;overflow:auto} -------------------------------------------------------------------------------- /src/blog/static/editor/preview.css: -------------------------------------------------------------------------------- 1 | .markdown-body{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;color:#333;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:16px;line-height:1.5;word-wrap:break-word}.markdown-body .pl-c{color:#969896}.markdown-body .pl-c1,.markdown-body .pl-s .pl-v{color:#0086b3}.markdown-body .pl-e,.markdown-body .pl-en{color:#795da3}.markdown-body .pl-s .pl-s1,.markdown-body .pl-smi{color:#333}.markdown-body .pl-ent{color:#63a35c}.markdown-body .pl-k{color:#a71d5d}.markdown-body .pl-pds,.markdown-body .pl-s,.markdown-body .pl-s .pl-pse .pl-s1,.markdown-body .pl-sr,.markdown-body .pl-sr .pl-cce,.markdown-body .pl-sr .pl-sra,.markdown-body .pl-sr .pl-sre{color:#183691}.markdown-body .pl-v{color:#ed6a43}.markdown-body .pl-id{color:#b52a1d}.markdown-body .pl-ii{color:#f8f8f8;background-color:#b52a1d}.markdown-body .pl-sr .pl-cce{font-weight:700;color:#63a35c}.markdown-body .pl-ml{color:#693a17}.markdown-body .pl-mh,.markdown-body .pl-mh .pl-en,.markdown-body .pl-ms{font-weight:700;color:#1d3e81}.markdown-body .pl-mq{color:teal}.markdown-body .pl-mi{font-style:italic;color:#333}.markdown-body .pl-mb{font-weight:700;color:#333}.markdown-body .pl-md{color:#bd2c00;background-color:#ffecec}.markdown-body .pl-mi1{color:#55a532;background-color:#eaffea}.markdown-body .pl-mdr{font-weight:700;color:#795da3}.markdown-body .pl-mo{color:#1d3e81}.markdown-body .octicon{display:inline-block;vertical-align:text-top;fill:currentColor}.markdown-body a{background-color:transparent;-webkit-text-decoration-skip:objects}.markdown-body a:active,.markdown-body a:hover{outline-width:0}.markdown-body strong{font-weight:inherit;font-weight:bolder}.markdown-body h1{font-size:2em;margin:.67em 0}.markdown-body img{border-style:none}.markdown-body svg:not(:root){overflow:hidden}.markdown-body code,.markdown-body kbd,.markdown-body pre{font-family:monospace,monospace;font-size:1em}.markdown-body hr{box-sizing:content-box;height:0;overflow:visible}.markdown-body input{font:inherit;margin:0;overflow:visible}.markdown-body [type=checkbox]{box-sizing:border-box;padding:0}.markdown-body *{box-sizing:border-box}.markdown-body input{font-family:inherit;font-size:inherit;line-height:inherit}.markdown-body a{color:#4078c0;text-decoration:none}.markdown-body a:active,.markdown-body a:hover{text-decoration:underline}.markdown-body strong{font-weight:600}.markdown-body hr{height:0;margin:15px 0;overflow:hidden;background:transparent;border:0;border-bottom:1px solid #ddd}.markdown-body hr:after,.markdown-body hr:before{display:table;content:""}.markdown-body hr:after{clear:both}.markdown-body table{border-spacing:0;border-collapse:collapse}.markdown-body td,.markdown-body th{padding:0}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:0;margin-bottom:0}.markdown-body h1{font-size:32px;font-weight:600}.markdown-body h2{font-size:24px;font-weight:600}.markdown-body h3{font-size:20px;font-weight:600}.markdown-body h4{font-size:16px;font-weight:600}.markdown-body h5{font-size:14px;font-weight:600}.markdown-body h6{font-size:12px;font-weight:600}.markdown-body p{margin-top:0;margin-bottom:10px}.markdown-body blockquote{margin:0}.markdown-body ol,.markdown-body ul{padding-left:0;margin-top:0;margin-bottom:0}.markdown-body ol ol,.markdown-body ul ol{list-style-type:lower-roman}.markdown-body ol ol ol,.markdown-body ol ul ol,.markdown-body ul ol ol,.markdown-body ul ul ol{list-style-type:lower-alpha}.markdown-body dd{margin-left:0}.markdown-body code{font-family:Consolas,Liberation Mono,Menlo,Courier,monospace;font-size:12px}.markdown-body pre{margin-top:0;margin-bottom:0;font:12px Consolas,Liberation Mono,Menlo,Courier,monospace}.markdown-body .octicon{vertical-align:text-bottom}.markdown-body input{-webkit-font-feature-settings:"liga" 0;font-feature-settings:"liga" 0}.markdown-body:after,.markdown-body:before{display:table;content:""}.markdown-body:after{clear:both}.markdown-body>:first-child{margin-top:0!important}.markdown-body>:last-child{margin-bottom:0!important}.markdown-body a:not([href]){color:inherit;text-decoration:none}.markdown-body .anchor{float:left;padding-right:4px;margin-left:-20px;line-height:1}.markdown-body .anchor:focus{outline:none}.markdown-body blockquote,.markdown-body dl,.markdown-body ol,.markdown-body p,.markdown-body pre,.markdown-body table,.markdown-body ul{margin-top:0;margin-bottom:16px}.markdown-body hr{height:.25em;padding:0;margin:24px 0;background-color:#e7e7e7;border:0}.markdown-body blockquote{padding:0 1em;color:#777;border-left:.25em solid #ddd}.markdown-body blockquote>:first-child{margin-top:0}.markdown-body blockquote>:last-child{margin-bottom:0}.markdown-body kbd{display:inline-block;padding:3px 5px;font-size:11px;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{margin-top:24px;margin-bottom:16px;font-weight:600;line-height:1.25}.markdown-body h1 .octicon-link,.markdown-body h2 .octicon-link,.markdown-body h3 .octicon-link,.markdown-body h4 .octicon-link,.markdown-body h5 .octicon-link,.markdown-body h6 .octicon-link{color:#000;vertical-align:middle;visibility:hidden}.markdown-body h1:hover .anchor,.markdown-body h2:hover .anchor,.markdown-body h3:hover .anchor,.markdown-body h4:hover .anchor,.markdown-body h5:hover .anchor,.markdown-body h6:hover .anchor{text-decoration:none}.markdown-body h1:hover .anchor .octicon-link,.markdown-body h2:hover .anchor .octicon-link,.markdown-body h3:hover .anchor .octicon-link,.markdown-body h4:hover .anchor .octicon-link,.markdown-body h5:hover .anchor .octicon-link,.markdown-body h6:hover .anchor .octicon-link{visibility:visible}.markdown-body h1{font-size:2em}.markdown-body h1,.markdown-body h2{padding-bottom:.3em;border-bottom:1px solid #eee}.markdown-body h2{font-size:1.5em}.markdown-body h3{font-size:1.25em}.markdown-body h4{font-size:1em}.markdown-body h5{font-size:.875em}.markdown-body h6{font-size:.85em;color:#777}.markdown-body ol,.markdown-body ul{padding-left:2em}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:0;margin-bottom:0}.markdown-body li>p{margin-top:16px}.markdown-body li+li{margin-top:.25em}.markdown-body dl{padding:0}.markdown-body dl dt{padding:0;margin-top:16px;font-size:1em;font-style:italic;font-weight:700}.markdown-body dl dd{padding:0 16px;margin-bottom:16px}.markdown-body table{display:block;width:100%;overflow:auto}.markdown-body table th{font-weight:700}.markdown-body table td,.markdown-body table th{padding:6px 13px;border:1px solid #ddd}.markdown-body table tr{background-color:#fff;border-top:1px solid #ccc}.markdown-body table tr:nth-child(2n){background-color:#f8f8f8}.markdown-body img{max-width:100%;box-sizing:content-box;background-color:#fff}.markdown-body code{padding:0;padding-top:.2em;padding-bottom:.2em;margin:0;font-size:85%;background-color:rgba(0,0,0,.04);border-radius:3px}.markdown-body code:after,.markdown-body code:before{letter-spacing:-.2em;content:"\A0"}.markdown-body pre{word-wrap:normal}.markdown-body pre>code{padding:0;margin:0;font-size:100%;word-break:normal;white-space:pre;background:transparent;border:0}.markdown-body .highlight{margin-bottom:16px}.markdown-body .highlight pre{margin-bottom:0;word-break:normal}.markdown-body .highlight pre,.markdown-body pre{padding:16px;overflow:auto;font-size:85%;line-height:1.45;background-color:#f7f7f7;border-radius:3px}.markdown-body pre code{display:inline;max-width:auto;padding:0;margin:0;overflow:visible;line-height:inherit;word-wrap:normal;background-color:transparent;border:0}.markdown-body pre code:after,.markdown-body pre code:before{content:normal}.markdown-body .pl-0{padding-left:0!important}.markdown-body .pl-1{padding-left:3px!important}.markdown-body .pl-2{padding-left:6px!important}.markdown-body .pl-3{padding-left:12px!important}.markdown-body .pl-4{padding-left:24px!important}.markdown-body .pl-5{padding-left:36px!important}.markdown-body .pl-6{padding-left:48px!important}.markdown-body .full-commit .btn-outline:not(:disabled):hover{color:#4078c0;border:1px solid #4078c0}.markdown-body kbd{display:inline-block;padding:3px 5px;font:11px Consolas,Liberation Mono,Menlo,Courier,monospace;line-height:10px;color:#555;vertical-align:middle;background-color:#fcfcfc;border:1px solid #ccc;border-bottom-color:#bbb;border-radius:3px;box-shadow:inset 0 -1px 0 #bbb}.markdown-body :checked+.radio-label{position:relative;z-index:1;border-color:#4078c0}.markdown-body .task-list-item{list-style-type:none}.markdown-body .task-list-item+.task-list-item{margin-top:3px}.markdown-body .task-list-item input{margin:0 .2em .25em -1.6em;vertical-align:middle}.markdown-body hr{border-bottom-color:#eee}.markdown__editor-preview{min-width:20%;padding:10px;font-size:16px;overflow:auto} -------------------------------------------------------------------------------- /src/blog/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/img/favicon.ico -------------------------------------------------------------------------------- /src/blog/static/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/img/logo.png -------------------------------------------------------------------------------- /src/blog/static/img/pretty/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/src/blog/static/img/pretty/0.jpg -------------------------------------------------------------------------------- /src/blog/static/js/jqpage.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | 4 | $.jqPaginator = function (el, options) { 5 | if(!(this instanceof $.jqPaginator)){ 6 | return new $.jqPaginator(el, options); 7 | } 8 | 9 | var self = this; 10 | 11 | self.$container = $(el); 12 | 13 | self.$container.data('jqPaginator', self); 14 | 15 | self.init = function () { 16 | 17 | if (options.first || options.prev || options.next || options.last || options.page) { 18 | options = $.extend({}, { 19 | first: '', 20 | prev: '', 21 | next: '', 22 | last: '', 23 | page: '' 24 | }, options); 25 | } 26 | 27 | self.options = $.extend({}, $.jqPaginator.defaultOptions, options); 28 | 29 | self.verify(); 30 | 31 | self.extendJquery(); 32 | 33 | self.render(); 34 | 35 | self.fireEvent(this.options.currentPage, 'init'); 36 | }; 37 | 38 | self.verify = function () { 39 | var opts = self.options; 40 | 41 | if (!self.isNumber(opts.totalPages)) { 42 | throw new Error('[jqPaginator] type error: totalPages'); 43 | } 44 | 45 | if (!self.isNumber(opts.totalCounts)) { 46 | throw new Error('[jqPaginator] type error: totalCounts'); 47 | } 48 | 49 | if (!self.isNumber(opts.pageSize)) { 50 | throw new Error('[jqPaginator] type error: pageSize'); 51 | } 52 | 53 | if (!self.isNumber(opts.currentPage)) { 54 | throw new Error('[jqPaginator] type error: currentPage'); 55 | } 56 | 57 | if (!self.isNumber(opts.visiblePages)) { 58 | throw new Error('[jqPaginator] type error: visiblePages'); 59 | } 60 | 61 | if (!opts.totalPages && !opts.totalCounts) { 62 | throw new Error('[jqPaginator] totalCounts or totalPages is required'); 63 | } 64 | 65 | if (!opts.totalPages && !opts.totalCounts) { 66 | throw new Error('[jqPaginator] totalCounts or totalPages is required'); 67 | } 68 | 69 | if (!opts.totalPages && opts.totalCounts && !opts.pageSize) { 70 | throw new Error('[jqPaginator] pageSize is required'); 71 | } 72 | 73 | if (opts.totalCounts && opts.pageSize) { 74 | opts.totalPages = Math.ceil(opts.totalCounts / opts.pageSize); 75 | } 76 | 77 | if (opts.currentPage < 1 || opts.currentPage > opts.totalPages) { 78 | throw new Error('[jqPaginator] currentPage is incorrect'); 79 | } 80 | 81 | if (opts.totalPages < 1) { 82 | throw new Error('[jqPaginator] totalPages cannot be less currentPage'); 83 | } 84 | }; 85 | 86 | self.extendJquery = function () { 87 | $.fn.jqPaginatorHTML = function (s) { 88 | return s ? this.before(s).remove() : $('

').append(this.eq(0).clone()).html(); 89 | }; 90 | }; 91 | 92 | self.render = function () { 93 | self.renderHtml(); 94 | self.setStatus(); 95 | self.bindEvents(); 96 | }; 97 | 98 | self.renderHtml = function () { 99 | var html = []; 100 | 101 | var pages = self.getPages(); 102 | for (var i = 0, j = pages.length; i < j; i++) { 103 | html.push(self.buildItem('page', pages[i])); 104 | } 105 | 106 | self.isEnable('prev') && html.unshift(self.buildItem('prev', self.options.currentPage - 1)); 107 | self.isEnable('first') && html.unshift(self.buildItem('first', 1)); 108 | self.isEnable('statistics') && html.unshift(self.buildItem('statistics')); 109 | self.isEnable('next') && html.push(self.buildItem('next', self.options.currentPage + 1)); 110 | self.isEnable('last') && html.push(self.buildItem('last', self.options.totalPages)); 111 | 112 | if (self.options.wrapper) { 113 | self.$container.html($(self.options.wrapper).html(html.join('')).jqPaginatorHTML()); 114 | } else { 115 | self.$container.html(html.join('')); 116 | } 117 | }; 118 | 119 | self.buildItem = function (type, pageData) { 120 | var html = self.options[type] 121 | .replace(/{{page}}/g, pageData) 122 | .replace(/{{totalPages}}/g, self.options.totalPages) 123 | .replace(/{{totalCounts}}/g, self.options.totalCounts); 124 | 125 | return $(html).attr({ 126 | 'jp-role': type, 127 | 'jp-data': pageData 128 | }).jqPaginatorHTML(); 129 | }; 130 | 131 | self.setStatus = function () { 132 | var options = self.options; 133 | 134 | if (!self.isEnable('first') || options.currentPage === 1) { 135 | $('[jp-role=first]', self.$container).addClass(options.disableClass); 136 | } 137 | if (!self.isEnable('prev') || options.currentPage === 1) { 138 | $('[jp-role=prev]', self.$container).addClass(options.disableClass); 139 | } 140 | if (!self.isEnable('next') || options.currentPage >= options.totalPages) { 141 | $('[jp-role=next]', self.$container).addClass(options.disableClass); 142 | } 143 | if (!self.isEnable('last') || options.currentPage >= options.totalPages) { 144 | $('[jp-role=last]', self.$container).addClass(options.disableClass); 145 | } 146 | 147 | $('[jp-role=page]', self.$container).removeClass(options.activeClass); 148 | $('[jp-role=page][jp-data=' + options.currentPage + ']', self.$container).addClass(options.activeClass); 149 | }; 150 | 151 | self.getPages = function () { 152 | var pages = [], 153 | visiblePages = self.options.visiblePages, 154 | currentPage = self.options.currentPage, 155 | totalPages = self.options.totalPages; 156 | 157 | if (visiblePages > totalPages) { 158 | visiblePages = totalPages; 159 | } 160 | 161 | var half = Math.floor(visiblePages / 2); 162 | var start = currentPage - half + 1 - visiblePages % 2; 163 | var end = currentPage + half; 164 | 165 | if (start < 1) { 166 | start = 1; 167 | end = visiblePages; 168 | } 169 | if (end > totalPages) { 170 | end = totalPages; 171 | start = 1 + totalPages - visiblePages; 172 | } 173 | 174 | var itPage = start; 175 | while (itPage <= end) { 176 | pages.push(itPage); 177 | itPage++; 178 | } 179 | 180 | return pages; 181 | }; 182 | 183 | self.isNumber = function (value) { 184 | var type = typeof value; 185 | return type === 'number' || type === 'undefined'; 186 | }; 187 | 188 | self.isEnable = function (type) { 189 | return self.options[type] && typeof self.options[type] === 'string'; 190 | }; 191 | 192 | self.switchPage = function (pageIndex) { 193 | self.options.currentPage = pageIndex; 194 | self.render(); 195 | }; 196 | 197 | self.fireEvent = function (pageIndex, type) { 198 | return (typeof self.options.onPageChange !== 'function') || (self.options.onPageChange(pageIndex, type) !== false); 199 | }; 200 | 201 | self.callMethod = function (method, options) { 202 | switch (method) { 203 | case 'option': 204 | self.options = $.extend({}, self.options, options); 205 | self.verify(); 206 | self.render(); 207 | break; 208 | case 'destroy': 209 | self.$container.empty(); 210 | self.$container.removeData('jqPaginator'); 211 | break; 212 | default : 213 | throw new Error('[jqPaginator] method "' + method + '" does not exist'); 214 | } 215 | 216 | return self.$container; 217 | }; 218 | 219 | self.bindEvents = function () { 220 | var opts = self.options; 221 | 222 | self.$container.off(); 223 | self.$container.on('click', '[jp-role]', function (event) { 224 | var $el = $(this); 225 | if ($el.hasClass(opts.disableClass) || $el.hasClass(opts.activeClass)) { 226 | // 用来阻止a标签的默认行为 227 | event.preventDefault(); 228 | return; 229 | } 230 | 231 | var pageIndex = +$el.attr('jp-data'); 232 | if (self.fireEvent(pageIndex, 'change')) { 233 | self.switchPage(pageIndex); 234 | } 235 | }); 236 | }; 237 | 238 | self.init(); 239 | 240 | return self.$container; 241 | }; 242 | 243 | $.jqPaginator.defaultOptions = { 244 | wrapper: '', 245 | first: '

  • <<
  • ', 246 | prev: '', 247 | next: '', 248 | last: '
  • >>
  • ', 249 | page: '
  • {{page}}
  • ', 250 | totalPages: 0, 251 | totalCounts: 0, 252 | pageSize: 0, 253 | currentPage: 1, 254 | visiblePages: 7, 255 | disableClass: 'disabled', 256 | activeClass: 'active', 257 | onPageChange: null 258 | }; 259 | 260 | $.fn.jqPaginator = function () { 261 | var self = this, 262 | args = Array.prototype.slice.call(arguments); 263 | 264 | if (typeof args[0] === 'string') { 265 | var $instance = $(self).data('jqPaginator'); 266 | if (!$instance) { 267 | throw new Error('[jqPaginator] the element is not instantiated'); 268 | } else { 269 | return $instance.callMethod(args[0], args[1]); 270 | } 271 | } else { 272 | return new $.jqPaginator(this, args[0]); 273 | } 274 | }; 275 | 276 | })(jQuery); -------------------------------------------------------------------------------- /src/blog/static/js/modernizr.js: -------------------------------------------------------------------------------- 1 | /* Modernizr 2.7.1 (Custom Build) | MIT & BSD 2 | * Build: http://modernizr.com/download/#-inlinesvg-shiv-cssclasses-prefixed-teststyles-testprop-testallprops-hasevent-prefixes-domprefixes 3 | */ 4 | ;window.Modernizr=function(a,b,c){function B(a){j.cssText=a}function C(a,b){return B(m.join(a+";")+(b||""))}function D(a,b){return typeof a===b}function E(a,b){return!!~(""+a).indexOf(b)}function F(a,b){for(var d in a){var e=a[d];if(!E(e,"-")&&j[e]!==c)return b=="pfx"?e:!0}return!1}function G(a,b,d){for(var e in a){var f=b[a[e]];if(f!==c)return d===!1?a[e]:D(f,"function")?f.bind(d||b):f}return!1}function H(a,b,c){var d=a.charAt(0).toUpperCase()+a.slice(1),e=(a+" "+o.join(d+" ")+d).split(" ");return D(b,"string")||D(b,"undefined")?F(e,b):(e=(a+" "+p.join(d+" ")+d).split(" "),G(e,b,c))}var d="2.7.1",e={},f=!0,g=b.documentElement,h="modernizr",i=b.createElement(h),j=i.style,k,l={}.toString,m=" -webkit- -moz- -o- -ms- ".split(" "),n="Webkit Moz O ms",o=n.split(" "),p=n.toLowerCase().split(" "),q={svg:"http://www.w3.org/2000/svg"},r={},s={},t={},u=[],v=u.slice,w,x=function(a,c,d,e){var f,i,j,k,l=b.createElement("div"),m=b.body,n=m||b.createElement("body");if(parseInt(d,10))while(d--)j=b.createElement("div"),j.id=e?e[d]:h+(d+1),l.appendChild(j);return f=["­",'"].join(""),l.id=h,(m?l:n).innerHTML+=f,n.appendChild(l),m||(n.style.background="",n.style.overflow="hidden",k=g.style.overflow,g.style.overflow="hidden",g.appendChild(n)),i=c(l,a),m?l.parentNode.removeChild(l):(n.parentNode.removeChild(n),g.style.overflow=k),!!i},y=function(){function d(d,e){e=e||b.createElement(a[d]||"div"),d="on"+d;var f=d in e;return f||(e.setAttribute||(e=b.createElement("div")),e.setAttribute&&e.removeAttribute&&(e.setAttribute(d,""),f=D(e[d],"function"),D(e[d],"undefined")||(e[d]=c),e.removeAttribute(d))),e=null,f}var a={select:"input",change:"input",submit:"form",reset:"form",error:"img",load:"img",abort:"img"};return d}(),z={}.hasOwnProperty,A;!D(z,"undefined")&&!D(z.call,"undefined")?A=function(a,b){return z.call(a,b)}:A=function(a,b){return b in a&&D(a.constructor.prototype[b],"undefined")},Function.prototype.bind||(Function.prototype.bind=function(b){var c=this;if(typeof c!="function")throw new TypeError;var d=v.call(arguments,1),e=function(){if(this instanceof e){var a=function(){};a.prototype=c.prototype;var f=new a,g=c.apply(f,d.concat(v.call(arguments)));return Object(g)===g?g:f}return c.apply(b,d.concat(v.call(arguments)))};return e}),r.inlinesvg=function(){var a=b.createElement("div");return a.innerHTML="",(a.firstChild&&a.firstChild.namespaceURI)==q.svg};for(var I in r)A(r,I)&&(w=I.toLowerCase(),e[w]=r[I](),u.push((e[w]?"":"no-")+w));return e.addTest=function(a,b){if(typeof a=="object")for(var d in a)A(a,d)&&e.addTest(d,a[d]);else{a=a.toLowerCase();if(e[a]!==c)return e;b=typeof b=="function"?b():b,typeof f!="undefined"&&f&&(g.className+=" "+(b?"":"no-")+a),e[a]=b}return e},B(""),i=k=null,function(a,b){function l(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function m(){var a=s.elements;return typeof a=="string"?a.split(" "):a}function n(a){var b=j[a[h]];return b||(b={},i++,a[h]=i,j[i]=b),b}function o(a,c,d){c||(c=b);if(k)return c.createElement(a);d||(d=n(c));var g;return d.cache[a]?g=d.cache[a].cloneNode():f.test(a)?g=(d.cache[a]=d.createElem(a)).cloneNode():g=d.createElem(a),g.canHaveChildren&&!e.test(a)&&!g.tagUrn?d.frag.appendChild(g):g}function p(a,c){a||(a=b);if(k)return a.createDocumentFragment();c=c||n(a);var d=c.frag.cloneNode(),e=0,f=m(),g=f.length;for(;e",g="hidden"in a,k=a.childNodes.length==1||function(){b.createElement("a");var a=b.createDocumentFragment();return typeof a.cloneNode=="undefined"||typeof a.createDocumentFragment=="undefined"||typeof a.createElement=="undefined"}()}catch(c){g=!0,k=!0}})();var s={elements:d.elements||"abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output progress section summary template time video",version:c,shivCSS:d.shivCSS!==!1,supportsUnknownElements:k,shivMethods:d.shivMethods!==!1,type:"default",shivDocument:r,createElement:o,createDocumentFragment:p};a.html5=s,r(b)}(this,b),e._version=d,e._prefixes=m,e._domPrefixes=p,e._cssomPrefixes=o,e.hasEvent=y,e.testProp=function(a){return F([a])},e.testAllProps=H,e.testStyles=x,e.prefixed=function(a,b,c){return b?H(a,b,c):H(a,"pfx")},g.className=g.className.replace(/(^|\s)no-js(\s|$)/,"$1$2")+(f?" js "+u.join(" "):""),e}(this,this.document); -------------------------------------------------------------------------------- /src/blog/static/js/react-dom-0.14.0.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ReactDOM v0.14.0 3 | * 4 | * Copyright 2013-2015, Facebook, Inc. 5 | * All rights reserved. 6 | * 7 | * This source code is licensed under the BSD-style license found in the 8 | * LICENSE file in the root directory of this source tree. An additional grant 9 | * of patent rights can be found in the PATENTS file in the same directory. 10 | * 11 | */ 12 | // Based off https://github.com/ForbesLindesay/umd/blob/master/template.js 13 | ;(function(f) { 14 | // CommonJS 15 | if (typeof exports === "object" && typeof module !== "undefined") { 16 | module.exports = f(require('react')); 17 | 18 | // RequireJS 19 | } else if (typeof define === "function" && define.amd) { 20 | define(['react'], f); 21 | 22 | // -------------------------------------------------------------------------------- /src/blog/templates/edit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 更新 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
    19 | 56 |
    57 | 58 | 70 | -------------------------------------------------------------------------------- /src/blog/templates/import.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 导入 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
    18 | 52 |
    53 | 54 | -------------------------------------------------------------------------------- /src/blog/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 登陆 4 | 5 | 6 | 7 |
    8 |

    这里不是你该来的地方

    9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 | 21 | -------------------------------------------------------------------------------- /src/blog/web_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from uvicorn import run 5 | from apistellar import Application 6 | from whitenoise import WhiteNoise 7 | 8 | # 静态文件每次请求重新查找 9 | WhiteNoise.autorefresh = True 10 | 11 | app_name = "blog" 12 | logging.basicConfig( 13 | level=logging.DEBUG, 14 | format='%(asctime)s [%(name)s] %(levelname)s: %(message)s') 15 | 16 | app = Application( 17 | app_name, debug=False, 18 | current_dir=os.path.dirname(os.path.abspath(__file__))) 19 | 20 | if __name__ == "__main__": 21 | run(app) 22 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from apistellar import init_settings 2 | 3 | init_settings("blog.settings") 4 | -------------------------------------------------------------------------------- /tests/factories.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from apistellar import settings 4 | 5 | 6 | def get_code(): 7 | code, _ = open(os.path.join( 8 | settings["PROJECT_PATH"], "code")).read().split("\n") 9 | return code 10 | -------------------------------------------------------------------------------- /tests/test_article/test_article.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pytest_apistellar import prop_alias 4 | from pytest_apistellar.parser import Attr 5 | 6 | from blog.blog.article.article import Article 7 | 8 | from factories import get_code 9 | 10 | article = prop_alias("blog.blog.article.article.Article") 11 | 12 | 13 | def assert_execute(sql, args, assert_sql, assert_args, **kwargs): 14 | assert sql == assert_sql 15 | assert args == assert_args 16 | 17 | 18 | @article("code", ret_factory=get_code) 19 | @pytest.mark.env(NEED_CODE="False") 20 | @pytest.mark.env(CODE_EXPIRE_INTERVAL=30 * 24 * 3600) 21 | @pytest.mark.asyncio 22 | class TestArticle(object): 23 | 24 | pytestmark = [ 25 | article("store", ret_val=Attr()), 26 | article("store.execute", callable=True) 27 | ] 28 | 29 | @pytest.mark.env(NEED_CODE="True") 30 | async def test_check_code_on_True(self): 31 | assert Article().right_code(get_code()) is True 32 | 33 | @pytest.mark.env(NEED_CODE="True") 34 | async def test_check_code_on_False(self): 35 | assert Article().right_code("111111") is False 36 | 37 | async def test_check_code_off(self): 38 | assert Article().right_code("111111") is True 39 | 40 | @article("store.description", ret_val=[("id", )]) 41 | @article("store.fetchone", ret_val=("111", ), callable=True) 42 | async def test_load_with_article(self): 43 | article = await Article.load() 44 | assert article["id"] == "111" 45 | 46 | @article("store.execute", ret_factory=assert_execute, 47 | assert_sql=f"INSERT INTO {Article.TABLE} " 48 | f"VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?);", 49 | assert_args=("2017010101010", "ccc", "aaa,bbb", "dddd", "夏洛之枫", 50 | "aaa", True, 1539140711.0, 1539140711.0, False)) 51 | async def test_save(self): 52 | data = { 53 | "id": "2017010101010", 54 | "description": "ccc", 55 | "tags": "aaa,bbb", 56 | "article": "dddd", 57 | "author": "夏洛之枫", 58 | "title": "aaa", 59 | "feature": "1", 60 | "created_at": "2018-10-10 11:11:11", 61 | "updated_at": "2018-10-10 11:11:11", 62 | "show": "0" 63 | } 64 | article = Article(data) 65 | await article.save() 66 | 67 | @article("store.execute", ret_factory=assert_execute, 68 | assert_sql=f"DELETE FROM {Article.TABLE} WHERE show=1 and id=?;", 69 | assert_args=("2017010101010", )) 70 | async def test_remove(self): 71 | article = Article(id="2017010101010") 72 | await article.remove() 73 | 74 | @article("_build_update_sql", ret_val=("test_sql", ["1"])) 75 | @article("store.execute", ret_factory=assert_execute, 76 | assert_sql="test_sql", 77 | assert_args=["1"]) 78 | async def test_update(self): 79 | data = { 80 | "id": "2017010101010", 81 | "description": "ccc", 82 | "show": "0" 83 | } 84 | article = Article(data) 85 | await article.update() 86 | 87 | @article("store.fetchone", callable=True) 88 | async def test_load_without_article(self): 89 | assert "id" not in await Article.load() 90 | 91 | @article("load_list", 92 | ret_val=[Article(tags="python,abc"), Article(tags="abc")]) 93 | async def test_get_total_tags(self): 94 | total, tags = await Article.get_total_tags() 95 | assert total == 2 96 | assert tags["python"] == 1 97 | assert tags["abc"] == 2 98 | 99 | @article("_build_select_sql", ret_val=( 100 | ["a", "b"], "SELECT * FROM articles WHERE 1=1 AND a=? AND b=?")) 101 | @article("store.execute", ret_factory=assert_execute, 102 | assert_sql="SELECT * FROM articles WHERE 1=1 AND a=? AND b=?", 103 | assert_args=["a", "b"]) 104 | @article("store.fetchall", ret_val=[("abc", "python")], callable=True) 105 | @article("store.description", ret_val=[("title", ), ("tags", )]) 106 | async def test_load_list(self): 107 | article_list = await Article.load_list() 108 | assert article_list[0].title == "abc" 109 | assert article_list[0].tags == "python" 110 | 111 | @article("load_list", ret_factory=lambda sub=None, **kwargs: sub) 112 | async def test_search(self): 113 | assert await Article.search("abc", 0, 10, fulltext=True) == \ 114 | " AND (article LIKE ? OR tags LIKE ?)" 115 | 116 | async def test_fuzzy_search_sub_sql_without_field(self): 117 | assert Article._fuzzy_search_sub_sql("", False) == (None, None) 118 | 119 | async def test_fuzzy_search_sub_sql_with_field_fulltext(self): 120 | sub, vals = Article._fuzzy_search_sub_sql("abc", True) 121 | assert sub == " AND (article LIKE ? OR tags LIKE ?)" 122 | assert vals == ['%abc%', '%abc%'] 123 | 124 | async def test_fuzzy_search_sub_sql_with_field_fulltext_false(self): 125 | sub, vals = Article._fuzzy_search_sub_sql("abc", False) 126 | assert sub == " AND tags LIKE ?" 127 | assert vals == ['%abc%'] 128 | 129 | async def test_build_select_sql_with_no_args(self): 130 | vals, sql = Article._build_select_sql() 131 | assert vals == list() 132 | assert sql == f"SELECT * FROM {Article.TABLE} WHERE 1=1;" 133 | 134 | async def test_build_select_sql_with_vals_and_sub(self): 135 | vals, sql = Article._build_select_sql(vals=["name"], sub=" AND title=?") 136 | assert vals == ["name"] 137 | assert sql == f"SELECT * FROM {Article.TABLE} WHERE 1=1 AND title=?;" 138 | 139 | async def test_build_select_sql_with_kwargs_of_in_opt(self): 140 | vals, sql = Article._build_select_sql({"id": ["1", "2"], "title": "a"}) 141 | assert "1" in vals 142 | assert "2" in vals 143 | assert "a" in vals 144 | assert "id IN (?, ?)" in sql 145 | assert "title=?" in sql 146 | 147 | async def test_build_select_sql_with_order_fields_and_projection(self): 148 | vals, sql = Article._build_select_sql( 149 | projection=["tags", "id", "title"], 150 | order_fields=[("id", "desc"), ("title", "asc")]) 151 | assert vals == [] 152 | assert "id desc" in sql 153 | assert "title asc" in sql 154 | assert "tags, id, title" in sql 155 | 156 | async def test_build_select_sql_with_size_and_from(self): 157 | vals, sql = Article._build_select_sql(size=10, _from=0) 158 | assert vals == [] 159 | assert "limit 10 offset 0" in sql 160 | 161 | async def test_build_select_sql_with_size_and_without_from(self): 162 | with pytest.raises(AssertionError): 163 | Article._build_select_sql(size=10) 164 | 165 | async def test_build_update_sql(self): 166 | data = { 167 | "id": "2017010101010", 168 | "tags": "aaa,bbb", 169 | "feature": "1" 170 | } 171 | article = Article(data) 172 | sql, args = Article._build_update_sql(article) 173 | assert sql == f"UPDATE {Article.TABLE} SET tags=?, feature=?, " \ 174 | f"updated_at=? WHERE id=?;" 175 | assert args[0] == "aaa,bbb" 176 | assert args[1] is True 177 | assert args[3] == "2017010101010" 178 | -------------------------------------------------------------------------------- /tests/test_article/test_article_exporter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from pytest_apistellar import prop_alias 4 | from blog.blog.article.article import Article 5 | from blog.blog.article.article_exporter import ArticleExporter 6 | 7 | article = prop_alias("blog.blog.article.article.Article") 8 | 9 | 10 | @pytest.mark.asyncio 11 | class TestArticleExporter(object): 12 | async def test__choice_function(self): 13 | article = Article(id="me") 14 | article_exporter = ArticleExporter(article, "", "") 15 | assert article_exporter._choice_function() == article_exporter.export_me 16 | article = Article(id="xxx") 17 | article_exporter = ArticleExporter(article, "", "") 18 | assert article_exporter._choice_function() == article_exporter.export_other 19 | 20 | @article("right_code", ret_val=False) 21 | async def test_export_me_code_not_right(self): 22 | article = Article(id="me") 23 | article_exporter = ArticleExporter(article, "", "") 24 | with pytest.raises(AssertionError): 25 | await article_exporter.export_me() 26 | 27 | @article("right_code", ret_val=True) 28 | async def test_export_me_code_right(self): 29 | article = Article(id="me", tags=["a", "b"], 30 | title="b", description="11", author="ma") 31 | article.format() 32 | article_exporter = ArticleExporter(article, "", "") 33 | article_file = await article_exporter.export_me() 34 | assert article_file.filename == "b.pdf" 35 | assert len(article_file.buffer) > 10 36 | 37 | @article("right_code", ret_val=True) 38 | async def test__replace_url(self): 39 | article_exporter = ArticleExporter("", "", "http://www.baidu.com/abc") 40 | html = '' \ 41 | '' 42 | html = article_exporter._replace_url(html) 43 | assert html.count("http://www.baidu.com/cut" 44 | "?width=60&height=20&url=http://img.shields.io/aaa") 45 | assert html.count("http://www.amazon.com/01.jpg") 46 | 47 | @article("right_code", ret_val=False) 48 | async def test_export_other(self): 49 | article = Article(id="1234", tags=["a", "b"], article="1111", 50 | title="b", description="11", author="ma") 51 | article_exporter = ArticleExporter(article, "", "") 52 | article_file = await article_exporter.export_other() 53 | assert article_file.filename == "b.md" 54 | assert article_file.buffer.count(b"comment") == 4 -------------------------------------------------------------------------------- /tests/test_article/test_service.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pytest 3 | 4 | from io import BytesIO 5 | from zipfile import ZipFile 6 | from apistellar import settings 7 | from collections import defaultdict 8 | from pytest_apistellar import prop_alias 9 | from toolkit.settings import SettingsLoader 10 | 11 | from blog.blog.article.service import ArticleService, Article 12 | from blog.blog.article.article_exporter import ArticleFile 13 | arti_ser = prop_alias("blog.blog.article.service.ArticleService") 14 | article = prop_alias("blog.blog.article.article.Article") 15 | 16 | 17 | @arti_ser("settings", ret_val=SettingsLoader().load("blog.settings")) 18 | @pytest.mark.asyncio 19 | class TestService(object): 20 | 21 | @article("load", ret_val=Article(id="20181010101010", 22 | article="![](http://www.baidu.com/)")) 23 | async def test_get(self): 24 | article = await ArticleService().get("20181010101010") 25 | assert article["first_img"] == "http://www.baidu.com/" 26 | 27 | @pytest.mark.prop( 28 | "blog.blog.article.article_exporter.ArticleExporter.export", 29 | ret_val=ArticleFile("test.pdf", b"aaaaa")) 30 | async def test_export(self): 31 | file_resp = await ArticleService().export([1], "", "") 32 | zip_file = BytesIO(file_resp.content) 33 | zf = ZipFile(zip_file) 34 | for file in zf.infolist(): 35 | assert file.filename == "test.pdf" 36 | assert file.file_size == 5 37 | 38 | @article("update") 39 | async def test_modify(self): 40 | article = Article(article="

    aaa

    ") 41 | await ArticleService().modify(article, "http://www.baidu.com/") 42 | assert article.article == "[comment]: (![]" \ 43 | "(http://www.baidu.com/))\n# aaa\n\n" 44 | 45 | @article("update") 46 | async def test_update(self): 47 | article = Article(id="20180101010101") 48 | await ArticleService().update(article) 49 | assert article.to_dict() == {"id": "20180101010101"} 50 | 51 | @article("save") 52 | async def test_upload_without_title(self, join_root_dir): 53 | f = open(join_root_dir("test_data/一键生成API文档.md"), "rb") 54 | f.filename = os.path.basename(f.name) 55 | article = Article(title="", article=f) 56 | await ArticleService().upload(article) 57 | assert article.title == "一键生成API文档" 58 | f.seek(0) 59 | assert article.article == f.read().decode() 60 | 61 | @article("save") 62 | async def test_upload_with_title(self, join_root_dir): 63 | f = open(join_root_dir("test_data/一键生成API文档.md"), "rb") 64 | article = Article(title="abc", article=f) 65 | await ArticleService().upload(article) 66 | assert article.title == "abc" 67 | 68 | @article("remove") 69 | async def test_remove(self): 70 | article = Article(id="20180101010101") 71 | await ArticleService().delete(article) 72 | assert article.to_dict() == {"id": "20180101010101"} 73 | 74 | @article("load", ret_val=Article(id="me", 75 | article="![](http://www.baidu.com/)")) 76 | async def test_about_with_article(self): 77 | article = await ArticleService().about("me") 78 | assert article["first_img"] == "http://www.baidu.com/" 79 | assert article["article"] == '

    ' 80 | 81 | @article("load", ret_val=Article()) 82 | @article("save") 83 | @pytest.mark.env(AUTHOR="test") 84 | async def test_about_without_article(self): 85 | id = "me" 86 | article = await ArticleService().about(id) 87 | assert article["first_img"] == "" 88 | assert article["author"] == "test" 89 | assert article["tags"] == id 90 | assert article["description"] == id 91 | assert article["feature"] is False 92 | assert article["title"] == id 93 | assert article["show"] is False 94 | assert article["article"] == "

    " + id + "

    " 95 | assert "updated_at" in article 96 | assert "created_at" in article 97 | 98 | @article("search", ret_factory= 99 | lambda *args, **kwargs: [Article(article="![](http://www.baidu.com/)")]) 100 | @article("get_total_tags", ret_val=(1, defaultdict(a=1, b=2))) 101 | async def test_show(self): 102 | rs = await ArticleService().show("", 0, 10, False) 103 | article = dict() 104 | article["first_img"] = "http://www.baidu.com/" 105 | assert rs["count"] == 1 106 | assert rs["tags"] == [("b", 2), ("a", 1)] 107 | assert rs["feature_articles"][0] == article 108 | assert rs["articles"][0] == article 109 | 110 | @pytest.mark.prop("asyncio.unix_events." 111 | "_UnixSelectorEventLoop.run_in_executor", asyncable=True) 112 | async def test_cut(self): 113 | save_name = await ArticleService().cut( 114 | "http://www.baidu.com/", 0, 0, 1024, 768) 115 | assert save_name == os.path.join( 116 | settings["PROJECT_PATH"], "static/temp/1169ee22f8.png") 117 | -------------------------------------------------------------------------------- /tests/test_data/一键生成API文档.md: -------------------------------------------------------------------------------- 1 | # abcde -------------------------------------------------------------------------------- /tests/test_import/test_import.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from apistellar import settings 4 | from pytest_apistellar import prop_alias 5 | 6 | from blog.blog.import_ import Import, Article 7 | 8 | article = prop_alias("blog.blog.article.article.Article") 9 | 10 | 11 | @pytest.mark.asyncio 12 | class TestImport(object): 13 | 14 | async def test_retrieve_success(self): 15 | article = ["aaaa", "[comment]: (abcde)"] 16 | val = Import.retrieve("title", article) 17 | assert val == "abcde" 18 | assert len(article) == 1 19 | assert article[0] == "aaaa" 20 | 21 | async def test_retrieve_failed(self): 22 | article = ["aaaa"] 23 | val = Import.retrieve("title", article) 24 | assert val == "" 25 | 26 | @article("load", ret_val=Article()) 27 | @article("save") 28 | async def test_insert(self, join_root_dir): 29 | article = await Import.insert( 30 | join_root_dir("test_data/一键生成API文档.md")) 31 | assert article.title == "一键生成API文档" 32 | assert article.tags == [] 33 | assert article.author == settings["AUTHOR"] 34 | 35 | @article("load", ret_val=Article(title="已存在的文件", tags=["1"])) 36 | @article("save") 37 | async def test_insert_exists(self, join_root_dir): 38 | article = await Import.insert( 39 | join_root_dir("test_data/一键生成API文档.md")) 40 | assert article.title == "已存在的文件" 41 | assert article.tags == ["1"] 42 | -------------------------------------------------------------------------------- /tests/test_utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | import pytest 3 | 4 | from blog.blog.utils import code_generator, get_stored_code, gen_code 5 | 6 | from factories import get_code 7 | 8 | 9 | def test_gen_code(join_root_dir): 10 | code, last_time = gen_code(0, 3600, join_root_dir("test_data/")) 11 | assert len(code) == 6 12 | assert last_time - int(time.time()) < 1 13 | 14 | 15 | def test_gen_code_not_expired(join_root_dir): 16 | rs = gen_code(time.time(), 3600, join_root_dir("test_data/")) 17 | assert rs is None 18 | 19 | 20 | def test_get_stored_code(join_root_dir): 21 | code, last_time = get_stored_code(join_root_dir("test_data/")) 22 | assert len(code) == 6 23 | assert isinstance(last_time, int) 24 | 25 | 26 | @pytest.mark.prop("blog.blog.utils.get_stored_code", ret_val=("ABCDEF", 111)) 27 | @pytest.mark.prop("blog.blog.utils.gen_code") 28 | def test_code_generator_stored_not_expired(): 29 | gen = code_generator(10) 30 | assert next(gen) == "ABCDEF" 31 | 32 | 33 | # 通过打开一个假文件来触发一次OSError 34 | @pytest.mark.prop("blog.blog.utils.get_stored_code", 35 | ret_factory=lambda *args, **kwargs: open("/tmp/abc/abc/abc")) 36 | def test_code_generator_not_stored_expired(): 37 | gen = code_generator(10) 38 | assert next(gen) == get_code() 39 | -------------------------------------------------------------------------------- /tools/auto_commit.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import re 4 | import time 5 | 6 | from argparse import ArgumentParser 7 | regex = re.compile("(modified:)|(Untracked files)") 8 | 9 | 10 | def main(paths): 11 | for path in paths: 12 | os.chdir(path) 13 | process = os.popen("/usr/bin/git status") 14 | buffer = process.read() 15 | if regex.search(buffer): 16 | os.system("/usr/bin/git add .") 17 | os.system("/usr/bin/git commit -m 'Save: Data auto save'") 18 | os.system("/usr/bin/git push") 19 | 20 | 21 | if __name__ == "__main__": 22 | parser = ArgumentParser() 23 | parser.add_argument("path", nargs="+", help="Which path to run save check. ") 24 | parser.add_argument("-i", "--interval", type=int, help="Check interval. ") 25 | args = parser.parse_args() 26 | while True: 27 | time.sleep(args.interval) 28 | print("Save check") 29 | main(args.path) 30 | -------------------------------------------------------------------------------- /tools/build.sh: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ShichaoMa/blog/ccd55ac732e9ff1e7fcfa6f42c860597a25da24e/tools/build.sh -------------------------------------------------------------------------------- /tools/change_profile.py: -------------------------------------------------------------------------------- 1 | #!/root/.pyenv/shims/python 2 | # -*- coding:utf-8 -*- 3 | import re 4 | import os 5 | import sys 6 | 7 | conf = open("/etc/nginx/conf.d/blog.conf", "r") 8 | 9 | data = conf.read() 10 | 11 | data = re.sub(r"(?<=//)(.*?)(?=\:3031;)", sys.argv[1], data) 12 | 13 | conf.close() 14 | 15 | conf = open("/etc/nginx/conf.d/blog.conf", "w") 16 | 17 | conf.write(data) 18 | 19 | conf.close() 20 | 21 | reboot = open("/etc/remote_reboot.py", "r") 22 | 23 | data = reboot.read() 24 | 25 | data = re.sub("(--host )(.*)( --port 22)", "\g<1>%s\g<3>"%sys.argv[1], data) 26 | 27 | reboot.close() 28 | 29 | reboot = open("/etc/remote_reboot.py", "w") 30 | 31 | reboot.write(data) 32 | 33 | reboot.close() 34 | 35 | os.system("systemctl restart nginx") 36 | -------------------------------------------------------------------------------- /tools/cut.js: -------------------------------------------------------------------------------- 1 | const CDP = require('chrome-remote-interface'); 2 | const argv = require('minimist')(process.argv.slice(2)); 3 | const file = require('fs'); 4 | 5 | // CLI Args 6 | const url = argv.url || 'https://www.google.com'; 7 | const format = argv.format === 'jpeg' ? 'jpeg' : 'png'; 8 | const viewportWidth = argv.viewportWidth || 1440; 9 | const viewportHeight = argv.viewportHeight || 900; 10 | const delay = argv.delay || 0; 11 | const userAgent = argv.userAgent; 12 | const fullPage = argv.full; 13 | 14 | // Start the Chrome Debugging Protocol 15 | CDP(async function(client) { 16 | // Extract used DevTools domains. 17 | const {DOM, Emulation, Network, Page, Runtime} = client; 18 | 19 | // Enable events on domains we are interested in. 20 | await Page.enable(); 21 | await DOM.enable(); 22 | await Network.enable(); 23 | 24 | // If user agent override was specified, pass to Network domain 25 | if (userAgent) { 26 | await Network.setUserAgentOverride({userAgent}); 27 | } 28 | 29 | // Set up viewport resolution, etc. 30 | const deviceMetrics = { 31 | width: viewportWidth, 32 | height: viewportHeight, 33 | deviceScaleFactor: 0, 34 | mobile: false, 35 | fitWindow: false, 36 | }; 37 | await Emulation.setDeviceMetricsOverride(deviceMetrics); 38 | await Emulation.setVisibleSize({width: viewportWidth, height: viewportHeight}); 39 | 40 | // Navigate to target page 41 | await Page.navigate({url}); 42 | 43 | // Wait for page load event to take screenshot 44 | Page.loadEventFired(async () => { 45 | // If the `full` CLI option was passed, we need to measure the height of 46 | // the rendered page and use Emulation.setVisibleSize 47 | if (fullPage) { 48 | const {root: {nodeId: documentNodeId}} = await DOM.getDocument(); 49 | const {nodeId: bodyNodeId} = await DOM.querySelector({ 50 | selector: 'body', 51 | nodeId: documentNodeId, 52 | }); 53 | const {model: {height}} = await DOM.getBoxModel({nodeId: bodyNodeId}); 54 | 55 | await Emulation.setVisibleSize({width: viewportWidth, height: height}); 56 | // This forceViewport call ensures that content outside the viewport is 57 | // rendered, otherwise it shows up as grey. Possibly a bug? 58 | await Emulation.forceViewport({x: 0, y: 0, scale: 1}); 59 | } 60 | 61 | setTimeout(async function() { 62 | const screenshot = await Page.captureScreenshot({format}); 63 | const buffer = new Buffer(screenshot.data, 'base64'); 64 | file.writeFile('output.png', buffer, 'base64', function(err) { 65 | if (err) { 66 | console.error(err); 67 | } else { 68 | console.log('Screenshot saved'); 69 | } 70 | client.close(); 71 | }); 72 | }, delay); 73 | }); 74 | }).on('error', err => { 75 | console.error('Cannot connect to browser:', err); 76 | }); 77 | -------------------------------------------------------------------------------- /tools/ip_change.py: -------------------------------------------------------------------------------- 1 | #!/home/pi/.pyenv/shims/python 2 | # -*- coding:utf-8 -*- 3 | import sys 4 | import time 5 | import socket 6 | import requests 7 | import paramiko 8 | 9 | current_ip = None 10 | user = "root" 11 | password = sys.argv[2] 12 | port = 28553 13 | host = sys.argv[1] 14 | 15 | 16 | def change(ip): 17 | ssh = paramiko.SSHClient() 18 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 19 | ssh.connect(host, port, user, password) 20 | stdin, stdout, stderr = ssh.exec_command("./change_profile.py %s"%ip) 21 | error = stderr.read() 22 | out = stdout.read() 23 | print(error, out) 24 | ssh.close() 25 | return error == b"" 26 | 27 | 28 | def getip(): 29 | # sock = socket.create_connection(('ns1.dnspod.net', 6666)) 30 | # ip = sock.recv(16) 31 | # sock.close() 32 | # return ip.decode("utf-8") 33 | return requests.get("http://ip.42.pl/raw").text 34 | 35 | if __name__ == '__main__': 36 | while True: 37 | try: 38 | ip = getip() 39 | print(ip) 40 | if current_ip != ip: 41 | if change(ip): 42 | current_ip = ip 43 | except Exception as e: 44 | print(e) 45 | time.sleep(30) 46 | sys.stdout.flush() 47 | -------------------------------------------------------------------------------- /tools/stop.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import re 4 | 5 | from argparse import ArgumentParser 6 | 7 | 8 | def kill(pid, signal): 9 | try: 10 | os.kill(pid, signal) 11 | except ProcessLookupError: 12 | pass 13 | 14 | 15 | def main(cmd, signal): 16 | current_pid = os.getpid() 17 | pids = list() 18 | ppids = list() 19 | buffer_lines = os.popen("ps -ef|grep '%s'"%cmd).readlines() 20 | for line in buffer_lines: 21 | if line.strip(): 22 | ele = re.findall("(\S+)", line.strip()) 23 | if int(ele[1]) != current_pid: 24 | pids.append(int(ele[1])) 25 | ppids.append(int(ele[2])) 26 | if len(pids) > 3: 27 | for index, pid in enumerate(pids): 28 | if pid in ppids: 29 | kill(pid, signal) 30 | elif ppids[index] in pids: 31 | kill(pid, signal) 32 | if len(pids) < 3: 33 | print("No such process") 34 | else: 35 | for pid in pids: 36 | kill(pid, signal) 37 | 38 | 39 | if __name__ == "__main__": 40 | parse = ArgumentParser() 41 | parse.add_argument("-c", "--command", required=True, help="check command to stop. ") 42 | parse.add_argument("-s", "--signal", type=int, help="signal to send", default=15) 43 | args = parse.parse_args() 44 | main(args.command, args.signal) 45 | --------------------------------------------------------------------------------