diff --git a/tools/app_generator/app.py b/tools/app_generator/app.py index 9ce1d3a8c7..e501fdf1b5 100644 --- a/tools/app_generator/app.py +++ b/tools/app_generator/app.py @@ -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/") +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]+.*(?") -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) diff --git a/tools/app_generator/requirements.txt b/tools/app_generator/requirements.txt index dde6e9b296..2c2bf55677 100644 --- a/tools/app_generator/requirements.txt +++ b/tools/app_generator/requirements.txt @@ -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 diff --git a/tools/app_generator/static/fetch_assets b/tools/app_generator/static/fetch_assets index b1a2689bd8..1be9859c0e 100644 --- a/tools/app_generator/static/fetch_assets +++ b/tools/app_generator/static/fetch_assets @@ -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/fork-awesome@1.2.0/css/fork-awesome.min.css > fork-awesome.min.css +sed -i 's@../fonts/@@g' ./fork-awesome.min.css +curl https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/fonts/forkawesome-webfont.woff2?v=1.2.0 > forkawesome-webfont.woff2 +curl https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/fonts/forkawesome-webfont.woff?v=1.2.0 > forkawesome-webfont.woff +curl https://cdn.jsdelivr.net/npm/fork-awesome@1.2.0/fonts/forkawesome-webfont.ttf?v=1.2.0 > forkawesome-webfont.ttf diff --git a/tools/app_generator/static/tailwind-local.css b/tools/app_generator/static/tailwind-local.css index 575f614eb9..69fb5155ef 100644 --- a/tools/app_generator/static/tailwind-local.css +++ b/tools/app_generator/static/tailwind-local.css @@ -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 { @@ -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; } diff --git a/tools/app_generator/static/tailwind.config.js b/tools/app_generator/static/tailwind.config.js index c5b0b2fc06..b1ebad7575 100644 --- a/tools/app_generator/static/tailwind.config.js +++ b/tools/app_generator/static/tailwind.config.js @@ -7,11 +7,5 @@ module.exports = { plugins: [ require('@tailwindcss/forms'), ], - safelist: [ - 'safelisted', - { - pattern: /^(text-[a-z]+-600|border-[a-z]+-400)$/, - }, - ] } diff --git a/tools/app_generator/templates/base.html b/tools/app_generator/templates/base.html index a4597eae26..3071a0efc0 100644 --- a/tools/app_generator/templates/base.html +++ b/tools/app_generator/templates/base.html @@ -1,12 +1,18 @@ - - + - {{ gettext("YunoHost app generator") }} + {{ _("YunoHost package generator") }} + {% if config.DEBUG %} + + + {% else %} + {% endif %}
diff --git a/tools/app_generator/templates/index.html b/tools/app_generator/templates/index.html index 62e95eff3f..d0e5c43689 100644 --- a/tools/app_generator/templates/index.html +++ b/tools/app_generator/templates/index.html @@ -1,9 +1,17 @@ -{% import "wtf.html" as wtf %} +{% macro form_errors(form, hiddens=True) %} + {%- if form.errors %} + {%- for fieldname, errors in form.errors.items() %} + {%- if is_hidden_field(form[fieldname]) and hiddens or + not is_hidden_field(form[fieldname]) and hiddens != 'only' %} + {%- for error in errors %} +

{{error}}

+ {%- endfor %} + {%- endif %} + {%- endfor %} + {%- endif %} +{%- endmacro %} -{% macro form_field(field, - form_type="basic", - horizontal_columns=('lg', 2, 10), - button_map={}) %} +{% macro form_field(field) %} {% if field.widget.input_type == 'checkbox' %}
-{% else %} - {{ wtf.form_field(field, form_type, horizontal_columns, button_map) }} +{%- elif field.type == 'RadioField' -%} + {% for item in field -%} +
+ +
+ {% endfor %} +{% else -%} +
+ {{field.label(class="control-label")|safe}} + {% if field.type == 'FileField' %} + {{field(**kwargs)|safe}} + {% else %} + {{field(class="form-control", **kwargs)|safe}} + {% endif %} + + {%- if field.errors %} + {%- for error in field.errors %} +

{{error}}

+ {%- endfor %} + {%- elif field.description -%} +

{{field.description|safe}}

+ {%- endif %} +
{% endif %} {% endmacro %} - {% extends "base.html" %} {% block main %}
YunoHost application logo

- {{ _("Yunohost application generation form") }} + {{ _("YunoHost package generator") }}

-

Version: {{ generator_info['GENERATOR_VERSION'] }}

+

Version: {{ config["GENERATOR_VERSION"] }}

- {% for lang in AVAILABLE_LANGUAGES.items() %} - + {% for lang_id, lang_label in config["LANGUAGES"].items() %} + {% endfor %}
@@ -44,17 +76,18 @@

{{ main_form.hidden_tag() }} -
- {{ wtf.form_errors(main_form, hiddens="only") }} +
+ {% if main_form.errors %}

{{ _("The form contains issues") }}

{% endif %} + {{ form_errors(main_form, hiddens="only") }}

{{ form_field(main_form.generator_mode) }}
-

{{ gettext("1/9 - General information") }}

+

{{ _("1/9 - General information") }}

-