* [docs] to be rendered in website * [docs] generating-docs.md * [fix] codacy * [docs] minor updates * [fix] routingversion-14
@@ -1,2 +1 @@ | |||
- Developer Tutorial [Videos](http://frappe.github.io/frappe/user/videos/) | |||
- Increased uploaded file size limit upto 10MB |
@@ -1,31 +1,9 @@ | |||
from __future__ import unicode_literals, absolute_import | |||
import click | |||
import os | |||
import os, shutil | |||
import frappe | |||
from frappe.commands import pass_context | |||
@click.command('write-docs') | |||
@pass_context | |||
@click.argument('app') | |||
@click.option('--target', default=None) | |||
@click.option('--local', default=False, is_flag=True, help='Run app locally') | |||
def write_docs(context, app, target=None, local=False): | |||
"Setup docs in target folder of target app" | |||
from frappe.utils.setup_docs import setup_docs | |||
if not target: | |||
target = os.path.abspath(os.path.join("..", "docs", app)) | |||
for site in context.sites: | |||
try: | |||
frappe.init(site=site) | |||
frappe.connect() | |||
make = setup_docs(app) | |||
make.make_docs(target, local) | |||
finally: | |||
frappe.destroy() | |||
@click.command('build-docs') | |||
@pass_context | |||
@click.argument('app') | |||
@@ -36,23 +14,26 @@ def write_docs(context, app, target=None, local=False): | |||
def build_docs(context, app, docs_version="current", target=None, local=False, watch=False): | |||
"Setup docs in target folder of target app" | |||
from frappe.utils import watch as start_watch | |||
if not target: | |||
target = os.path.abspath(os.path.join("..", "docs", app)) | |||
from frappe.utils.setup_docs import add_breadcrumbs_tag | |||
for site in context.sites: | |||
_build_docs_once(site, app, docs_version, target, local) | |||
if watch: | |||
def trigger_make(source_path, event_type): | |||
if "/templates/autodoc/" in source_path: | |||
_build_docs_once(site, app, docs_version, target, local) | |||
if "/docs/user/" in source_path: | |||
# user file | |||
target_path = frappe.get_app_path(target, 'www', 'docs', 'user', | |||
os.path.relpath(source_path, start=frappe.get_app_path(app, 'docs', 'user'))) | |||
shutil.copy(source_path, target_path) | |||
add_breadcrumbs_tag(target_path) | |||
elif ("/docs.css" in source_path | |||
or "/docs/" in source_path | |||
or "docs.py" in source_path): | |||
_build_docs_once(site, app, docs_version, target, local, only_content_updated=True) | |||
apps_path = frappe.get_app_path(app, "..", "..") | |||
apps_path = frappe.get_app_path(app) | |||
start_watch(apps_path, handler=trigger_make) | |||
def _build_docs_once(site, app, docs_version, target, local, only_content_updated=False): | |||
@@ -62,17 +43,16 @@ def _build_docs_once(site, app, docs_version, target, local, only_content_update | |||
frappe.init(site=site) | |||
frappe.connect() | |||
make = setup_docs(app) | |||
make = setup_docs(app, target) | |||
if not only_content_updated: | |||
make.build(docs_version) | |||
make.make_docs(target, local) | |||
#make.make_docs(target, local) | |||
finally: | |||
frappe.destroy() | |||
commands = [ | |||
build_docs, | |||
write_docs, | |||
] |
@@ -2,12 +2,10 @@ | |||
from __future__ import unicode_literals | |||
docs_version = "7.x.x" | |||
source_link = "https://github.com/frappe/frappe" | |||
docs_base_url = "https://frappe.github.io/frappe" | |||
headline = "Superhero Web Framework" | |||
sub_heading = "Build extensions to ERPNext or make your own app" | |||
docs_base_url = "/docs" | |||
headline = "Frappé Framework" | |||
sub_heading = "Tutorials, API documentation and Model Reference" | |||
hide_install = True | |||
long_description = """Frappe is a full stack web application framework written in Python, | |||
Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext | |||
@@ -25,7 +23,7 @@ to ERPNext. | |||
Frappe Framework was designed to build [ERPNext](https://erpnext.com), open source | |||
ERP for managing small and medium sized businesses. | |||
[Get started with the Tutorial](https://frappe.github.io/frappe/user/) | |||
[Get started with the Tutorial](/docs/user/) | |||
""" | |||
google_analytics_id = 'UA-8911157-23' | |||
@@ -1,10 +0,0 @@ | |||
<!-- title: Table of Contents --> | |||
<h1>Table of Contents</h1> | |||
<br> | |||
{% include "templates/includes/full_index.html" %} | |||
<!-- autodoc --> | |||
<!-- jinja --> | |||
<!-- no-breadcrumbs --> |
@@ -1,9 +0,0 @@ | |||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | |||
# See license.txt | |||
from __future__ import unicode_literals | |||
import frappe | |||
from frappe.website.utils import get_full_index | |||
def get_context(context): | |||
context.full_index = get_full_index() |
@@ -1,57 +0,0 @@ | |||
<!-- title: Frappe Framework: Documentation --> | |||
<!-- description: Superhero Web Framework --> | |||
<!-- no-breadcrumbs --> | |||
<style> | |||
</style> | |||
<!-- start-hero --> | |||
<div class="splash"> | |||
<div class="container"> | |||
<div class="col-sm-10 col-sm-offset-1"> | |||
<div class="jumbotron"> | |||
<h1>Superhero Web Framework</h1> | |||
<p>Build extensions to ERPNext or make your own app</p> | |||
</div> | |||
<div class="section" style="padding-top: 0px; margin-top: -30px;"> | |||
<div class="fake-browser-frame"> | |||
<img class="img-responsive browser-image feature-image" | |||
src="assets/img/home.png"> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- end-hero --> | |||
<div class="container"> | |||
<div class="col-sm-10 col-sm-offset-1"> | |||
<div class="section"> | |||
<p>Frappe is a full stack web application framework written in Python, | |||
Javascript, HTML/CSS with MySQL as the backend. It was built for ERPNext | |||
but is pretty generic and can be used to build database driven apps.</p> | |||
<p>The key differece in Frappe compared to other frameworks is that Frappe | |||
is that meta-data is also treated as data and is used to build front-ends | |||
very easily. Frappe comes with a full blown admin UI called the <strong>Desk</strong> | |||
that handles forms, navigation, lists, menus, permissions, file attachment | |||
and much more out of the box.</p> | |||
<p>Frappe also has a plug-in architecture that can be used to build plugins | |||
to ERPNext.</p> | |||
<p>Frappe Framework was designed to build <a href="https://erpnext.com">ERPNext</a>, open source | |||
ERP for managing small and medium sized businesses.</p> | |||
<p><a href="https://frappe.github.io/frappe/user/">Get started with the Tutorial</a></p> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- autodoc --> | |||
<!-- jinja --> |
@@ -1,6 +0,0 @@ | |||
assets | |||
user | |||
contents | |||
current | |||
install | |||
license |
@@ -1,30 +0,0 @@ | |||
<!-- title: Frappe Framework Installation --> | |||
# Installation | |||
Frappe Framework is based on the <a href="https://frappe.io">Frappe Framework</a>, a full stack web framework based on Python, MariaDB, Redis, Node. | |||
To intall Frappe Framework, you will have to install the <a href="https://github.com/frappe/bench">Frappe Bench</a>, the command-line, package manager and site manager for Frappe Framework. For more details, read the Bench README. | |||
After you have installed Frappe Bench, go to you bench folder, which is `frappe.bench` by default and setup **frappe**. | |||
bench get-app frappe {{ source_link }} | |||
Then create a new site to install the app. | |||
bench new-site mysite | |||
This will create a new folder in your `/sites` directory and create a new database for this site. | |||
Next, install frappe in this site | |||
bench --site mysite install-app frappe | |||
To run this locally, run | |||
bench start | |||
Fire up your browser and go to http://localhost:8000 and you should see the login screen. Login as **Administrator** and **admin** (or the password you set at the time of `new-site`) and you are set. | |||
<!-- jinja --> | |||
<!-- autodoc --> |
@@ -1,16 +0,0 @@ | |||
<!-- title: License MIT --> | |||
<h1>MIT</h1> | |||
<p>The MIT License (MIT)</p> | |||
<p>Copyright (c) 2016 Frappe Technologies Pvt. Ltd.</p> | |||
<p>Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:</p> | |||
<p>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</p> | |||
<p>THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.</p> | |||
<!-- autodoc --> |
@@ -1,18 +1,17 @@ | |||
# Generating Documentation Website for your App | |||
Frappe version 6.7 onwards includes a full-blown documentation generator so that you can easily create a website for your app that has both user docs and developers docs (auto-generated). These pages are generated as static HTML pages so that you can add them as GitHub pages. | |||
Frappe version 6.7 onwards includes a full-blown documentation generator so that you can easily create a website for your app that has both user docs and developers docs (auto-generated). | |||
Version 8.7 onwards, these will be generated in a target app. | |||
## Writing Docs | |||
### 1. Setting up docs | |||
#### 1.1. Setup `docs.py` | |||
The first step is to setup the docs folder. For that you must create a new file in your app `config/docs.py` if it is not auto-generated. In your `docs.py` file, add the following module properties. | |||
source_link = "https://github.com/[orgname]/[reponame]" | |||
docs_base_url = "https://[orgname].github.io/[reponame]" | |||
headline = "This is what my app does" | |||
sub_heading = "Slightly more details with key features" | |||
long_description = """(long description in markdown)""" | |||
@@ -29,16 +28,6 @@ The first step is to setup the docs folder. For that you must create a new file | |||
pass | |||
#### 1.2. Generate `/docs` | |||
To generate the docs for the `current` version, go to the command line and write | |||
bench --site [site] build-docs [appname] | |||
If you want to maintain versions of your docs, then you can add a version number instead of `current` | |||
This will create a `/docs` folder in your app. | |||
### 2. Add User Documentation | |||
To add user documentation, add folders and pages in your `/docs/user` folder in the same way you would build a website pages in the `www` folder. | |||
@@ -54,61 +43,28 @@ Some quick tips: | |||
While linking make sure you add `{{ docs_base_url }}` to all your links. | |||
{% raw %}<a href="{{ docs_base_url }}/user/link/to/page.html">Link Description</a>{% endraw %} | |||
{% raw %}<a href="/docs/user/link/to/page.html">Link Description</a>{% endraw %} | |||
### 4. Adding Images | |||
You can add images in the `/docs/assets` folder. You can add links to the images as follows: | |||
{% raw %}<img src="{{ docs_base_url }}/assets/img/my-img/gif" class="screenshot">{% endraw %} | |||
--- | |||
## Setting up output docs | |||
The output docs are generated in your `docs/appname` folder using the `write-docs` command. | |||
--- | |||
## Viewing Locally | |||
To test your docs locally, add a `--local` option to the `write-docs` command. | |||
bench --site [site] write-docs [appname] --local | |||
Then it will build urls so that you can view these files locally. To view them locally in your browser, you can use the Python SimpleHTTPServer | |||
Run this from your `docs/myapp` folder: | |||
python -m SimpleHTTPServer 8080 | |||
{% raw %}<img src="/docs/assets/img/my-img/gif" class="screenshot">{% endraw %} | |||
--- | |||
## Publishing to GitHub Pages | |||
To publish your docs on GitHub pages, you will have to create an empty and orphan branch in your repository called `gh-pages` and push your documentation there. | |||
## Building Docs | |||
1. To easily publish your docs on gh-pages, commit and push your `apps/docs` folder on you master branch first. | |||
2. The `/docs` generation will also generate a `/docs` folder in your bench, parallel to your `/sites` folder. e.g. `/frappe-bench/docs` | |||
3. Generate you documentation using the `write-docs` command. | |||
4. Go to your docs folder `cd docs/myapp` | |||
5. Checkout the gh-pages branch `git checkout --orphan gh-pages` | |||
6. Push your documentation to Github. | |||
You must create a new app that will have the output of the docs, which is called the "target" app. For example, the docs for ERPNext are hosted at erpnext.org, which is based on the app "foundation". You can create a new app just to push docs of any other app. | |||
Note > The branch name `gh-pages` is only if you are using GitHub. If you are hosting this on any other static file server, you can create any other orphan branch instead. | |||
To output docs to another app, | |||
Putting it all together: | |||
bench --site [site] build-docs [app] --target [target_app] | |||
# build the apps/docs folder and write the compiled docs at docs/appname | |||
bench --site [site] build-docs [appname] | |||
This will create a new folder `/docs` inside the `www` folder of the target app and generate automatic docs (from code), model references and copy user docs and assets. | |||
# commit to the gh-pages branch (for GitHub Pages) | |||
cd docs/appname | |||
git checkout --orphan gh-pages | |||
git remote add origin [remote git repository] | |||
git add * | |||
git commit -m "Documentation Initialization" | |||
git push origin gh-pages | |||
To view the docs, just go the the `/docs` url on your target app. Example: | |||
To check your documentation online go to: https://[orgname].github.io/[reponame] | |||
https://erpnext.org/docs |
@@ -39,7 +39,7 @@ You can define complex queries such as: | |||
### 4. Advanced (adding filters) | |||
If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.github.io/frappe/user/en/guides/reports-and-printing/how-to-make-script-reports) by adding a `.js` file in your query report folder. To include filters in your query, use `%(filter_key)s` where your filter value will be shown. | |||
If you are making a standard report, you can add filters in your query report just like [script reports](https://frappe.io/docs/user/en/guides/reports-and-printing/how-to-make-script-reports) by adding a `.js` file in your query report folder. To include filters in your query, use `%(filter_key)s` where your filter value will be shown. | |||
For example | |||
@@ -4,9 +4,9 @@ You can create tabulated reports using server side scripts by creating a new Rep | |||
> Note: You will need Administrator Permissions for this. | |||
Since these reports give you unrestricted access via Python scripts, they can only be created by Administrators. The script part of the report becomes a part of the repository of the application. If you have not created an app, [read this](https://frappe.github.io/frappe/user/en/guides/app-development/). | |||
Since these reports give you unrestricted access via Python scripts, they can only be created by Administrators. The script part of the report becomes a part of the repository of the application. If you have not created an app, [read this](https://frappe.io/docs/user/en/guides/app-development/). | |||
> Note: You must be in [Developer Mode](https://frappe.github.io/frappe/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this | |||
> Note: You must be in [Developer Mode](https://frappe.io/docs/user/en/guides/app-development/how-enable-developer-mode-in-frappe) to do this | |||
### 1. Create a new Report | |||
@@ -6,11 +6,12 @@ | |||
#### 1. Python | |||
Frappe uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappe. | |||
Frappe uses Python (v2.7) for server-side programming. It is highly recommended to learn Python before you start building apps with Frappe. | |||
To write quality server-side code, you must also include automated tests. | |||
Resources: | |||
1. [Codecademy Tutorial for Python](https://www.codecademy.com/learn/python) | |||
1. [Official Python Tutorial](https://docs.python.org/2.7/tutorial/index.html) | |||
1. [Basics of Test-driven development](http://code.tutsplus.com/tutorials/beginning-test-driven-development-in-python--net-30137) | |||
@@ -19,11 +20,12 @@ Resources: | |||
#### 2. MariaDB / MySQL | |||
To create database-driven apps with Frappe, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries. | |||
To create database-driven apps with Frappe, you must understand the basics of database management, like how to install, login, create new databases, and basic SQL queries. | |||
Resources: | |||
1. [Codecademy Tutorial for SQL](https://www.codecademy.com/learn/learn-sql) | |||
1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) | |||
1. [A basic MySQL tutorial by DigitalOcean](https://www.digitalocean.com/community/tutorials/a-basic-mysql-tutorial) | |||
1. [Getting started with MariaDB](https://mariadb.com/kb/en/mariadb/documentation/getting-started/) | |||
--- | |||
@@ -33,6 +35,7 @@ Resources: | |||
If you want to build user interfaces using Frappe, you will need to learn basic HTML / CSS and the Boostrap CSS Framework. | |||
Resources: | |||
1. [Codecademy Tutorial for HTML/CSS](https://www.codecademy.com/learn/learn-html-css) | |||
1. [Getting started with Bootstrap](https://getbootstrap.com/getting-started/) | |||
@@ -44,6 +47,7 @@ To customize forms and create rich user interfaces, you should learn JavaScript | |||
Resources: | |||
1. [Codecademy Tutorial for JavaScript](https://www.codecademy.com/learn/learn-javascript) | |||
1. [Codecademy Tutorial for jQuery](https://www.codecademy.com/learn/jquery) | |||
--- | |||
@@ -53,6 +57,7 @@ Resources: | |||
If you are customizing Print templates or Web pages, you need to learn the Jinja Templating language. It is an easy way to create dynamic web pages (HTML). | |||
Resources: | |||
1. [Primer on Jinja Templating](https://realpython.com/blog/python/primer-on-jinja-templating/) | |||
1. [Official Documentation](http://jinja.pocoo.org/) | |||
@@ -63,6 +68,7 @@ Resources: | |||
Learn how to contribute back to an open source project using Git and GitHub, two great tools to help you manage your code and share it with others. | |||
Resources: | |||
1. [Basic Git Tutorial](https://try.github.io) | |||
2. [How to contribute to Open Source](https://opensource.guide/how-to-contribute/) | |||
@@ -258,7 +258,7 @@ a[disabled="disabled"]:hover { | |||
} | |||
.link-btn { | |||
position: absolute; | |||
top: 2px; | |||
top: 3px; | |||
right: 4px; | |||
border-radius: 2px; | |||
padding: 3px; | |||
@@ -597,9 +597,3 @@ a.edit:visited, | |||
.page-content-wrapper > .row .col-sm-4 { | |||
display: none; | |||
} | |||
.screenshot { | |||
border: 2px solid #d1d8dd; | |||
box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); | |||
margin: 15px 0px; | |||
max-width: 100%; | |||
} |
@@ -430,6 +430,9 @@ h6 a { | |||
color: inherit !important; | |||
text-decoration: none; | |||
} | |||
li { | |||
line-height: 1.7em; | |||
} | |||
.navbar-brand { | |||
max-width: none; | |||
} | |||
@@ -503,6 +506,9 @@ h6 a { | |||
min-height: 140px; | |||
border-top: 1px solid #EBEFF2; | |||
} | |||
.page_content { | |||
padding-bottom: 30px; | |||
} | |||
.carousel-control .icon { | |||
position: absolute; | |||
top: 50%; | |||
@@ -599,7 +605,7 @@ fieldset { | |||
} | |||
.web-sidebar .sidebar-item { | |||
margin: 0px; | |||
padding: 12px 0px; | |||
padding-bottom: 12px; | |||
border: none; | |||
color: #8D99A6; | |||
font-size: 12px; | |||
@@ -607,21 +613,14 @@ fieldset { | |||
.web-sidebar .sidebar-item .badge { | |||
font-weight: normal; | |||
} | |||
.web-sidebar .sidebar-item:first-child { | |||
padding-top: 10px; | |||
} | |||
.web-sidebar .sidebar-item:last-child { | |||
padding-bottom: 10px; | |||
} | |||
.web-sidebar .sidebar-item a { | |||
color: #8D99A6; | |||
color: #36414C !important; | |||
} | |||
.web-sidebar .sidebar-item a.active { | |||
color: #36414C !important; | |||
font-weight: 500 !important; | |||
} | |||
.web-sidebar .sidebar-items { | |||
margin-top: -10px; | |||
margin-bottom: 30px; | |||
} | |||
.web-sidebar .sidebar-items .title { | |||
@@ -675,69 +674,25 @@ fieldset { | |||
.web-list-item:last-child { | |||
border-bottom: 0px; | |||
} | |||
.blog-info { | |||
text-align: center; | |||
margin-top: 30px; | |||
} | |||
.post-description { | |||
padding-bottom: 8px; | |||
} | |||
.post-description p { | |||
margin-bottom: 8px; | |||
} | |||
.blog-footer { | |||
padding: 5px 15px; | |||
border-top: 1px solid #EBEFF2; | |||
margin: 0px -15px -20px -15px; | |||
} | |||
.blog-list-content .website-list .result { | |||
.website-list .result { | |||
border: 0px; | |||
} | |||
.blog-list-content .web-list-item:hover { | |||
.web-list-item:hover { | |||
background: transparent; | |||
} | |||
.blog-category { | |||
letter-spacing: 0.5px; | |||
text-align: center; | |||
margin-bottom: 30px; | |||
} | |||
.author { | |||
letter-spacing: 0.5px; | |||
border-bottom: 1px solid #EBEFF2; | |||
padding-bottom: 30px; | |||
} | |||
.blogger { | |||
padding-top: 0px; | |||
padding-bottom: 50px; | |||
} | |||
.blog-dot:before { | |||
.spacer-dot:before { | |||
padding-right: 8px; | |||
padding-left: 8px; | |||
content: "\2022"; | |||
} | |||
.blog-list-item { | |||
margin-top: 30px; | |||
margin-bottom: 30px; | |||
} | |||
.blog-list-item .blog-header { | |||
font-size: 1.6em; | |||
} | |||
.blog-header { | |||
font-weight: 700; | |||
font-size: 2em; | |||
} | |||
.add-comment-section { | |||
padding-bottom: 30px; | |||
} | |||
.blog-comments { | |||
position: relative; | |||
border-top: 1px solid #d1d8dd; | |||
} | |||
.blog-comment-row { | |||
.comment-row { | |||
margin: 0px -15px; | |||
padding: 15px; | |||
} | |||
.blog-comment-row:last-child { | |||
.comment-row:last-child { | |||
margin-bottom: 30px; | |||
border-bottom: 0px; | |||
} | |||
@@ -837,7 +792,7 @@ a.active { | |||
} | |||
.sidebar-block, | |||
.page-content { | |||
padding-top: 50px; | |||
padding-top: 30px; | |||
padding-bottom: 50px; | |||
} | |||
.your-account-info { | |||
@@ -871,19 +826,6 @@ a.active { | |||
li.footer-child-item { | |||
margin: 15px 0px; | |||
} | |||
.blog-info { | |||
text-align: center; | |||
margin-top: 30px; | |||
} | |||
.blog-text { | |||
padding-top: 50px; | |||
padding-bottom: 50px; | |||
font-size: 18px; | |||
line-height: 1.5; | |||
} | |||
.blog-text p { | |||
margin-bottom: 30px; | |||
} | |||
.comment-view { | |||
padding-bottom: 30px; | |||
} | |||
@@ -927,7 +869,7 @@ li.footer-child-item { | |||
overflow: hidden; | |||
} | |||
.vert-line > div + div { | |||
border-left: 1px solid #EBEFF2; | |||
border-left: 1px solid #d1d8dd; | |||
} | |||
.vert-line > div { | |||
padding-bottom: 2000px; | |||
@@ -994,3 +936,13 @@ li.footer-child-item { | |||
padding: 10px; | |||
border-radius: 4px; | |||
} | |||
.docfields pre { | |||
background-color: transparent; | |||
border: none; | |||
} | |||
.screenshot { | |||
border: 1px solid #d1d8dd; | |||
box-shadow: 1px 1px 7px rgba(0, 0, 0, 0.15); | |||
margin: 15px 0px; | |||
max-width: 100%; | |||
} |
@@ -141,17 +141,17 @@ frappe.msgprint = function(msg, title) { | |||
msg = replace_newlines(data.message); | |||
} | |||
var msg_exists = false; | |||
if(data.clear) { | |||
msg_dialog.msg_area.empty(); | |||
var msg_exists = false; | |||
} else { | |||
var msg_exists = msg_dialog.msg_area.html(); | |||
msg_exists = msg_dialog.msg_area.html(); | |||
} | |||
if(data.title || !msg_exists) { | |||
// set title only if it is explicitly given | |||
// and no existing title exists | |||
msg_dialog.set_title(data.title || __('Message')) | |||
msg_dialog.set_title(data.title || __('Message')); | |||
} | |||
// show / hide indicator | |||
@@ -24,7 +24,7 @@ a[disabled="disabled"] { | |||
.link-btn { | |||
position: absolute; | |||
top: 2px; | |||
top: 3px; | |||
right: 4px; | |||
border-radius: 2px; | |||
padding: 3px; | |||
@@ -403,10 +403,4 @@ a.edit, a.edit:hover, a.edit:focus, a.edit:visited, .edit-container .icon { | |||
} | |||
} | |||
.screenshot { | |||
border: 2px solid @border-color; | |||
box-shadow: 1px 1px 7px rgba(0,0,0,0.15); | |||
margin: 15px 0px; | |||
max-width: 100%; | |||
} | |||
@@ -29,6 +29,10 @@ h1, h2, h3, h4, h5, h6 { | |||
} | |||
li { | |||
line-height: 1.7em; | |||
} | |||
.navbar-brand { | |||
max-width: none; | |||
@@ -120,6 +124,10 @@ h1, h2, h3, h4, h5, h6 { | |||
border-top: 1px solid @light-border-color; | |||
} | |||
.page_content { | |||
padding-bottom: 30px; | |||
} | |||
.carousel-control .icon { | |||
position: absolute; | |||
top: 50%; | |||
@@ -235,7 +243,7 @@ fieldset { | |||
.sidebar-item { | |||
margin: 0px; | |||
padding: 12px 0px; | |||
padding-bottom: 12px; | |||
border: none; | |||
color: @text-muted; | |||
font-size: 12px; | |||
@@ -246,25 +254,17 @@ fieldset { | |||
} | |||
.sidebar-item:first-child { | |||
padding-top: 10px; | |||
} | |||
.sidebar-item:last-child { | |||
padding-bottom: 10px; | |||
} | |||
.sidebar-item a { | |||
color: @text-muted; | |||
color: @text-color !important; | |||
} | |||
.sidebar-item a.active { | |||
color: @text-color !important; | |||
font-weight:500 !important; | |||
font-weight: 500 !important; | |||
} | |||
.sidebar-items { | |||
// margin-top:30px; | |||
margin-top: -10px; | |||
margin-bottom:30px; | |||
.title{ | |||
font-size: 14px; | |||
@@ -331,92 +331,30 @@ fieldset { | |||
border-bottom: 0px; | |||
} | |||
// .web-list-item:hover { | |||
// background: @panel-bg; | |||
// } | |||
.blog-info { | |||
text-align:center; | |||
margin-top: 30px; | |||
} | |||
.post-description { | |||
padding-bottom: 8px; | |||
p { | |||
margin-bottom: 8px; | |||
} | |||
} | |||
.blog-footer { | |||
padding: 5px 15px; | |||
border-top: 1px solid @light-border-color; | |||
margin: 0px -15px -20px -15px; | |||
} | |||
.blog-list-content .website-list .result { | |||
.website-list .result { | |||
border: 0px; | |||
} | |||
.blog-list-content .web-list-item:hover { | |||
.web-list-item:hover { | |||
background: transparent; | |||
} | |||
.blog-category { | |||
letter-spacing: 0.5px; | |||
text-align: center; | |||
margin-bottom: 30px; | |||
} | |||
.author { | |||
letter-spacing: 0.5px; | |||
border-bottom: 1px solid @light-border-color; | |||
padding-bottom:30px; | |||
} | |||
.blogger { | |||
padding-top: 0px; | |||
padding-bottom: 50px; | |||
} | |||
.blog-dot:before{ | |||
.spacer-dot:before{ | |||
padding-right:8px; | |||
padding-left:8px; | |||
content:"\2022"; | |||
} | |||
.blog-list-item { | |||
margin-top: 30px; | |||
margin-bottom: 30px; | |||
.blog-header { | |||
font-size: 1.6em; | |||
} | |||
} | |||
.blog-header { | |||
font-weight: 700; | |||
font-size: 2em; | |||
} | |||
.add-comment-section { | |||
padding-bottom: 30px; | |||
} | |||
.blog-comments, | |||
.help-article-comments { | |||
} | |||
.blog-comments { | |||
position: relative; | |||
border-top: 1px solid @border-color; | |||
} | |||
.blog-comment-row { | |||
.comment-row { | |||
margin: 0px -15px; | |||
padding: 15px; | |||
} | |||
.blog-comment-row:last-child { | |||
.comment-row:last-child { | |||
margin-bottom: 30px; | |||
border-bottom: 0px; | |||
} | |||
@@ -540,7 +478,7 @@ a.active { | |||
} | |||
.sidebar-block, .page-content { | |||
padding-top: 50px; | |||
padding-top: 30px; | |||
padding-bottom: 50px; | |||
} | |||
.your-account-info { | |||
@@ -584,21 +522,6 @@ li.footer-child-item { | |||
margin: 15px 0px; | |||
} | |||
.blog-info { | |||
text-align:center; | |||
margin-top: 30px; | |||
} | |||
.blog-text { | |||
padding-top: 50px; | |||
padding-bottom: 50px; | |||
font-size: 18px; | |||
line-height: 1.5; | |||
p { | |||
margin-bottom: 30px; | |||
} | |||
} | |||
.comment-view { | |||
padding-bottom: 30px; | |||
} | |||
@@ -643,7 +566,7 @@ li.footer-child-item { | |||
} | |||
.vert-line {overflow:hidden;} | |||
.vert-line>div+div{border-left:1px solid @light-border-color;} | |||
.vert-line>div+div{border-left:1px solid @border-color;} | |||
.vert-line>div{ | |||
padding-bottom:2000px; margin-bottom:-2000px;} | |||
@@ -717,4 +640,19 @@ li.footer-child-item { | |||
border: 1px solid @border-color; | |||
padding: 10px; | |||
border-radius: 4px; | |||
} | |||
} | |||
// docs | |||
.docfields { | |||
pre { | |||
background-color: transparent; | |||
border: none; | |||
} | |||
} | |||
.screenshot { | |||
border: 1px solid @border-color; | |||
box-shadow: 1px 1px 7px rgba(0,0,0,0.15); | |||
margin: 15px 0px; | |||
max-width: 100%; | |||
} |
@@ -7,9 +7,9 @@ | |||
{{ source_link(app, app.name, True) }} | |||
</div> | |||
<table class="table table-bordered"> | |||
<table class="table table-bordered" style='max-width: 400px;'> | |||
<tr> | |||
<td style="width: 20%"> | |||
<td style="width: 40%"> | |||
App Name | |||
</td> | |||
<td> | |||
@@ -37,10 +37,10 @@ | |||
<h3>Contents</h3> | |||
<ul> | |||
<li> | |||
<a href="models">Models (DocTypes)</a> | |||
<a href="/docs/current/models">Models (DocTypes)</a> | |||
</li> | |||
<li> | |||
<a href="api">Server-side API</a> | |||
<a href="/docs/current/api">Server-side API</a> | |||
</li> | |||
</ul> | |||
@@ -5,57 +5,15 @@ | |||
{% if app.style %} {{ app.style }} {% endif %} | |||
</style> | |||
<!-- start-hero --> | |||
<div class="splash"> | |||
<div class="container"> | |||
<div class="col-sm-10 col-sm-offset-1"> | |||
<div class="jumbotron"> | |||
<h1>{{ app.headline }}</h1> | |||
<p>{{ app.sub_heading }}</p> | |||
</div> | |||
<div class="section" style="padding-top: 0px; margin-top: -30px;"> | |||
<div class="fake-browser-frame"> | |||
<img class="img-responsive browser-image feature-image" | |||
src="assets/img/home.png"> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
<!-- end-hero --> | |||
{% if app.long_description %} | |||
<div class="container"> | |||
<div class="col-sm-10 col-sm-offset-1"> | |||
<div class="section"> | |||
{{ app.long_description|markdown }} | |||
</div> | |||
</div> | |||
</div> | |||
{% if app.headline %} | |||
<h1>{{ app.headline }}</h1> | |||
{% endif %} | |||
{% if app.sub_heading %} | |||
<h3 style='margin-top: 10px; margin-bottom: 30px;'>{{ app.sub_heading }}</h3> | |||
{% endif %} | |||
{% if not app.hide_install %} | |||
<div class="container"> | |||
<div class="col-sm-10 col-sm-offset-1"> | |||
<div class="section" id="install"> | |||
<h2>Install</h2> | |||
<h4>From your site</h4> | |||
<p>To install this app, login to your site and click on "Installer". Search for <b>{{ app.title }}</b> and click on "Install"</p> | |||
<h4>Using Bench</h4> | |||
<p>Go to your bench folder and setup the new app</p> | |||
<pre><code class="sh">$ bench get-app {{app.name}} {{app.source_link}} | |||
$ bench new-site testsite | |||
$ bench --site testsite install-app {{app.name}}</code></pre> | |||
<p>Login to your site to configure the app.</p> | |||
<p><a href="install.html">Detailed Installation Steps</a></p> | |||
</div> | |||
<div class="section"> | |||
<h2>Author</h2> | |||
<p>{{ app.publisher }} ({{ app.email }})</p> | |||
</div> | |||
</div> | |||
</div> | |||
{% if app.long_description %} | |||
{{ app.long_description|markdown }} | |||
{% endif %} | |||
<!-- autodoc --> | |||
@@ -1,10 +1,12 @@ | |||
<!-- title: {{ doctype }} --> | |||
<!-- add-breadcrumbs --> | |||
{% from "templates/autodoc/macros.html" import automodule, version, | |||
source_link, doctype_link %} | |||
{% set doc = frappe.get_doc("DocType", doctype) %} | |||
{% set controller = autodoc.get_controller(doctype) %} | |||
<h1>{{ doctype }}</h1> | |||
<div class="dev-header"> | |||
{{ version(doctype) }} | |||
{{ source_link(app, app.name + "/" + scrub(doc.module) | |||
@@ -22,7 +24,7 @@ | |||
<h3>Fields</h3> | |||
<table class="table table-bordered"> | |||
<table class="table table-bordered docfields"> | |||
<thead> | |||
<tr> | |||
<th style="width: 5%">Sr</th> | |||
@@ -59,7 +59,7 @@ | |||
{% macro doctype_link(app, doctype) %} | |||
{% set module = frappe.db.get_value("DocType", doctype, "module") %} | |||
{% if doctype and module %} | |||
<a href="{{ app.docs_base_url }}/{{ app.docs_version }}/models/{{ | |||
<a href="{{ docs_base_url }}/{{ app.docs_version }}/models/{{ | |||
scrub(module) }}/{{ scrub(doctype) }}">{{ doctype }}</a> | |||
{% endif %} | |||
{% endmacro %} |
@@ -1,6 +1,9 @@ | |||
<!-- title: {{ app.title }}: Models (DocTypes) --> | |||
<!-- add-breadcrumbs --> | |||
{% from "templates/autodoc/macros.html" import source_link, version %} | |||
<h1>{{ app.title }}: Models (DocTypes)</h1> | |||
<div class="dev-header"> | |||
{{ version(app.name) }} | |||
{{ source_link(app, app.name, True) }} | |||
@@ -1,6 +1,9 @@ | |||
<!-- title: Module {{ name }} --> | |||
<!-- add-breadcrumbs --> | |||
{% from "templates/autodoc/macros.html" import source_link, version %} | |||
<h1>Module {{ name }}</h1> | |||
<div class="dev-header"> | |||
{{ version(app.name) }} | |||
{{ source_link(app, app.name + "/" + scrub(name), True) }} | |||
@@ -1,6 +1,9 @@ | |||
<!-- title: {{ title }} --> | |||
<!-- add-breadcrumbs --> | |||
{% from "templates/autodoc/macros.html" import source_link, version %} | |||
<h1>{{ title }}</h1> | |||
<div class="dev-header"> | |||
{{ version(app.name) }} | |||
{{ source_link(app, title, True) }} | |||
@@ -1,7 +1,10 @@ | |||
<!-- title: {{ name }} --> | |||
<!-- add-breadcrumbs --> | |||
{%- from "templates/autodoc/macros.html" import automodule, source_link, | |||
version -%} | |||
<h1>{{ name }}</h1> | |||
<div class="dev-header"> | |||
{{ version(app.name) }} | |||
{{ source_link(app, full_module_name.replace(".", "/") + ".py") }} | |||
@@ -5,6 +5,25 @@ | |||
{% block hero %}{% endblock %} | |||
{% block page_content %} | |||
<style> | |||
.blog-list-content { | |||
border: 0px; | |||
background: transparent; | |||
} | |||
.blog-list-item { | |||
margin-top: 30px; | |||
margin-bottom: 30px; | |||
} | |||
.blog-list-item .blog-header { | |||
font-size: 1.6em; | |||
} | |||
.post-description { | |||
padding-bottom: 8px; | |||
} | |||
.post-description p { | |||
margin-bottom: 8px; | |||
} | |||
</style> | |||
<!-- no-header --> | |||
<!-- no-breadcrumbs --> | |||
<div class="blog-list-content"> | |||
@@ -1,4 +1,4 @@ | |||
<div class="blog-comment-row"> | |||
<div class="comment-row"> | |||
<div class="inline-block" style="vertical-align: top"> | |||
<div class="avatar avatar-medium" style="margin-top: 11px;"> | |||
<img itemprop="thumbnailUrl" src="{{ frappe.get_gravatar(comment.sender) }}" /> | |||
@@ -2,10 +2,12 @@ | |||
<ol> | |||
{% for item in children_map[route] %} | |||
<li> | |||
<a href="{{ url_prefix }}{{ item.route }}{{ item.extn or "" }}">{{ item.title }}</a> | |||
<a href="{{ url_prefix }}{{ item.route }}">{{ item.title }}</a> | |||
{# | |||
{% if children_map[item.route] %} | |||
{{ make_item_list(item.route, children_map) }} | |||
{% endif %} | |||
#} | |||
</li> | |||
{% endfor %} | |||
</ol> | |||
@@ -22,7 +22,7 @@ | |||
{% endif %} | |||
{% for item in sidebar_items -%} | |||
<li class="sidebar-item"> | |||
<a href="{{ item.route }}" class="text-muted {{ 'active' if pathname==item.route else '' }}" | |||
<a href="{{ item.route }}" class="gray {{ 'active' if pathname==item.route else '' }}" | |||
{% if item.target %}target="{{ item.target }}"{% endif %}> | |||
{{ _(item.title or item.label) }} | |||
</a> | |||
@@ -2,8 +2,10 @@ | |||
{% block hero %}{% endblock %} | |||
{% block content %} | |||
<div class="page-container" id="page-{{ name or route }}" data-path="{{ pathname }}" | |||
{% if page_or_generator=="Generator" %}data-doctype="{{ doctype }}"{% endif %}> | |||
<div class="page-container" id="page-{{ name or route }}" | |||
data-path="{{ pathname }}" | |||
{%- if page_or_generator=="Generator" %} | |||
data-doctype="{{ doctype }}"{% endif %}> | |||
<div class="row {% if show_sidebar %}vert-line{% endif %}"> | |||
{% if show_sidebar %} | |||
<div class="col-sm-3 sidebar-block hidden-xs"> | |||
@@ -29,7 +31,8 @@ | |||
{% if show_search %} | |||
<div class="page-search-block"> | |||
{% block search %} | |||
{% include 'templates/includes/search_box.html' %} | |||
{% include | |||
'templates/includes/search_box.html' %} | |||
{% endblock %} | |||
</div> | |||
{% endif %} | |||
@@ -41,9 +44,9 @@ | |||
{% endif %} | |||
</div> | |||
</div> | |||
</div> | |||
<div class="page_content"> | |||
{%- block page_content -%}{%- endblock -%} | |||
<div class="page_content"> | |||
{%- block page_content -%}{%- endblock -%} | |||
</div> | |||
</div> | |||
</div> | |||
</div> | |||
@@ -75,7 +75,7 @@ def get_allowed_functions_for_jenv(): | |||
import frappe.utils.data | |||
from frappe.utils.autodoc import automodule, get_version | |||
from frappe.model.document import get_controller | |||
from frappe.website.utils import get_shade | |||
from frappe.website.utils import (get_shade, get_toc, get_next_link) | |||
from frappe.modules import scrub | |||
import mimetypes | |||
from html2text import html2text | |||
@@ -128,11 +128,16 @@ def get_allowed_functions_for_jenv(): | |||
'csrf_token': frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else '' | |||
}, | |||
}, | |||
'style': { | |||
'border_color': '#d1d8dd' | |||
}, | |||
"autodoc": { | |||
"get_version": get_version, | |||
"automodule": automodule, | |||
"get_controller": get_controller | |||
}, | |||
'get_toc': get_toc, | |||
'get_next_link': get_next_link, | |||
"_": frappe._, | |||
"get_shade": get_shade, | |||
"scrub": scrub, | |||
@@ -159,8 +164,10 @@ def get_jloader(): | |||
if frappe.local.flags.in_setup_help: | |||
apps = ['frappe'] | |||
else: | |||
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True) | |||
apps.reverse() | |||
apps = frappe.get_hooks('template_apps') | |||
if not apps: | |||
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps(sort=True) | |||
apps.reverse() | |||
if not "frappe" in apps: | |||
apps.append('frappe') | |||
@@ -13,12 +13,12 @@ from frappe.utils import markdown | |||
from six import iteritems | |||
class setup_docs(object): | |||
def __init__(self, app): | |||
def __init__(self, app, target_app): | |||
"""Generate source templates for models reference and module API | |||
and templates at `templates/autodoc` | |||
""" | |||
self.app = app | |||
self.target_app = target_app | |||
frappe.flags.web_pages_folders = ['docs',] | |||
frappe.flags.web_pages_apps = [self.app,] | |||
@@ -44,7 +44,6 @@ class setup_docs(object): | |||
"sub_heading": self.docs_config.sub_heading, | |||
"source_link": self.docs_config.source_link, | |||
"hide_install": getattr(self.docs_config, "hide_install", False), | |||
"docs_base_url": self.docs_config.docs_base_url, | |||
"long_description": markdown(getattr(self.docs_config, "long_description", "")), | |||
"license": self.hooks.get("app_license")[0], | |||
"branch": getattr(self.docs_config, "branch", None) or "develop", | |||
@@ -59,7 +58,7 @@ class setup_docs(object): | |||
def build(self, docs_version): | |||
"""Build templates for docs models and Python API""" | |||
self.docs_path = frappe.get_app_path(self.app, "docs") | |||
self.docs_path = frappe.get_app_path(self.target_app, 'www', "docs") | |||
self.path = os.path.join(self.docs_path, docs_version) | |||
self.app_context["app"]["docs_version"] = docs_version | |||
@@ -91,14 +90,50 @@ class setup_docs(object): | |||
#print parts | |||
module, doctype = parts[-3], parts[-1] | |||
if doctype not in ("doctype", "boilerplate"): | |||
if doctype != "boilerplate": | |||
self.write_model_file(basepath, module, doctype) | |||
# standard python module | |||
if self.is_py_module(basepath, folders, files): | |||
self.write_modules(basepath, folders, files) | |||
self.build_user_docs() | |||
#self.build_user_docs() | |||
self.copy_user_assets() | |||
self.add_sidebars() | |||
self.add_breadcrumbs_for_user_pages() | |||
def add_breadcrumbs_for_user_pages(self): | |||
for basepath, folders, files in os.walk(os.path.join(self.docs_path, | |||
'user')): # pylint: disable=unused-variable | |||
for fname in files: | |||
if fname.endswith('.md') or fname.endswith('.html'): | |||
add_breadcrumbs_tag(os.path.join(basepath, fname)) | |||
def add_sidebars(self): | |||
'''Add _sidebar.json in each folder in docs''' | |||
for basepath, folders, files in os.walk(self.docs_path): # pylint: disable=unused-variable | |||
with open(os.path.join(basepath, '_sidebar.json'), 'w') as sidebarfile: | |||
sidebarfile.write(frappe.as_json([ | |||
{"title": "Docs Home", "route": "/docs"}, | |||
{"title": "User Guide", "route": "/docs/user"}, | |||
{"title": "Server API", "route": "/docs/current/api"}, | |||
{"title": "Models (Reference)", "route": "/docs/current/models"}, | |||
{"title": "Improve Docs", "route": | |||
"{0}/tree/develop/{1}/docs".format(self.docs_config.source_link, self.app)} | |||
])) | |||
def copy_user_assets(self): | |||
'''Copy docs/user and docs/assets to the target app''' | |||
print('Copying docs/user and docs/assets...') | |||
shutil.rmtree(os.path.join(self.docs_path, 'user'), | |||
ignore_errors=True) | |||
shutil.rmtree(os.path.join(self.docs_path, 'assets'), | |||
ignore_errors=True) | |||
shutil.copytree(os.path.join(self.app_path, 'docs', 'user'), | |||
os.path.join(self.docs_path, 'user')) | |||
shutil.copytree(os.path.join(self.app_path, 'docs', 'assets'), | |||
frappe.get_app_path(self.target_app, 'www', 'docs', 'assets')) | |||
def make_home_pages(self): | |||
"""Make standard home pages for docs, developer docs, api and models | |||
@@ -463,3 +498,9 @@ edit_link = ''' | |||
</div> | |||
</div> | |||
</div>''' | |||
def add_breadcrumbs_tag(path): | |||
with open(path, 'r') as f: | |||
content = frappe.as_unicode(f.read()) | |||
with open(path, 'w') as f: | |||
f.write(('<!-- add-breadcrumbs -->\n' + content).encode('utf-8')) |
@@ -16,15 +16,17 @@ def get_context(path, args=None): | |||
if args: | |||
context.update(args) | |||
context = build_context(context) | |||
if hasattr(frappe.local, 'request'): | |||
# for <body data-path=""> (remove leading slash) | |||
# path could be overriden in render.resolve_from_map | |||
context["path"] = frappe.local.request.path[1:] | |||
context["path"] = frappe.local.request.path.strip('/ ') | |||
else: | |||
context["path"] = path | |||
context.route = context.path | |||
context = build_context(context) | |||
# set using frappe.respond_as_web_page | |||
if hasattr(frappe.local, 'response') and frappe.local.response.get('context'): | |||
context.update(frappe.local.response.context) | |||
@@ -69,6 +71,9 @@ def build_context(context): | |||
if context.url_prefix and context.url_prefix[-1]!='/': | |||
context.url_prefix += '/' | |||
# for backward compatibility | |||
context.docs_base_url = '/docs' | |||
context.update(get_website_settings()) | |||
context.update(frappe.local.conf.get("website_context") or {}) | |||
@@ -105,7 +110,21 @@ def build_context(context): | |||
update_controller_context(context, extension) | |||
add_metatags(context) | |||
add_sidebar_and_breadcrumbs(context) | |||
# determine templates to be used | |||
if not context.base_template_path: | |||
app_base = frappe.get_hooks("base_template") | |||
context.base_template_path = app_base[0] if app_base else "templates/base.html" | |||
if context.title_prefix and context.title and not context.title.startswith(context.title_prefix): | |||
context.title = '{0} - {1}'.format(context.title_prefix, context.title) | |||
return context | |||
def add_sidebar_and_breadcrumbs(context): | |||
'''Add sidebar and breadcrumbs to context''' | |||
from frappe.website.router import get_page_info_from_template | |||
if context.show_sidebar: | |||
context.no_cache = 1 | |||
add_sidebar_data(context) | |||
@@ -117,16 +136,12 @@ def build_context(context): | |||
context.sidebar_items = json.loads(sidebarfile.read()) | |||
context.show_sidebar = 1 | |||
# determine templates to be used | |||
if not context.base_template_path: | |||
app_base = frappe.get_hooks("base_template") | |||
context.base_template_path = app_base[0] if app_base else "templates/base.html" | |||
if context.title_prefix and context.title and not context.title.startswith(context.title_prefix): | |||
context.title = '{0} - {1}'.format(context.title_prefix, context.title) | |||
return context | |||
if context.add_breadcrumbs and not context.parents: | |||
if context.basepath: | |||
parent_path = os.path.dirname(context.path).rstrip('/') | |||
page_info = get_page_info_from_template(parent_path) | |||
if page_info: | |||
context.parents = [dict(route=parent_path, title=page_info.title)] | |||
def add_sidebar_data(context): | |||
from frappe.utils.user import get_fullname_and_avatar | |||
@@ -1,6 +1,36 @@ | |||
{% extends "templates/web.html" %} | |||
{% block page_content %} | |||
<style> | |||
.blog-header { | |||
font-weight: 700; | |||
font-size: 2em; | |||
} | |||
.blog-comments { | |||
position: relative; | |||
border-top: 1px solid {{ style.border_color }}; | |||
} | |||
.blog-info { | |||
text-align:center; | |||
margin-top: 30px; | |||
} | |||
.blog-text { | |||
padding-top: 50px; | |||
padding-bottom: 50px; | |||
font-size: 18px; | |||
line-height: 1.5; | |||
} | |||
.blog-text p { | |||
margin-bottom: 30px; | |||
} | |||
.blogger { | |||
padding-top: 0px; | |||
padding-bottom: 50px; | |||
} | |||
.blogger-name { | |||
margin-top: 0px; | |||
} | |||
</style> | |||
<div class="blog-container"> | |||
<article class="blog-content" itemscope itemtype="http://schema.org/BlogPosting"> | |||
<meta itemprop="datePublished" content="{{ frappe.format_date(published_on) }}"></meta> | |||
@@ -10,9 +40,9 @@ | |||
<h1 itemprop="headline" class="blog-header">{{ title }}</h1> | |||
<p class="post-by text-muted"> | |||
<a href="/blog?blogger={{ blogger }}" class="no-decoration">{{ _("By") }} {{ blogger_info and blogger_info.full_name or full_name }}</a> | |||
<i class="blog-dot"></i> {{ frappe.format_date(published_on) }} | |||
<i class="blog-dot"></i> <a href="/blog?blog_category={{ blog_category }}" class="no-decoration">{{ category.title }}</a> | |||
<i class="blog-dot"></i> {{ comment_text }} | |||
<i class="spacer-dot"></i> {{ frappe.format_date(published_on) }} | |||
<i class="spacer-dot"></i> <a href="/blog?blog_category={{ blog_category }}" class="no-decoration">{{ category.title }}</a> | |||
<i class="spacer-dot"></i> {{ comment_text }} | |||
</p> | |||
</div> | |||
<div itemprop="articleBody" class="longform blog-text"> | |||
@@ -12,11 +12,11 @@ | |||
<p class="post-by text-muted small"> | |||
<a href="/blog?blogger={{ post.blogger }}" | |||
class="no-decoration">{{ _("By") }} {{ post.full_name }}</a> | |||
<i class="blog-dot"></i> {{ frappe.format_date(post.published_on) }} | |||
<i class="blog-dot"></i> | |||
<i class="spacer-dot"></i> {{ frappe.format_date(post.published_on) }} | |||
<i class="spacer-dot"></i> | |||
<a href="/blog?blog_category={{ post.blog_category }}" | |||
class="no-decoration">{{ post.category.title }}</a> | |||
<i class="blog-dot"></i> {{ post.comment_text }} | |||
<i class="spacer-dot"></i> {{ post.comment_text }} | |||
</p> | |||
</div> | |||
</div> | |||
@@ -0,0 +1,32 @@ | |||
import frappe, re, os | |||
def purifycss(): | |||
source = frappe.get_app_path('frappe_theme', 'public', 'less', 'frappe_theme.less') | |||
target_apps = ['erpnext_com', 'frappe_io', 'translator', 'chart_of_accounts_builder', 'frappe_theme'] | |||
with open(source, 'r') as f: | |||
src = f.read() | |||
classes = [] | |||
for line in src.splitlines(): | |||
line = line.strip() | |||
if not line: | |||
continue | |||
if line[0]=='@': | |||
continue | |||
classes.extend(re.findall('\.([^0-9][^ :&.{,(]*)', line)) | |||
classes = list(set(classes)) | |||
for app in target_apps: | |||
for basepath, folders, files in os.walk(frappe.get_app_path(app)): | |||
for fname in files: | |||
if fname.endswith('.html') or fname.endswith('.md'): | |||
#print 'checking {0}...'.format(fname) | |||
with open(os.path.join(basepath, fname), 'r') as f: | |||
src = f.read() | |||
for c in classes: | |||
if c in src: | |||
classes.remove(c) | |||
for c in sorted(classes): | |||
print c |
@@ -6,14 +6,16 @@ import frappe | |||
from frappe import _ | |||
import frappe.sessions | |||
from frappe.utils import cstr | |||
import mimetypes, json | |||
import os, mimetypes, json | |||
from six import iteritems | |||
from werkzeug.wrappers import Response | |||
from werkzeug.routing import Map, Rule, NotFound | |||
from werkzeug.wsgi import wrap_file | |||
from frappe.website.context import get_context | |||
from frappe.website.utils import get_home_page, can_cache, delete_page_cache | |||
from frappe.website.utils import (get_home_page, can_cache, delete_page_cache, | |||
get_toc, get_next_link) | |||
from frappe.website.router import clear_sitemap | |||
from frappe.translate import guess_language | |||
@@ -21,14 +23,16 @@ class PageNotFoundError(Exception): pass | |||
def render(path=None, http_status_code=None): | |||
"""render html page""" | |||
path = resolve_path(path or frappe.local.request.path.strip('/ ')) | |||
path = resolve_path(path or frappe.local.request.path) | |||
path = path.strip('/ ') | |||
data = None | |||
# if in list of already known 404s, send it | |||
if can_cache() and frappe.cache().hget('website_404', frappe.request.url): | |||
data = render_page('404') | |||
http_status_code = 404 | |||
elif is_static_file(path): | |||
return get_static_file_reponse() | |||
else: | |||
try: | |||
data = render_page_by_language(path) | |||
@@ -71,6 +75,32 @@ def render(path=None, http_status_code=None): | |||
return build_response(path, data, http_status_code or 200) | |||
def is_static_file(path): | |||
if ('.' not in path): | |||
return False | |||
extn = path.rsplit('.', 1)[-1] | |||
if extn in ('html', 'md', 'js'): | |||
return False | |||
for app in frappe.get_installed_apps(): | |||
file_path = frappe.get_app_path(app, 'www') + '/' + path | |||
if os.path.exists(file_path): | |||
frappe.flags.file_path = file_path | |||
return True | |||
return False | |||
def get_static_file_reponse(): | |||
try: | |||
f = open(frappe.flags.file_path, 'rb') | |||
except IOError: | |||
raise NotFound | |||
response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True) | |||
response.mimetype = mimetypes.guess_type(frappe.flags.file_path)[0] or b'application/octet-stream' | |||
return response | |||
def build_response(path, data, http_status_code, headers=None): | |||
# build response | |||
response = Response() | |||
@@ -143,6 +173,12 @@ def build_page(path): | |||
elif context.template: | |||
html = frappe.get_template(context.template).render(context) | |||
if '{index}' in html: | |||
html = html.replace('{index}', get_toc(context.route)) | |||
if '{next}' in html: | |||
html = html.replace('{next}', get_next_link(context.route)) | |||
# html = frappe.get_template(context.base_template_path).render(context) | |||
if can_cache(context.no_cache): | |||
@@ -221,7 +257,9 @@ def clear_cache(path=None): | |||
'''Clear website caches | |||
:param path: (optional) for the given path''' | |||
frappe.cache().delete_value("website_generator_routes") | |||
for key in ('website_generator_routes', 'website_pages', | |||
'website_full_index'): | |||
frappe.cache().delete_value(key) | |||
delete_page_cache(path) | |||
frappe.cache().delete_value("website_404") | |||
if not path: | |||
@@ -6,7 +6,6 @@ import frappe, os | |||
from frappe.website.utils import can_cache, delete_page_cache, extract_title | |||
from frappe.model.document import get_controller | |||
from frappe import _ | |||
def resolve_route(path): | |||
"""Returns the page route object based on searching in pages and generators. | |||
@@ -15,7 +14,7 @@ def resolve_route(path): | |||
The only exceptions are `/about` and `/contact` these will be searched in Web Pages | |||
first before checking the standard pages.""" | |||
if path not in ("about", "contact"): | |||
context = get_page_context_from_template(path) | |||
context = get_page_info_from_template(path) | |||
if context: | |||
return context | |||
return get_page_context_from_doctype(path) | |||
@@ -23,7 +22,7 @@ def resolve_route(path): | |||
context = get_page_context_from_doctype(path) | |||
if context: | |||
return context | |||
return get_page_context_from_template(path) | |||
return get_page_info_from_template(path) | |||
def get_page_context(path): | |||
page_context = None | |||
@@ -54,12 +53,12 @@ def make_page_context(path): | |||
return context | |||
def get_page_context_from_template(path): | |||
def get_page_info_from_template(path): | |||
'''Return page_info from path''' | |||
for app in frappe.get_installed_apps(frappe_last=True): | |||
app_path = frappe.get_app_path(app) | |||
folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages') | |||
folders = get_start_folders() | |||
for start in folders: | |||
search_path = os.path.join(app_path, start, path) | |||
@@ -68,14 +67,15 @@ def get_page_context_from_template(path): | |||
for o in options: | |||
option = frappe.as_unicode(o) | |||
if os.path.exists(option) and not os.path.isdir(option): | |||
return get_page_info(option, app, app_path=app_path) | |||
return get_page_info(option, app, start, app_path=app_path) | |||
return None | |||
def get_page_context_from_doctype(path): | |||
page_info = get_page_info_from_doctypes(path) | |||
if page_info: | |||
return frappe.get_doc(page_info.get("doctype"), page_info.get("name")).get_page_info() | |||
return frappe.get_doc(page_info.get("doctype"), | |||
page_info.get("name")).get_page_info() | |||
def clear_sitemap(): | |||
delete_page_cache("*") | |||
@@ -120,57 +120,60 @@ def get_page_info_from_doctypes(path=None): | |||
def get_pages(app=None): | |||
'''Get all pages. Called for docs / sitemap''' | |||
pages = {} | |||
frappe.local.flags.in_get_all_pages = True | |||
folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages') | |||
def _build(app): | |||
pages = {} | |||
if app: | |||
apps = [app] | |||
else: | |||
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps() | |||
if app: | |||
apps = [app] | |||
else: | |||
apps = frappe.local.flags.web_pages_apps or frappe.get_installed_apps() | |||
for app in apps: | |||
app_path = frappe.get_app_path(app) | |||
for app in apps: | |||
app_path = frappe.get_app_path(app) | |||
for start in folders: | |||
path = os.path.join(app_path, start) | |||
pages.update(get_pages_from_path(path, app, app_path)) | |||
frappe.local.flags.in_get_all_pages = False | |||
for start in get_start_folders(): | |||
pages.update(get_pages_from_path(start, app, app_path)) | |||
return pages | |||
return pages | |||
return frappe.cache().get_value('website_pages', lambda: _build(app)) | |||
def get_pages_from_path(path, app, app_path): | |||
def get_pages_from_path(start, app, app_path): | |||
pages = {} | |||
if os.path.exists(path): | |||
for basepath, folders, files in os.walk(path): | |||
start_path = os.path.join(app_path, start) | |||
if os.path.exists(start_path): | |||
for basepath, folders, files in os.walk(start_path): | |||
# add missing __init__.py | |||
if not '__init__.py' in files: | |||
open(os.path.join(basepath, '__init__.py'), 'a').close() | |||
for fname in files: | |||
fname = frappe.utils.cstr(fname) | |||
if not '.' in fname: | |||
continue | |||
page_name, extn = fname.rsplit(".", 1) | |||
if extn in ('js', 'css') and os.path.exists(os.path.join(basepath, fname + '.html')): | |||
# js, css is linked to html, skip | |||
continue | |||
if extn in ("html", "xml", "js", "css", "md"): | |||
page_info = get_page_info(path, app, basepath, app_path, fname) | |||
page_info = get_page_info(os.path.join(basepath, fname), | |||
app, start, basepath, app_path, fname) | |||
pages[page_info.route] = page_info | |||
# print frappe.as_json(pages[-1]) | |||
return pages | |||
def get_page_info(path, app, basepath=None, app_path=None, fname=None): | |||
def get_page_info(path, app, start, basepath=None, app_path=None, fname=None): | |||
'''Load page info''' | |||
if not fname: | |||
if fname is None: | |||
fname = os.path.basename(path) | |||
if not app_path: | |||
if app_path is None: | |||
app_path = frappe.get_app_path(app) | |||
if not basepath: | |||
if basepath is None: | |||
basepath = os.path.dirname(path) | |||
page_name, extn = fname.rsplit(".", 1) | |||
@@ -187,9 +190,16 @@ def get_page_info(path, app, basepath=None, app_path=None, fname=None): | |||
if page_info.basename == 'index': | |||
page_info.basename = "" | |||
page_info.route = page_info.name = page_info.page_name = os.path.join(os.path.relpath(basepath, path), | |||
page_info.basename).strip('/.') | |||
# get route from template name | |||
page_info.route = page_info.template.replace(start, '').strip('/') | |||
if os.path.basename(page_info.route) in ('index.html', 'index.md'): | |||
page_info.route = os.path.dirname(page_info.route) | |||
# remove the extension | |||
if page_info.route.endswith('.md') or page_info.route.endswith('.html'): | |||
page_info.route = page_info.route.rsplit('.', 1)[0] | |||
page_info.name = page_info.page_name = page_info.route | |||
# controller | |||
page_info.controller_path = os.path.join(basepath, page_name.replace("-", "_") + ".py") | |||
@@ -202,9 +212,11 @@ def get_page_info(path, app, basepath=None, app_path=None, fname=None): | |||
# get the source | |||
setup_source(page_info) | |||
if page_info.only_content: | |||
# extract properties from HTML comments | |||
load_properties(page_info) | |||
# extract properties from HTML comments | |||
load_properties(page_info) | |||
# if not page_info.title: | |||
# print('no-title-for', page_info.route) | |||
return page_info | |||
@@ -255,48 +267,14 @@ def setup_index(page_info): | |||
if os.path.exists(index_txt_path): | |||
page_info.index = open(index_txt_path, 'r').read().splitlines() | |||
def make_toc(context, out, app=None): | |||
'''Insert full index (table of contents) for {index} tag''' | |||
from frappe.website.utils import get_full_index | |||
if '{index}' in out: | |||
html = frappe.get_template("templates/includes/full_index.html").render({ | |||
"full_index": get_full_index(app=app), | |||
"url_prefix": context.url_prefix or "/", | |||
"route": context.route | |||
}) | |||
out = out.replace('{index}', html) | |||
if '{next}' in out: | |||
# insert next link | |||
next_item = None | |||
children_map = get_full_index(app=app) | |||
parent_route = os.path.dirname(context.route) | |||
children = children_map[parent_route] | |||
if parent_route and children: | |||
for i, c in enumerate(children): | |||
if c.route == context.route and i < (len(children) - 1): | |||
next_item = children[i+1] | |||
next_item.url_prefix = context.url_prefix or "/" | |||
if next_item: | |||
if next_item.route and next_item.title: | |||
html = ('<p class="btn-next-wrapper">'+_("Next")\ | |||
+': <a class="btn-next" href="{url_prefix}{route}">{title}</a></p>').format(**next_item) | |||
out = out.replace('{next}', html) | |||
return out | |||
def load_properties(page_info): | |||
'''Load properties like no_cache, title from raw''' | |||
if not page_info.title: | |||
page_info.title = extract_title(page_info.source, page_info.name) | |||
page_info.title = extract_title(page_info.source, page_info.route) | |||
if page_info.title and not '{% block title %}' in page_info.source: | |||
page_info.source += '\n{% block title %}{{ title }}{% endblock %}' | |||
# if page_info.title and not '{% block title %}' in page_info.source: | |||
# if not page_info.only_content: | |||
# page_info.source += '\n{% block title %}{{ title }}{% endblock %}' | |||
if "<!-- no-breadcrumbs -->" in page_info.source: | |||
page_info.no_breadcrumbs = 1 | |||
@@ -304,13 +282,19 @@ def load_properties(page_info): | |||
if "<!-- show-sidebar -->" in page_info.source: | |||
page_info.show_sidebar = 1 | |||
if "<!-- add-breadcrumbs -->" in page_info.source: | |||
page_info.add_breadcrumbs = 1 | |||
if "<!-- no-header -->" in page_info.source: | |||
page_info.no_header = 1 | |||
else: | |||
# every page needs a header | |||
# add missing header if there is no <h1> tag | |||
if (not '{% block header %}' in page_info.source) and (not '<h1' in page_info.source): | |||
page_info.source += '\n{% block header %}<h1>{{ title }}</h1>{% endblock %}' | |||
# else: | |||
# # every page needs a header | |||
# # add missing header if there is no <h1> tag | |||
# if (not '{% block header %}' in page_info.source) and (not '<h1' in page_info.source): | |||
# if page_info.only_content: | |||
# page_info.source = '<h1>{{ title }}</h1>\n' + page_info.source | |||
# else: | |||
# page_info.source += '\n{% block header %}<h1>{{ title }}</h1>{% endblock %}' | |||
if "<!-- no-cache -->" in page_info.source: | |||
page_info.no_cache = 1 | |||
@@ -346,7 +330,7 @@ def sync_global_search(): | |||
for app in frappe.get_installed_apps(frappe_last=True): | |||
app_path = frappe.get_app_path(app) | |||
folders = frappe.local.flags.web_pages_folders or ('www', 'templates/pages') | |||
folders = get_start_folders() | |||
for start in folders: | |||
for basepath, folders, files in os.walk(os.path.join(app_path, start)): | |||
@@ -374,3 +358,5 @@ def sync_global_search(): | |||
sync_global_search() | |||
def get_start_folders(): | |||
return frappe.local.flags.web_pages_folders or ('www', 'templates/pages') |
@@ -5,9 +5,9 @@ from __future__ import unicode_literals | |||
import frappe, re, os | |||
from six import iteritems | |||
def delete_page_cache(path): | |||
cache = frappe.cache() | |||
cache.delete_value('full_index') | |||
groups = ("website_page", "page_context") | |||
if path: | |||
for name in groups: | |||
@@ -184,38 +184,89 @@ def abs_url(path): | |||
path = "/" + path | |||
return path | |||
def get_toc(route, url_prefix=None, app=None): | |||
'''Insert full index (table of contents) for {index} tag''' | |||
from frappe.website.utils import get_full_index | |||
full_index = get_full_index(app=app) | |||
return frappe.get_template("templates/includes/full_index.html").render({ | |||
"full_index": full_index, | |||
"url_prefix": url_prefix or "/", | |||
"route": route.rstrip('/') | |||
}) | |||
def get_next_link(route, url_prefix=None, app=None): | |||
# insert next link | |||
next_item = None | |||
route = route.rstrip('/') | |||
children_map = get_full_index(app=app) | |||
parent_route = os.path.dirname(route) | |||
children = children_map[parent_route] | |||
if parent_route and children: | |||
for i, c in enumerate(children): | |||
if c.route == route and i < (len(children) - 1): | |||
next_item = children[i+1] | |||
next_item.url_prefix = url_prefix or "/" | |||
if next_item: | |||
if next_item.route and next_item.title: | |||
html = ('<p class="btn-next-wrapper">' + frappe._("Next")\ | |||
+': <a class="btn-next" href="{url_prefix}{route}">{title}</a></p>').format(**next_item) | |||
return html | |||
return '' | |||
def get_full_index(route=None, app=None): | |||
"""Returns full index of the website for www upto the n-th level""" | |||
from frappe.website.router import get_pages | |||
if not frappe.local.flags.children_map: | |||
from frappe.website.router import get_pages | |||
children_map = {} | |||
pages = get_pages(app=app) | |||
# make children map | |||
for route, page_info in iteritems(pages): | |||
parent_route = os.path.dirname(route) | |||
children_map.setdefault(parent_route, []).append(page_info) | |||
if frappe.flags.local_docs: | |||
page_info.extn = '.html' | |||
# order as per index if present | |||
for route, children in children_map.items(): | |||
page_info = pages[route] | |||
if page_info.index: | |||
new_children = [] | |||
page_info.extn = '' | |||
for name in page_info.index: | |||
child_route = page_info.route + '/' + name | |||
if child_route in pages: | |||
new_children.append(pages[child_route]) | |||
# add remaining pages not in index.txt | |||
for c in children: | |||
if c not in new_children: | |||
new_children.append(c) | |||
children_map[route] = new_children | |||
def _build(): | |||
children_map = {} | |||
added = [] | |||
pages = get_pages(app=app) | |||
# make children map | |||
for route, page_info in iteritems(pages): | |||
parent_route = os.path.dirname(route) | |||
if parent_route not in added: | |||
children_map.setdefault(parent_route, []).append(page_info) | |||
# order as per index if present | |||
for route, children in children_map.items(): | |||
if not route in pages: | |||
# no parent (?) | |||
continue | |||
page_info = pages[route] | |||
if page_info.index or ('index' in page_info.template): | |||
new_children = [] | |||
page_info.extn = '' | |||
for name in (page_info.index or []): | |||
child_route = page_info.route + '/' + name | |||
if child_route in pages: | |||
if child_route not in added: | |||
new_children.append(pages[child_route]) | |||
added.append(child_route) | |||
# add remaining pages not in index.txt | |||
_children = sorted(children, lambda a, b: cmp( | |||
os.path.basename(a.route), os.path.basename(b.route))) | |||
for child_route in _children: | |||
if child_route not in new_children: | |||
if child_route not in added: | |||
new_children.append(child_route) | |||
added.append(child_route) | |||
children_map[route] = new_children | |||
return children_map | |||
children_map = frappe.cache().get_value('website_full_index', _build) | |||
frappe.local.flags.children_map = children_map | |||
@@ -223,12 +274,14 @@ def get_full_index(route=None, app=None): | |||
def extract_title(source, path): | |||
'''Returns title from `<!-- title -->` or <h1> or path''' | |||
title = '' | |||
if "<!-- title:" in source: | |||
title = re.findall('<!-- title:([^>]*) -->', source)[0].strip() | |||
elif "<h1>" in source: | |||
match = re.findall('<h1>([^<]*)', source) | |||
title = match[0].strip()[:300] | |||
else: | |||
title = os.path.basename(path).replace('_', ' ').replace('-', ' ').title() | |||
if not title: | |||
title = os.path.basename(path.rsplit('.', )[0].rstrip('/')).replace('_', ' ').replace('-', ' ').title() | |||
return title |
@@ -1,7 +1,8 @@ | |||
// website_script.js | |||
{% if javascript -%}{{ javascript }}{%- endif %} | |||
{% if google_analytics_id -%} | |||
<!-- Google Analytics --> | |||
// Google Analytics | |||
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ | |||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), | |||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) | |||
@@ -9,5 +10,5 @@ m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) | |||
ga('create', '{{ google_analytics_id }}', 'auto'); | |||
ga('send', 'pageview'); | |||
<!-- End Google Analytics --> | |||
// End Google Analytics | |||
{%- endif %} |
@@ -10,7 +10,8 @@ no_sitemap = 1 | |||
base_template_path = "templates/www/website_script.js" | |||
def get_context(context): | |||
context.javascript = frappe.db.get_single_value('Website Script', 'javascript') or "" | |||
context.javascript = frappe.db.get_single_value('Website Script', | |||
'javascript') or "" | |||
theme = get_active_theme() | |||
js = strip(theme and theme.js or "") | |||