Skip to content

Commit

Permalink
app_generator: more cleanup, misc bug fixes, simplify code and develo…
Browse files Browse the repository at this point in the history
…pment flow (#2313)
  • Loading branch information
alexAubin authored May 8, 2024
1 parent 43aebcc commit f61a6da
Show file tree
Hide file tree
Showing 14 changed files with 299 additions and 2,089 deletions.
163 changes: 49 additions & 114 deletions tools/app_generator/app.py
Original file line number Diff line number Diff line change
@@ -1,152 +1,107 @@
#### Imports
import logging
from io import BytesIO
import re
import os
import jinja2 as j2
import logging
import zipfile
import random
import string
from io import BytesIO
from flask import (
Flask,
render_template,
render_template_string,
request,
redirect,
flash,
send_file,
make_response,
session,
)
from markupsafe import Markup # No longer imported from Flask

# Form libraries
from flask_wtf import FlaskForm
from flask_babel import Babel, lazy_gettext as _

from wtforms import (
StringField,
RadioField,
SelectField,
SubmitField,
TextAreaField,
BooleanField,
SelectMultipleField,
HiddenField,
)
from wtforms.validators import (
DataRequired,
InputRequired,
Optional,
Regexp,
URL,
Length,
)
from wtforms.fields import HiddenField

# Translations
from flask_babel import Babel
from flask_babel import lazy_gettext as _

from flask import redirect, request, make_response # Language swap by redirecting

# Markdown to HTML - for debugging purposes
from misaka import Markdown, HtmlRenderer
YOLOGEN_VERSION = "0.11"
LANGUAGES = {"en": _("English"), "fr": _("French")}

# Managing zipfiles
import zipfile
from flask_cors import CORS
from urllib import parse
from secrets import token_urlsafe
###############################################################################
# App initialization, misc configs
###############################################################################

logger = logging.getLogger()

#### GLOBAL VARIABLES
YOLOGEN_VERSION = "0.10"
GENERATOR_DICT = {"GENERATOR_VERSION": YOLOGEN_VERSION}

#### Create FLASK and Jinja Environments
app = Flask(__name__)
app.config["SECRET_KEY"] = token_urlsafe(16) # Necessary for the form CORS
cors = CORS(app)

environment = j2.Environment(loader=j2.FileSystemLoader("templates/"))
app = Flask(__name__, static_url_path="/static", static_folder="static")

if app.config.get("DEBUG"):
app.config["TEMPLATES_AUTO_RELOAD"] = True

def is_hidden_field_filter(field):
app.config["LANGUAGES"] = LANGUAGES
app.config["GENERATOR_VERSION"] = YOLOGEN_VERSION

return isinstance(field, HiddenField)
# This is the secret key used for session signing
app.secret_key = "".join(random.choice(string.ascii_lowercase) for i in range(32))


app.jinja_env.globals["bootstrap_is_hidden_field"] = is_hidden_field_filter
def get_locale():
return (
session.get("lang")
or request.accept_languages.best_match(LANGUAGES.keys())
or "en"
)

# Handle translations
BABEL_TRANSLATION_DIRECTORIES = "translations"

babel = Babel()

LANGUAGES = {"en": _("English"), "fr": _("French")}
babel = Babel(app, locale_selector=get_locale)


@app.context_processor
def inject_conf_var():
return dict(AVAILABLE_LANGUAGES=LANGUAGES)


def configure(app):
babel.init_app(app, locale_selector=get_locale)
app.config["LANGUAGES"] = LANGUAGES


def get_locale():
print(request.accept_languages.best_match(app.config["LANGUAGES"].keys()))
print(request.cookies.get("lang", "en"))
# return 'en' # to test
# return 'fr'
if request.args.get("language"):
print(request.args.get("language"))
session["language"] = request.args.get("language")
return request.cookies.get("lang", "en")
# return request.accept_languages.best_match(app.config['LANGUAGES'].keys()) # The result is based on the Accept-Language header. For testing purposes, you can directly return a language code, for example: return ‘de’

def jinja_globals():

configure(app)
d = {
"locale": get_locale(),
}

#### Custom functions
if app.config.get("DEBUG"):
d["tailwind_local"] = open("static/tailwind-local.css").read()

return d

# Define custom filter
@app.template_filter("render_markdown")
def render_markdown(text):
renderer = HtmlRenderer()
markdown = Markdown(renderer)
return markdown(text)

app.jinja_env.globals["is_hidden_field"] = lambda field: isinstance(field, HiddenField)

# Add custom filter
j2.filters.FILTERS["render_markdown"] = render_markdown

@app.route("/lang/<lang>")
def set_lang(lang=None):

# Converting markdown to html
def markdown_file_to_html_string(file):
with open(file, "r") as file:
markdown_content = file.read()
# Convert content from Markdown to HTML
html_content = render_markdown(markdown_content)
# Return Markdown and HTML contents
return markdown_content, html_content
assert lang in app.config["LANGUAGES"].keys()
session["lang"] = lang

return make_response(redirect(request.referrer or "/"))

### Forms


# Language selector. Not used (in GeneratorForm) until it's fixed or superseeded.
# Use it in the HTML with {{ form_field(main_form.generator_language) }}
class Translations(FlaskForm):
generator_language = SelectField(
_("Select language"),
choices=[("none", "")] + [language for language in LANGUAGES.items()],
default=["en"],
id="selectLanguage",
)
###############################################################################
# Forms
###############################################################################


class GeneralInfos(FlaskForm):

app_id = StringField(
Markup(_("Application identifier (id)")),
_("Application identifier (id)"),
description=_("Small caps and without spaces"),
validators=[DataRequired(), Regexp("[a-z_1-9]+.*(?<!_ynh)$")],
render_kw={
Expand Down Expand Up @@ -525,10 +480,8 @@ class AppConfig(FlaskForm):
class Documentation(FlaskForm):
# TODO : # screenshot
description = TextAreaField(
Markup(
_(
"""doc/DESCRIPTION.md: A comprehensive presentation of the app, possibly listing the main features, possible warnings and specific details on its functioning in Yunohost (e.g. warning about integration issues)."""
)
_(
"doc/DESCRIPTION.md: A comprehensive presentation of the app, possibly listing the main features, possible warnings and specific details on its functioning in Yunohost (e.g. warning about integration issues)."
),
validators=[Optional()],
render_kw={
Expand Down Expand Up @@ -701,7 +654,6 @@ def main_form_route():
return render_template(
"index.html",
main_form=main_form,
generator_info=GENERATOR_DICT,
generated_files={},
)

Expand Down Expand Up @@ -761,29 +713,21 @@ def __init__(self, id_, destination_path=None):
template_dir = os.path.dirname(__file__) + "/templates/"
for app_file in app_files:
template = open(template_dir + app_file.id + ".j2").read()
app_file.content = render_template_string(
template, data=dict(request.form | GENERATOR_DICT)
)
app_file.content = render_template_string(template, data=dict(request.form))
app_file.content = re.sub(r"\n\s+$", "\n", app_file.content, flags=re.M)
app_file.content = re.sub(r"\n{3,}", "\n\n", app_file.content, flags=re.M)

print(main_form.use_custom_config_file.data)
if main_form.use_custom_config_file.data:
app_files.append(
AppFile("appconf", "conf/" + main_form.custom_config_file.data)
)
app_files[-1].content = main_form.custom_config_file_content.data
print(main_form.custom_config_file.data)
print(main_form.custom_config_file_content.data)

# TODO : same for cron job
if submit_mode == "download":
# Generate the zip file
f = BytesIO()
with zipfile.ZipFile(f, "w") as zf:
print("Exporting zip archive for app: " + request.form["app_id"])
for app_file in app_files:
print(app_file.id)
zf.writestr(app_file.destination_path, app_file.content)
f.seek(0)
# Send the zip file to the user
Expand All @@ -794,19 +738,10 @@ def __init__(self, id_, destination_path=None):
return render_template(
"index.html",
main_form=main_form,
generator_info=GENERATOR_DICT,
generated_files=app_files,
)


# Localisation
@app.route("/language/<language>")
def set_language(language=None):
response = make_response(redirect(request.referrer or "/"))
response.set_cookie("lang", language)
return response


#### Running the web server
if __name__ == "__main__":
app.run(debug=True)
26 changes: 11 additions & 15 deletions tools/app_generator/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
blinker==1.6.3
cffi==1.16.0
Babel==2.15.0
blinker==1.8.2
click==8.1.7
dominate==2.8.0
Flask==3.0.0
flask_babel~=4.0.0
Flask-Cors==4.0.0
Flask-Misaka==1.0.0
Flask==3.0.3
flask-babel==4.0.0
Flask-WTF==1.2.1
itsdangerous==2.1.2
Jinja2==3.1.4
MarkupSafe==2.1.3
misaka==2.1.1
pycparser==2.21
visitor==0.1.3
Werkzeug==3.0.1
WTForms==3.0.1
gunicorn==22.0.0
itsdangerous==2.2.0
Jinja2==3.1.4
MarkupSafe==2.1.5
packaging==24.0
pytz==2024.1
Werkzeug==3.0.3
WTForms==3.1.2
11 changes: 10 additions & 1 deletion tools/app_generator/static/fetch_assets
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Download standalone tailwind to compile what we need
# Production -> download standalone tailwind to compile only what we need
wget https://github.com/tailwindlabs/tailwindcss/releases/download/v3.3.3/tailwindcss-linux-x64
chmod +x tailwindcss-linux-x64
./tailwindcss-linux-x64 --input tailwind-local.css --output tailwind.css --minify

# Development -> we use the JS magic thingy
curl -L https://cdn.tailwindcss.com?plugins=forms > tailwind-css.js

# Forkawesome
curl https://cdn.jsdelivr.net/npm/[email protected]/css/fork-awesome.min.css > fork-awesome.min.css
sed -i 's@../fonts/@@g' ./fork-awesome.min.css
curl https://cdn.jsdelivr.net/npm/[email protected]/fonts/forkawesome-webfont.woff2?v=1.2.0 > forkawesome-webfont.woff2
curl https://cdn.jsdelivr.net/npm/[email protected]/fonts/forkawesome-webfont.woff?v=1.2.0 > forkawesome-webfont.woff
curl https://cdn.jsdelivr.net/npm/[email protected]/fonts/forkawesome-webfont.ttf?v=1.2.0 > forkawesome-webfont.ttf

34 changes: 9 additions & 25 deletions tools/app_generator/static/tailwind-local.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@
}

h2 {
@apply text-xl font-bold;
@apply text-xl font-medium;
}

h3 {
@apply text-lg font-bold;
@apply text-lg font-medium;
}

.hide {
Expand All @@ -43,50 +43,34 @@
@apply block rounded-lg border border-gray-400 mb-2;
}
.panel-heading {
@apply text-white bg-blue-500 hover:bg-blue-700 p-2 font-bold;
@apply text-white bg-blue-500 hover:bg-blue-600 px-2 py-1.5;
}
.panel-body {
@apply p-2;
transition: max-height 0.2s ease-in;
max-height: 0px;
}

.alert-info {
@apply text-blue-900 border-blue-900 bg-blue-200 rounded-lg p-4;
}

.active, .collapse-button:hover {
background-color: #318ddc;
}

.collapse-title:after {
content: '\002B';
color: white;
font-weight: bold;
float: right;
margin-left: 5px;
}

.expanded .collapse-title::after {
content: "\2212";
margin-right: 5px;
}

.collapsed {
padding: 0px 15px 0px 15px;
}

.collapsible {
max-height: 0px;
overflow: hidden;
transition: max-height 0.2s ease-out;
.active .collapse-title:after {
content: '\2212';
}

label {
@apply font-bold;
}

input {
@apply rounded-lg;
}

.form-group, .checkbox {
@apply px-2 py-4;
}
Expand Down
Loading

0 comments on commit f61a6da

Please sign in to comment.