Skip to content

Commit 8ffab70

Browse files
committed
Move docs into the database. Add doc searching, and migrate to bootstrap4.
1 parent bba7516 commit 8ffab70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1447
-221
lines changed

docs/__init__.py

Whitespace-only changes.

docs/admin.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
##########################################################################
2+
#
3+
# pgAdmin Website
4+
#
5+
# Copyright (C) 2017, The pgAdmin Development Team
6+
# This software is released under the PostgreSQL Licence
7+
#
8+
##########################################################################
9+
10+
from __future__ import unicode_literals
11+
12+
from django.contrib import admin
13+
14+
from .models import Page
15+
16+
17+
class PageAdmin(admin.ModelAdmin):
18+
list_display = ('_get_title',)
19+
20+
def _get_title(self, obj):
21+
return str(obj)
22+
_get_title.allow_tags = True
23+
24+
25+
admin.site.register(Page, PageAdmin)

docs/apps.py

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
##########################################################################
2+
#
3+
# pgAdmin Website
4+
#
5+
# Copyright (C) 2017, The pgAdmin Development Team
6+
# This software is released under the PostgreSQL Licence
7+
#
8+
##########################################################################
9+
10+
from __future__ import unicode_literals
11+
12+
from django.apps import AppConfig
13+
14+
15+
class DocsConfig(AppConfig):
16+
name = 'docs'

docs/management/__init__.py

Whitespace-only changes.

docs/management/commands/__init__.py

Whitespace-only changes.

docs/management/commands/docloader.py

+166
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
##########################################################################
2+
#
3+
# pgAdmin Website
4+
#
5+
# Copyright (C) 2017, The pgAdmin Development Team
6+
# This software is released under the PostgreSQL Licence
7+
#
8+
##########################################################################
9+
10+
from bs4 import BeautifulSoup
11+
import glob
12+
import os
13+
from django.core.management.base import BaseCommand, CommandError
14+
from docs.models import Page
15+
from download.models import Package, Version
16+
from django.db.utils import IntegrityError
17+
from django.db.transaction import set_autocommit, commit, rollback
18+
from pgaweb.settings import STATIC_URL
19+
20+
21+
class Command(BaseCommand):
22+
help = 'Load HTML docs from a directory into the database.'
23+
24+
def add_arguments(self, parser):
25+
parser.add_argument('package', help="the name of the Package to link "
26+
"the document to.")
27+
28+
parser.add_argument('version', help="the version number of the "
29+
"package to link the document to. "
30+
"Any text including and following "
31+
"the first non-numeric or . will "
32+
"be treated as the suffix.")
33+
34+
parser.add_argument('source', help="the path to the directory "
35+
"containing the HTML files to load "
36+
"and index. Must be relative to "
37+
"$INSTDIR/static/docs")
38+
39+
def load_docs(self, directory, static, version):
40+
"""
41+
Load document pages from the given directory into the Document
42+
43+
:param directory: The directory to load pages from
44+
:param directory: The path to static files
45+
:param version: The Version object to load Pages into
46+
:return: The number of Pages loaded
47+
"""
48+
49+
count = 0
50+
path = os.path.join(directory, '**/*.htm*')
51+
52+
for file in glob.iglob(path, recursive=True):
53+
self.stdout.write("Loading: {} ".format(file))
54+
55+
f = open(file, 'r')
56+
html = f.read()
57+
f.close()
58+
59+
# Get the filename
60+
filename = file.replace(directory, '')
61+
if filename.startswith('/'):
62+
filename = filename[1:]
63+
64+
static_dir = os.path.join(STATIC_URL, static)
65+
if '/' in filename:
66+
static_dir = os.path.join(STATIC_URL, static,
67+
filename[0:filename.rfind('/')])
68+
69+
# Extract the HTML
70+
soup = BeautifulSoup(html, features="html.parser")
71+
72+
# Update paths to images
73+
for img in soup.findAll('img'):
74+
img['src'] = os.path.join(static_dir, img['src'])
75+
76+
for obj in soup.findAll('object'):
77+
if obj.has_attr('data'):
78+
obj['data'] = os.path.join(static_dir, obj['data'])
79+
if obj.has_attr('width'):
80+
del obj['width']
81+
82+
# Remove the search box if present
83+
for div in soup.findAll('div'):
84+
if div.has_attr('id') and div['id'] == 'searchbox':
85+
div.extract()
86+
87+
# If there's no title, use the filename (without the .html)
88+
if soup.find('title') is not None:
89+
title = soup.find('title').decode_contents(formatter="html").strip()
90+
else:
91+
title = filename[:-5]
92+
93+
# Remove the title, as we no longer need it
94+
[x.extract() for x in soup.find_all('title')]
95+
96+
# Get the page header navigation
97+
header = soup.find('div', role='navigation').decode_contents(formatter="html").strip()
98+
99+
# Get the contents panel
100+
contents_soup = soup.find('div', class_='sphinxsidebarwrapper')
101+
[x.extract() for x in contents_soup.find_all('h3')]
102+
contents = contents_soup.decode_contents(formatter="html").strip()
103+
104+
# Get the page content
105+
body = soup.find('div', class_='body').decode_contents(formatter="html").strip()
106+
107+
# We want the Contents header to be an H1 not H2
108+
contents = contents.replace('h3>', 'h1>')
109+
110+
# Create the new Page
111+
try:
112+
page = Page.objects.create(version=version, file=filename,
113+
title=title, header=header,
114+
contents=contents, body=body)
115+
except IntegrityError as e:
116+
rollback()
117+
raise CommandError("Rolling back: failed to insert {}: {}".
118+
format(filename, str(e)))
119+
120+
page.save()
121+
122+
count = count + 1
123+
124+
return count
125+
126+
def handle(self, *args, **options):
127+
"""The core structure of the app."""
128+
129+
# Get the various paths we need
130+
root_dir = os.path.realpath(os.path.join(
131+
os.path.dirname(__file__), '../../../'))
132+
133+
source_path = os.path.join(root_dir, 'static/docs', options['source'])
134+
135+
static_dir = os.path.join('docs', options['source'])
136+
if os.path.isfile(source_path):
137+
static_dir = os.path.dirname(os.path.join('docs',
138+
options['source']))
139+
140+
# Disable autocommit so this all runs in a transaction
141+
set_autocommit(False)
142+
143+
# Check the Package exists
144+
package = Package.objects.filter(name=options['package']).first()
145+
146+
if package is None:
147+
raise CommandError("The Package object could not be found.")
148+
149+
# Check the Version exists
150+
version = Version.objects.filter(name=options['version'],
151+
package=package).first()
152+
153+
if version is None:
154+
raise CommandError("The Version object could not be found.")
155+
156+
# Delete all existing pages and load the new ones from the dir
157+
Page.objects.filter(version_id=version.id).delete()
158+
159+
count = self.load_docs(source_path,
160+
static_dir,
161+
version)
162+
163+
commit()
164+
165+
self.stdout.write("Loaded {} pages into {}".format(
166+
count, str(version)))

docs/migrations/0001_initial.py

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.18 on 2019-07-18 11:04
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
import tsvector_field
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
initial = True
13+
14+
dependencies = [
15+
('download', '0004_auto_20190718_0929'),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name='Page',
21+
fields=[
22+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23+
('file', models.CharField(help_text='The file name of the page', max_length=100)),
24+
('title', models.CharField(help_text='The title of the page', max_length=500)),
25+
('header', models.TextField(blank=True, help_text='The header tabs for the page.')),
26+
('body', models.TextField(blank=True, help_text='The body of the page.')),
27+
('search', tsvector_field.SearchVectorField(blank=True, columns=[tsvector_field.WeightedColumn('title', 'A'), tsvector_field.WeightedColumn('body', 'D')], language='english')),
28+
('version', models.ForeignKey(help_text='Select the Product Version to which this page applies.', on_delete=django.db.models.deletion.CASCADE, to='download.Version')),
29+
],
30+
),
31+
migrations.AlterUniqueTogether(
32+
name='page',
33+
unique_together=set([('version', 'file')]),
34+
),
35+
]
+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.11.18 on 2019-07-19 09:42
3+
from __future__ import unicode_literals
4+
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('docs', '0001_initial'),
12+
]
13+
14+
operations = [
15+
migrations.AlterModelOptions(
16+
name='page',
17+
options={'ordering': ('version', 'title')},
18+
),
19+
migrations.AddField(
20+
model_name='page',
21+
name='contents',
22+
field=models.TextField(blank=True, help_text='The contents block of the page.'),
23+
),
24+
migrations.AlterField(
25+
model_name='page',
26+
name='header',
27+
field=models.TextField(blank=True, help_text='The header block for the page.'),
28+
),
29+
]

docs/migrations/__init__.py

Whitespace-only changes.

docs/models.py

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
##########################################################################
2+
#
3+
# pgAdmin Website
4+
#
5+
# Copyright (C) 2017, The pgAdmin Development Team
6+
# This software is released under the PostgreSQL Licence
7+
#
8+
##########################################################################
9+
10+
from django.core.cache import cache
11+
from django.db import models
12+
from django.db.models.signals import post_save
13+
from django.dispatch import receiver
14+
from django.urls import reverse
15+
from download.models import Version
16+
import tsvector_field
17+
18+
19+
@receiver(post_save)
20+
def clear_the_cache(**kwargs):
21+
if kwargs['sender']._meta.label != 'admin.LogEntry':
22+
cache.clear()
23+
24+
25+
class Page(models.Model):
26+
"""
27+
Stores documentation pages that are not external files. Related to
28+
:model:`docs.Document`,
29+
"""
30+
version = models.ForeignKey(Version, on_delete=models.CASCADE,
31+
help_text="Select the Product Version to "
32+
"which this page applies.")
33+
file = models.CharField(null=False, blank=False, max_length=100,
34+
help_text="The file name of the page")
35+
title = models.CharField(null=False, blank=False, max_length=500,
36+
help_text="The title of the page")
37+
header = models.TextField(null=False, blank=True,
38+
help_text="The header block for the page.")
39+
contents = models.TextField(null=False, blank=True,
40+
help_text="The contents block of the page.")
41+
body = models.TextField(null=False, blank=True,
42+
help_text="The body of the page.")
43+
search = tsvector_field.SearchVectorField([
44+
tsvector_field.WeightedColumn('title', 'A'),
45+
tsvector_field.WeightedColumn('body', 'D'),
46+
], 'english', blank=True)
47+
48+
@property
49+
def url(self):
50+
return reverse('page', args=[self.version.package.slug,
51+
self.version.name,
52+
self.file])
53+
54+
class Meta:
55+
unique_together = ['version', 'file']
56+
ordering = ('version', 'title')
57+
58+
def __str__(self):
59+
return self.title

docs/templates/docs/docs.html

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{% extends "pgaweb/page.html" %}
2+
3+
{% block title %}Documentation{% endblock %}
4+
5+
{% block content %}
6+
<h1>Documentation</h1>
7+
8+
<p>The pgAdmin documentation for the current development code, and recent major releases of the application is available
9+
for online browsing. Please select the documentation version you would like to view from the options below.</p>
10+
11+
<p>The documentation is automatically imported from the pgAdmin <a href="{% url 'development_resources' %}#git">GIT</a>
12+
source code repository, and is only available in English.</p>
13+
14+
{% regroup pages by version.package.name as page_list %}
15+
{% for page in page_list %}
16+
<h2>{{ page.grouper }}</h2>
17+
<div class="well pga-downloads">
18+
<ul class="fa-ul">
19+
{% for pg in page.list %}
20+
<li><i class="fa-li fa fa-book" aria-hidden="true"></i><a href="{% url 'page' pg.version.package.slug pg.version.slug 'index.html' %}">{{ pg.version.name }}</a>{% if not pg.version.pre_release %} (released {{ pg.version.released }}){% endif %}</li>
21+
{% endfor %}
22+
</ul>
23+
</div>
24+
{% endfor %}
25+
{% endblock %}

0 commit comments

Comments
 (0)