Skip to content

Commit

Permalink
Merge pull request #20 from usegalaxy-au/boilerplate-generator
Browse files Browse the repository at this point in the history
Boilerplate generator
  • Loading branch information
neoformit authored Dec 17, 2024
2 parents 3f98067 + dc287c6 commit 7b4b222
Show file tree
Hide file tree
Showing 38 changed files with 1,481 additions and 39 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
*.sqlite3
app/app/static/
app/app/media/
temp/

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
6 changes: 6 additions & 0 deletions app/app/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
STATIC_ROOT = BASE_DIR / 'app/static'
MEDIA_ROOT = BASE_DIR / 'app/media'
LOG_ROOT = ensure_dir(BASE_DIR / 'app/logs')
TEMP_DIR = ensure_dir(BASE_DIR / 'app/temp')
DEFAULT_EXPORTED_LAB_CONTENT_ROOT = (
f'http://{HOSTNAME}/static/labs/content/docs/base.yml')

Expand Down Expand Up @@ -97,8 +98,13 @@
'django.contrib.messages',
'django.contrib.staticfiles',
'labs',
'crispy_forms',
"crispy_bootstrap5",
]

CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
CRISPY_TEMPLATE_PACK = "bootstrap5"

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
Expand Down
116 changes: 116 additions & 0 deletions app/labs/bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
"""Render a new lab from form data."""

import random
import shutil
import string
import time
import zipfile
from django.conf import settings
from django.template.loader import render_to_string
from django.utils.text import slugify
from pathlib import Path

HOURS_72 = 3 * 24 * 60 * 60
ALPHANUMERIC = string.ascii_letters + string.digits
TEMPLATE_DIR = Path('labs/bootstrap')
TEMPLATES_TO_RENDER = [
'base.yml',
'intro.md',
'conclusion.md',
'footer.md',
'section-1.yml',
'README.md',
'custom.css',
'CONTRIBUTORS',
]
GALAXY_SERVERS = {
'': 'usegalaxy.org',
'Europe': 'usegalaxy.eu',
'Australia': 'usegalaxy.org.au',
}


def random_string(length):
return ''.join(random.choices(ALPHANUMERIC, k=length))


def lab(form_data):
"""Render a new lab from form data."""
clean_dir(settings.TEMP_DIR)
output_dir = settings.TEMP_DIR / random_string(6)
form_data['logo_filename'] = create_logo(form_data, output_dir)
render_templates(form_data, output_dir)
render_server_yml(form_data, output_dir)
zipfile_path = output_dir.with_suffix('.zip')
root_dir = Path(slugify(form_data['lab_name']))
with zipfile.ZipFile(zipfile_path, 'w') as zf:
for path in output_dir.rglob('*'):
zf.write(path, root_dir / path.relative_to(output_dir))
return zipfile_path


def render_templates(data, output_dir):
for template in TEMPLATES_TO_RENDER:
subdir = None
if template.endswith('.md') and 'README' not in template:
subdir = 'templates'
elif template.endswith('css'):
subdir = 'static'
elif template == 'section-1.yml':
subdir = 'sections'
render_file(
data,
template,
output_dir,
subdir=subdir,
)


def render_file(data, template, output_dir, subdir=None, filename=None):
outfile_relpath = filename or template
if subdir:
outfile_relpath = Path(subdir) / outfile_relpath
path = output_dir / outfile_relpath
path.parent.mkdir(parents=True, exist_ok=True)
content = render_to_string(TEMPLATE_DIR / template, data)
path.write_text(content)
return path


def render_server_yml(data, output_dir):
"""Create a YAML file for each Galaxy server."""
for site_name, root_domain in GALAXY_SERVERS.items():
data['site_name'] = site_name
data['galaxy_base_url'] = (
'https://'
+ data['subdomain']
+ '.'
+ root_domain
)
data['root_domain'] = root_domain
render_file(
data,
'server.yml',
output_dir,
filename=f'{root_domain}.yml',
)


def create_logo(data, output_dir):
"""Copy the uploaded logo to the output directory."""
logo_file = data.get(
'logo'
) or settings.BASE_DIR / 'labs/example_labs/docs/static/flask.svg'
logo_dest_path = output_dir / 'static' / logo_file.name
logo_dest_path.parent.mkdir(parents=True, exist_ok=True)
with logo_file.open('rb') as src, logo_dest_path.open('wb') as dest:
shutil.copyfileobj(src, dest)
return logo_dest_path.name


def clean_dir(directory):
"""Delete directories that were created more than 7 days ago."""
for path in directory.iterdir():
if path.is_dir() and path.stat().st_ctime < time.time() - HOURS_72:
shutil.rmtree(path)
return directory
112 changes: 112 additions & 0 deletions app/labs/forms.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
"""User facing forms for making support requests (help/tools/data)."""

import logging
from crispy_forms.helper import FormHelper
from crispy_forms.layout import Layout, HTML, Submit
from django import forms
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.core.validators import FileExtensionValidator
from utils.mail import retry_send_mail
from utils.webforms import SpamFilterFormMixin

from . import bootstrap, validators

logger = logging.getLogger('django')

MAIL_APPEND_TEXT = f"Sent from {settings.HOSTNAME}"
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']
INTRO_MD = 'Welcome to the Galaxy {{ site_name }} {{ lab_name }}!'
CONCLUSION_MD = ('Thanks for checking out the Galaxy {{ site_name }}'
' {{ lab_name }}!')
FOOTER_MD = f"""
<footer class="text-center">
This page was generated by the
<a href="{settings.HOSTNAME}/bootstrap">Galaxy Labs Engine</a>.
</footer>
"""


def dispatch_form_mail(
Expand Down Expand Up @@ -80,3 +96,99 @@ def dispatch(self, subject=None):
+ data['message']
)
)


class LabBootstrapForm(SpamFilterFormMixin, forms.Form):
"""Form to bootstrap a new lab."""

lab_name = forms.CharField(
label="Lab name",
widget=forms.TextInput(attrs={
'placeholder': "e.g. Genome Lab",
'autocomplete': 'off',
}),
)
subdomain = forms.CharField(
label="Galaxy Lab Subdomain",
widget=forms.TextInput(attrs={
'placeholder': "e.g. genome",
'autocomplete': 'off',
}),
help_text=(
"The subdomain that the lab will be served under. i.e."
" &lt;subdomain&gt;.usegalaxy.org"
),
)
github_username = forms.CharField(
label="GitHub Username",
widget=forms.TextInput(attrs={
'autocomplete': 'off',
}),
help_text=(
'(Optional) Your GitHub username to add to the list of'
' contributors.'
),
validators=[validators.validate_github_username],
required=False,
)
logo = forms.FileField(
label="Lab logo",
help_text=("(Optional) Upload a custom logo to be displayed in the Lab"
" header. Try to make it square and less than 100KB"
" - SVG format is highly recommended."),
required=False,
allow_empty_file=False,
validators=[
FileExtensionValidator(
allowed_extensions=IMAGE_EXTENSIONS,
),
],
widget=forms.FileInput(attrs={
'accept': ','.join(f"image/{ext}" for ext in IMAGE_EXTENSIONS),
}),
)

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.helper = FormHelper()
self.helper.layout = Layout(
'lab_name',
'subdomain',
'github_username',
'logo',
HTML(self.antispam_html),
Submit('submit', 'Build'),
)

def clean_subdomain(self):
"""Validate the subdomain."""
subdomain = self.cleaned_data['subdomain'].lower().strip()
if not subdomain.isalnum():
raise forms.ValidationError("Subdomain must be alphanumeric.")
return subdomain

def clean_lab_name(self):
"""Validate the lab name."""
lab_name = self.cleaned_data['lab_name'].title().strip()
if not lab_name.replace(' ', '').isalnum():
raise forms.ValidationError("Lab name cannot be empty.")
return lab_name

def clean_github_username(self):
username = self.cleaned_data.get('github_username')
return username.strip() if username else None

def bootstrap_lab(self):
data = self.cleaned_data
data.update({
'intro_md': INTRO_MD, # TODO: Render from user-input
'conclusion_md': CONCLUSION_MD,
'footer_md': FOOTER_MD,
'root_domain': 'usegalaxy.org',
'galaxy_base_url': (
f"https://{data['subdomain']}.usegalaxy.org"),
'section_paths': [
'sections/section-1.yml', # TODO: Auto-render from user input
],
})
return bootstrap.lab(data)
4 changes: 2 additions & 2 deletions app/labs/static/labs/content/docs/section_1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ tabs:

- id: workflows
title: Workflows
heading_md: |
heading_md: >
A workflow is a series of Galaxy tools that have been linked together
to perform a specific analysis. You can use and customize the example workflows
below.
[Learn more](https://galaxyproject.org/learn/advanced-workflow/).
<a href="https://galaxyproject.org/learn/advanced-workflow">Learn more</a>.
content:
- button_link: "{{ galaxy_base_url }}/workflows/trs_import?trs_server=workflowhub.eu&run_form=true&trs_id=222"
button_tip: Import to Galaxy AU
Expand Down
38 changes: 29 additions & 9 deletions app/labs/static/labs/content/docs/templates/intro.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@
</a>
</li>
<li>
See a full working example of an exported Galaxy Lab page
See a full working example of a Galaxy Lab page
<a href="/?content_root={{ EXAMPLE_LABS.FULL.RAW_URL }}">here</a>.
</li>
<li>
See a minimal working example of a lab page
<a href="/?content_root={{ EXAMPLE_LABS.SIMPLE.RAW_URL }}">here</a>.
</li>
<li>
Start a new Lab page in 30 seconds
<a href="/bootstrap">here</a>.
</li>
<li>
View the full <code>section.yml</code> schema documentation
<a href="/schema">here</a>.
</li>
</ul>

<p>
Expand Down Expand Up @@ -76,7 +84,8 @@ <h2 class="accordion-header" id="headingOne">
<a href="{{ EXAMPLE_LABS.DOCS.WEB_DIR_ROOT }}/section_1.yml" target="_blank">section_1.yml</a>
and
<a href="{{ EXAMPLE_LABS.DOCS.WEB_DIR_ROOT }}/section_2.yml" target="_blank">section_2.yml</a>
for examples of how to structure this content in YAML format.
for examples of how to structure this content in YAML format, or check out the full
<a href="/schema">sections schema definition</a> to see exactly how this YAML should be structured.
</li>
</ul>
</p>
Expand All @@ -95,15 +104,15 @@ <h2 class="accordion-header" id="headingTwo">
<p>
<ol>
<li>
Copy our
<a href="{{ EXAMPLE_LABS.DOCS.WEB_DIR_URL }}" target="_blank">
example content
</a>
directory to your own github repository.
Generate a new content folder using our
<a href="/bootstrap">Lab Generator</a>.
</li>
<li>
Build your Galaxy Lab's info, tools and workflows by editing the YAML and HTML files.
Custom CSS and JavaScript are possible too.
Build on your Galaxy Lab content by editing the downloaded YAML
and MD/HTML files. Custom CSS and JavaScript are possible too.
Try to keep the content as concise and modular as possible, and
think about what essential resources your users should know about.
Critical Tools, Workflows and tutorials are a great place to start.
</li>
<li>
Request the site with the <code>content_root</code> GET parameter pointing to your remote content:
Expand Down Expand Up @@ -171,12 +180,22 @@ <h2 class="accordion-header" id="headingThree">
data-bs-target="#infoModal"
>Try one now!</a>
</p>

<p class="alert alert-info">
Try to make all Markdown/HTML content as modular as possible by using
variables defined in the <code>base.yml</code> and
<code>server.yml</code> files. This means that content will be rendered
differently on each Galaxy server, depending on what's in that
server's <code>server.yml</code> file!
</p>
</div>
</div>
</div>
</div>
</section>

<!-- Modals -->

<div class="modal fade" id="infoModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
Expand Down Expand Up @@ -206,6 +225,7 @@ <h5 class="modal-title">Galaxy {{ site_name }}</h5>


<script>
// Some fancy JavaScript to add "View on GitHub" overlay to each section
function addHoverInfo(id, relpath) {
const exportInfoButton = $(`
<a
Expand Down
2 changes: 1 addition & 1 deletion app/labs/static/labs/content/simple/base.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Default spec for an exported lab landing page

# You can test with: /?content_root=https://raw.githubusercontent.com/usegalaxy-au/galaxy-labs-engine/dev/app/labs/content/docs/base.yml
# You can test with: /?content_root=https://raw.githubusercontent.com/usegalaxy-au/galaxy-labs-engine/dev/app/labs/content/simple/base.yml

site_name: Archaeology
lab_name: Archaeology Lab
Expand Down
Loading

0 comments on commit 7b4b222

Please sign in to comment.