mirror of
https://github.com/YunoHost/apps.git
synced 2024-09-03 20:06:07 +02:00
appstore: draft add to wishlist form + process
This commit is contained in:
parent
efb4555a5c
commit
1995213f52
5 changed files with 190 additions and 14 deletions
118
store/app.py
118
store/app.py
|
@ -1,3 +1,4 @@
|
||||||
|
import re
|
||||||
import toml
|
import toml
|
||||||
import base64
|
import base64
|
||||||
import hashlib
|
import hashlib
|
||||||
|
@ -7,7 +8,9 @@ import random
|
||||||
import urllib
|
import urllib
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from slugify import slugify
|
||||||
from flask import Flask, send_from_directory, render_template, session, redirect, request
|
from flask import Flask, send_from_directory, render_template, session, redirect, request
|
||||||
|
from github import Github, InputGitAuthor
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
catalog = json.load(open("apps.json"))
|
catalog = json.load(open("apps.json"))
|
||||||
|
@ -15,14 +18,24 @@ catalog['categories'] = {c['id']:c for c in catalog['categories']}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
config = toml.loads(open("config.toml").read())
|
config = toml.loads(open("config.toml").read())
|
||||||
DISCOURSE_SSO_SECRET = config["DISCOURSE_SSO_SECRET"]
|
|
||||||
DISCOURSE_SSO_ENDPOINT = config["DISCOURSE_SSO_ENDPOINT"]
|
|
||||||
CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = config["CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE"]
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("You should create a config.toml with the appropriate key/values, cf config.toml.example")
|
print("You should create a config.toml with the appropriate key/values, cf config.toml.example")
|
||||||
print(e)
|
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
mandatory_config_keys = [
|
||||||
|
"DISCOURSE_SSO_SECRET",
|
||||||
|
"DISCOURSE_SSO_ENDPOINT",
|
||||||
|
"CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE",
|
||||||
|
"GITHUB_LOGIN",
|
||||||
|
"GITHUB_TOKEN",
|
||||||
|
"GITHUB_EMAIL",
|
||||||
|
]
|
||||||
|
|
||||||
|
for key in mandatory_config_keys:
|
||||||
|
if key not in config:
|
||||||
|
print(f"Missing key in config.toml: {key}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
if config.get("DEBUG"):
|
if config.get("DEBUG"):
|
||||||
app.debug = True
|
app.debug = True
|
||||||
app.config["DEBUG"] = True
|
app.config["DEBUG"] = True
|
||||||
|
@ -48,7 +61,7 @@ category_color = {
|
||||||
for id_, category in catalog['categories'].items():
|
for id_, category in catalog['categories'].items():
|
||||||
category["color"] = category_color[id_]
|
category["color"] = category_color[id_]
|
||||||
|
|
||||||
wishlist = json.load(open("wishlist.json"))
|
wishlist = toml.load(open("../wishlist.toml"))
|
||||||
|
|
||||||
# This is the secret key used for session signing
|
# This is the secret key used for session signing
|
||||||
app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)])
|
app.secret_key = ''.join([str(random.randint(0, 9)) for i in range(99)])
|
||||||
|
@ -113,6 +126,95 @@ def browse_wishlist():
|
||||||
return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist)
|
return render_template("wishlist.html", user=session.get('user', {}), wishlist=wishlist)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/wishlist/add', methods=['GET', 'POST'])
|
||||||
|
def add_to_wishlist():
|
||||||
|
if request.method == "POST":
|
||||||
|
|
||||||
|
user = session.get('user', {})
|
||||||
|
if not user:
|
||||||
|
errormsg = "You must be logged in to submit an app to the wishlist"
|
||||||
|
return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=errormsg)
|
||||||
|
|
||||||
|
name = request.form['name'].strip().replace("\n", "")
|
||||||
|
description = request.form['description'].strip().replace("\n", "")
|
||||||
|
upstream = request.form['upstream'].strip().replace("\n", "")
|
||||||
|
website = request.form['website'].strip().replace("\n", "")
|
||||||
|
|
||||||
|
checks = [
|
||||||
|
(len(name) >= 3, "App name should be at least 3 characters"),
|
||||||
|
(len(name) <= 30, "App name should be less than 30 characters"),
|
||||||
|
(len(description) >= 5, "App name should be at least 5 characters"),
|
||||||
|
(len(description) <= 100, "App name should be less than 100 characters"),
|
||||||
|
(len(upstream) >= 10, "Upstream code repo URL should be at least 10 characters"),
|
||||||
|
(len(upstream) <= 150, "Upstream code repo URL should be less than 150 characters"),
|
||||||
|
(len(website) <= 150, "Website URL should be less than 150 characters"),
|
||||||
|
(re.match(r"^[\w\.\-\(\)\ ]+$", name), "App name contains special characters"),
|
||||||
|
]
|
||||||
|
|
||||||
|
for check, errormsg in checks:
|
||||||
|
if not check:
|
||||||
|
return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=errormsg)
|
||||||
|
|
||||||
|
slug = slugify(name)
|
||||||
|
|
||||||
|
github = Github((config["GITHUB_LOGIN"], config["GITHUB_TOKEN"]))
|
||||||
|
author = InputGitAuthor(config["GITHUB_LOGIN"], config["GITHUB_EMAIL"])
|
||||||
|
repo = github.get_repo("Yunohost/apps")
|
||||||
|
current_wishlist_rawtoml = repo.get_contents("wishlist.toml", ref="app-store") # FIXME : ref=repo.default_branch)
|
||||||
|
current_wishlist_sha = current_wishlist_rawtoml.sha
|
||||||
|
current_wishlist_rawtoml = current_wishlist_rawtoml.decoded_content.decode()
|
||||||
|
new_wishlist = toml.loads(current_wishlist_rawtoml)
|
||||||
|
|
||||||
|
if slug in new_wishlist:
|
||||||
|
return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=f"An entry with the name {slug} already exists in the wishlist")
|
||||||
|
|
||||||
|
new_wishlist[slug] = {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"upstream": upstream,
|
||||||
|
"website": website,
|
||||||
|
}
|
||||||
|
|
||||||
|
new_wishlist = dict(sorted(new_wishlist.items()))
|
||||||
|
new_wishlist_rawtoml = toml.dumps(new_wishlist)
|
||||||
|
new_branch = f"add-to-wishlist-{slug}"
|
||||||
|
try:
|
||||||
|
# Get the commit base for the new branch, and create it
|
||||||
|
commit_sha = repo.get_branch("app-store").commit.sha # FIXME app-store -> repo.default_branch
|
||||||
|
repo.create_git_ref(ref=f"refs/heads/{new_branch}", sha=commit_sha)
|
||||||
|
except:
|
||||||
|
print("... Branch already exists, skipping")
|
||||||
|
return False
|
||||||
|
|
||||||
|
message = f"Add {name} to wishlist"
|
||||||
|
repo.update_file(
|
||||||
|
"wishlist.toml",
|
||||||
|
message=message,
|
||||||
|
content=new_wishlist,
|
||||||
|
sha=current_wishlist_sha,
|
||||||
|
branch=new_branch,
|
||||||
|
author=author,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Wait a bit to preserve the API rate limit
|
||||||
|
time.sleep(1.5)
|
||||||
|
|
||||||
|
body = f"""
|
||||||
|
### Add {name} to wishlist
|
||||||
|
|
||||||
|
- [ ] Confirm app is self-hostable and generally makes sense to possibly integrate in YunoHost
|
||||||
|
- [ ] Confirm app's license is opensource/free software (or not-totally-free, case by case TBD)
|
||||||
|
- [ ] Description describes concisely what the app is/does
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Open the PR
|
||||||
|
pr = self.repo.create_pull(
|
||||||
|
title=message, body=body, head=new_branch, base="app-store" # FIXME app-store -> repo.default_branch
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return render_template("wishlist_add.html", user=session.get('user', {}), errormsg=None)
|
||||||
|
|
||||||
|
|
||||||
################################################
|
################################################
|
||||||
|
|
||||||
|
@ -123,11 +225,11 @@ def create_nonce_and_build_url_to_login_on_discourse_sso():
|
||||||
|
|
||||||
nonce = ''.join([str(random.randint(0, 9)) for i in range(99)])
|
nonce = ''.join([str(random.randint(0, 9)) for i in range(99)])
|
||||||
|
|
||||||
url_data = {"nonce": nonce, "return_sso_url": CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE}
|
url_data = {"nonce": nonce, "return_sso_url": config["CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE"]}
|
||||||
url_encoded = urllib.parse.urlencode(url_data)
|
url_encoded = urllib.parse.urlencode(url_data)
|
||||||
payload = base64.b64encode(url_encoded.encode()).decode()
|
payload = base64.b64encode(url_encoded.encode()).decode()
|
||||||
sig = hmac.new(DISCOURSE_SSO_SECRET.encode(), msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
|
sig = hmac.new(config["DISCOURSE_SSO_SECRET"].encode(), msg=payload.encode(), digestmod=hashlib.sha256).hexdigest()
|
||||||
data = {"sig": sig, "sso": payload}
|
data = {"sig": sig, "sso": payload}
|
||||||
url = f"{DISCOURSE_SSO_ENDPOINT}?{urllib.parse.urlencode(data)}"
|
url = f"{config['DISCOURSE_SSO_ENDPOINT']}?{urllib.parse.urlencode(data)}"
|
||||||
|
|
||||||
return nonce, url
|
return nonce, url
|
||||||
|
|
|
@ -3,3 +3,6 @@ DISCOURSE_SSO_SECRET = "abcdefghijklmnopqrstuvwxyz1234567890"
|
||||||
DISCOURSE_SSO_ENDPOINT = "https://forum.yunohost.org/session/sso_provider"
|
DISCOURSE_SSO_ENDPOINT = "https://forum.yunohost.org/session/sso_provider"
|
||||||
CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = "http://localhost:5000/sso_login_callback"
|
CALLBACK_URL_AFTER_LOGIN_ON_DISCOURSE = "http://localhost:5000/sso_login_callback"
|
||||||
DEBUG = false
|
DEBUG = false
|
||||||
|
GITHUB_LOGIN = "yunohost-bot"
|
||||||
|
GITHUB_EMAIL = "yunohost [at] yunohost.org" # Replace the [at] by actual @
|
||||||
|
GITHUB_TOKEN = "superSecretToken"
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
Flask==2.3.2
|
Flask==2.3.2
|
||||||
|
python-slugify
|
||||||
|
PyGithub
|
||||||
|
toml
|
||||||
|
|
|
@ -25,8 +25,8 @@
|
||||||
|
|
||||||
<a
|
<a
|
||||||
|
|
||||||
class="inline-block rounded border text-blue-600 border-blue-500 px-4 pt-3 text-sm font-medium hover:bg-blue-500 hover:text-white"
|
class="inline-block rounded-md border text-blue-600 border-blue-500 px-4 pt-3 text-sm font-medium hover:bg-blue-500 hover:text-white"
|
||||||
href="#"
|
href="{{ url_for('add_to_wishlist') }}"
|
||||||
>
|
>
|
||||||
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
<i class="fa fa-plus fa-fw" aria-hidden="true"></i>
|
||||||
Add an app to the wishlist
|
Add an app to the wishlist
|
||||||
|
@ -55,7 +55,7 @@
|
||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<tbody class="divide-y divide-gray-200">
|
<tbody class="divide-y divide-gray-200">
|
||||||
{% for infos in wishlist %}
|
{% for app, infos in wishlist.items() %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="px-4 py-2 font-bold text-gray-900 max-w-[10em]">
|
<td class="px-4 py-2 font-bold text-gray-900 max-w-[10em]">
|
||||||
{{ infos['name'] }}
|
{{ infos['name'] }}
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
href="{{ infos['website'] }}"
|
href="{{ infos['website'] }}"
|
||||||
class="inline-block"
|
class="inline-block"
|
||||||
>
|
>
|
||||||
<i class="fa fa-globe rounded border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
|
<i class="fa fa-globe rounded-md border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
@ -77,14 +77,14 @@
|
||||||
href="{{ infos['upstream'] }}"
|
href="{{ infos['upstream'] }}"
|
||||||
class="inline-block"
|
class="inline-block"
|
||||||
>
|
>
|
||||||
<i class="fa fa-code rounded border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
|
<i class="fa fa-code rounded-md border px-3 py-2 hover:bg-gray-100" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-1 py-2">
|
<td class="px-1 py-2">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="inline-block rounded border text-blue-600 border-blue-500 px-4 py-2 text-xs font-medium hover:bg-blue-500 hover:text-white"
|
class="inline-block rounded-md border text-blue-600 border-blue-500 px-4 py-2 text-xs font-medium hover:bg-blue-500 hover:text-white"
|
||||||
>
|
>
|
||||||
<i class="fa fa-bookmark fa-fw" aria-hidden="true"></i>
|
<i class="fa fa-bookmark fa-fw" aria-hidden="true"></i>
|
||||||
Vote
|
Vote
|
||||||
|
|
68
store/templates/wishlist_add.html
Normal file
68
store/templates/wishlist_add.html
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block main %}
|
||||||
|
<div class="mt-5 text-center">
|
||||||
|
<h2 class="text-2xl font-bold text-gray-900">
|
||||||
|
Add an application to the wishlist
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="overflow-x-auto max-w-md mx-auto pt-5">
|
||||||
|
|
||||||
|
{% if not user %}
|
||||||
|
<div role="alert" class="rounded-md border-s-4 border-orange-500 bg-orange-50 p-4 mb-5">
|
||||||
|
<p class="mt-2 text-sm text-orange-700 font-bold">
|
||||||
|
<i class="fa fa-exclamation-triangle fa-fw" aria-hidden="true"></i>
|
||||||
|
You must first login to be allowed to submit an app to the wishlist
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div role="alert" class="rounded-md border-s-4 border-sky-500 bg-sky-50 p-4">
|
||||||
|
<p class="mt-2 text-sm text-sky-700 font-bold">
|
||||||
|
<i class="fa fa-info-circle fa-fw" aria-hidden="true"></i>
|
||||||
|
Please check the license of the app your are proposing
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-sky-700">
|
||||||
|
The YunoHost project will only package free/open-source software (with possible case-by-case exceptions for apps which are not-totally-free)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if errormsg %}
|
||||||
|
<div role="alert" class="rounded-md border-s-4 border-red-500 bg-red-50 p-4 my-5">
|
||||||
|
<p class="mt-2 text-sm text-red-700 font-bold">
|
||||||
|
<i class="fa fa-exclamation-triangle fa-fw" aria-hidden="true"></i>
|
||||||
|
{{ errormsg }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
<form method="POST" action="{{ url_for('add_to_wishlist') }}" class="mt-8 mb-8" >
|
||||||
|
|
||||||
|
<label for="name" class="mt-5 block font-bold text-gray-700">Name</label>
|
||||||
|
<input name="name" type="text" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" maxlength="30" required onkeyup="this.value = this.value.replace(/[^a-zA-Z0-9.-\\(\\)\\ ]/, '')" />
|
||||||
|
|
||||||
|
<label for="description" class="mt-5 block font-bold text-gray-700">Description</label>
|
||||||
|
<textarea name="description" type="text" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" required rows='3' maxlength='100'></textarea>
|
||||||
|
<span class="text-xs text-gray-600"><span class="font-bold">Please be concise and focus on what the app does.</span> No need to repeat "[App] is ...". No need to state that it is free/open-source or self-hosted (otherwise it wouldn't be packaged for YunoHost). Avoid marketing stuff like 'the most', or vague properties like 'easy', 'simple', 'lightweight'.</span>
|
||||||
|
|
||||||
|
<label for="upstream" class="mt-5 block font-bold text-gray-700">Project code repository</label>
|
||||||
|
<input name="upstream" type="url" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" maxlength="150" required />
|
||||||
|
|
||||||
|
<label for="website" class="mt-5 block font-bold text-gray-700">Project website</label>
|
||||||
|
<input name="website" type="url" class="w-full mt-1 rounded-md border-gray-200 text-gray-700 shadow-sm" maxlength="150" />
|
||||||
|
<span class="text-xs text-gray-600">Please <emph>do not</emph> just copy-paste the code repository URL. If the project has no proper website, then leave the field empty.</span>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="mx-auto block rounded-md border text-white bg-blue-500 px-4 mt-5 py-2 font-medium {% if user %}hover:bg-blue-700{% endif %}"
|
||||||
|
{% if not user %}disabled{% endif %}
|
||||||
|
>
|
||||||
|
Submit
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
Loading…
Add table
Reference in a new issue