Add files via upload

This commit is contained in:
Joshua Hare 2025-05-11 20:35:32 +10:00 committed by GitHub
parent a2b827d02f
commit bb7016cb8e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 62 additions and 44 deletions

View File

@ -8,4 +8,4 @@ COPY instance/ instance/
COPY config.py . COPY config.py .
COPY .env . COPY .env .
EXPOSE 5009 EXPOSE 5009
CMD ["flask", "run", "--host=0.0.0.0" "--port=5009"] CMD ["flask", "run", "--host=0.0.0.0", "--port=5009"]

View File

@ -1,5 +1,5 @@
from flask_wtf import FlaskForm from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed from flask_wtf.file import FileField, FileAllowed, MultipleFileField
from wtforms import StringField, TextAreaField, SubmitField, PasswordField, SelectField, SelectMultipleField, BooleanField from wtforms import StringField, TextAreaField, SubmitField, PasswordField, SelectField, SelectMultipleField, BooleanField
from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional, ValidationError from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional, ValidationError
import re import re
@ -9,10 +9,12 @@ from app import db
def validate_url_or_path(form, field): def validate_url_or_path(form, field):
if not field.data: if not field.data:
return return
if field.data.startswith('/app/uploads/'): # Allow upload paths like /uploads/<uuid>_<filename>.jpg|png
path_pattern = r'^/app/uploads/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[\w\-\.]+\.(jpg|png)$' if field.data.startswith('/uploads/'):
path_pattern = r'^/uploads/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[\w\-\.]+\.(jpg|png)$'
if re.match(path_pattern, field.data, re.I): if re.match(path_pattern, field.data, re.I):
return return
# Allow external URLs
url_pattern = r'^(https?:\/\/)?([\w\-]+\.)+[\w\-]+(\/[\w\-\.]*)*\/?(\?[^\s]*)?(#[^\s]*)?$' url_pattern = r'^(https?:\/\/)?([\w\-]+\.)+[\w\-]+(\/[\w\-\.]*)*\/?(\?[^\s]*)?(#[^\s]*)?$'
if not re.match(url_pattern, field.data): if not re.match(url_pattern, field.data):
raise ValidationError('Invalid URL or file path.') raise ValidationError('Invalid URL or file path.')
@ -44,7 +46,7 @@ class FHIRAppForm(FlaskForm):
licensing_pricing = SelectField('Licensing & Pricing', coerce=int, validators=[DataRequired()]) licensing_pricing = SelectField('Licensing & Pricing', coerce=int, validators=[DataRequired()])
os_support = SelectMultipleField('OS Support', coerce=int, validators=[DataRequired()]) os_support = SelectMultipleField('OS Support', coerce=int, validators=[DataRequired()])
app_image_urls = TextAreaField('App Image URLs (one per line)', validators=[Optional(), Length(max=1000)], render_kw={"placeholder": "e.g., https://example.com/image1.png"}) app_image_urls = TextAreaField('App Image URLs (one per line)', validators=[Optional(), Length(max=1000)], render_kw={"placeholder": "e.g., https://example.com/image1.png"})
app_image_uploads = FileField('Upload App Images', validators=[FileAllowed(['jpg', 'png'], 'Images only!')]) app_image_uploads = MultipleFileField('Upload App Images', validators=[FileAllowed(['jpg', 'png'], 'Images only!')])
submit = SubmitField('Register App') submit = SubmitField('Register App')
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):

View File

@ -154,10 +154,9 @@ def register():
if form.app_image_urls.data: if form.app_image_urls.data:
app_images.extend([url.strip() for url in form.app_image_urls.data.splitlines() if url.strip().startswith(('http://', 'https://'))]) app_images.extend([url.strip() for url in form.app_image_urls.data.splitlines() if url.strip().startswith(('http://', 'https://'))])
if form.app_image_uploads.data: if form.app_image_uploads.data:
file = form.app_image_uploads.data for file in form.app_image_uploads.data:
if file and allowed_file(file.filename): if file and allowed_file(file.filename):
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
(my apologies, I did not mean to interrupt you, please go ahead and continue.
save_path = os.path.join(UPLOAD_FOLDER, filename) save_path = os.path.join(UPLOAD_FOLDER, filename)
logger.debug(f"Attempting to save app image to {save_path}") logger.debug(f"Attempting to save app image to {save_path}")
try: try:
@ -258,7 +257,7 @@ def edit_app(app_id):
app_images = [url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()] app_images = [url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()]
if form.app_image_uploads.data: if form.app_image_uploads.data:
file = form.app_image_uploads.data for file in form.app_image_uploads.data:
if file and allowed_file(file.filename): if file and allowed_file(file.filename):
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
save_path = os.path.join(UPLOAD_FOLDER, filename) save_path = os.path.join(UPLOAD_FOLDER, filename)

View File

@ -114,7 +114,8 @@
</div> </div>
<div class="mb-3"> <div class="mb-3">
{{ form.app_image_uploads.label(class="form-label") }} {{ form.app_image_uploads.label(class="form-label") }}
{{ form.app_image_uploads(class="form-control") }} {{ form.app_image_uploads(class="form-control", multiple=True) }}
<small class="form-text text-muted">Select multiple images to upload.</small>
{% for error in form.app_image_uploads.errors %} {% for error in form.app_image_uploads.errors %}
<span class="text-danger">{{ error }}</span> <span class="text-danger">{{ error }}</span>
{% endfor %} {% endfor %}

View File

@ -124,7 +124,7 @@
<div class="app-item col-md-4 mb-3" style="animation: fadeIn 0.5s;"> <div class="app-item col-md-4 mb-3" style="animation: fadeIn 0.5s;">
<div class="card app-card shadow-sm h-100"> <div class="card app-card shadow-sm h-100">
{% if app.logo_url %} {% if app.logo_url %}
<img src="{{ app.logo_url }}" class="card-img-top" alt="{{ app.name }} logo" style="max-height: 150px; object-fit: contain; padding: 1rem;" onerror="this.style.display='none';"> <img src="{{ app.logo_url }}" class="card-img-top" alt="{{ app.name }} logo" style="max-height: 150px; object-fit: contain; padding: 1rem;" onerror="this.src='https://via.placeholder.com/150?text=No+Logo';">
{% else %} {% else %}
<img src="https://via.placeholder.com/150?text=No+Logo" class="card-img-top" alt="No Logo" style="max-height: 150px; object-fit: contain; padding: 1rem;"> <img src="https://via.placeholder.com/150?text=No+Logo" class="card-img-top" alt="No Logo" style="max-height: 150px; object-fit: contain; padding: 1rem;">
{% endif %} {% endif %}
@ -132,6 +132,14 @@
<h5 class="card-title">{{ app.name }}</h5> <h5 class="card-title">{{ app.name }}</h5>
<p class="card-text flex-grow-1">{{ app.description | truncate(100) }}</p> <p class="card-text flex-grow-1">{{ app.description | truncate(100) }}</p>
<p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p> <p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p>
<div class="d-flex flex-wrap gap-2 mb-2">
{% if app.website %}
<a href="{{ app.website }}" class="btn btn-primary btn-sm" aria-label="Visit {{ app.name }} website">Website</a>
{% endif %}
{% if app.launch_url %}
<a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
{% endif %}
</div>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a> <a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>
</div> </div>
</div> </div>

View File

@ -18,7 +18,7 @@
<div class="col-md-4 mb-3"> <div class="col-md-4 mb-3">
<div class="card app-card shadow-sm h-100"> <div class="card app-card shadow-sm h-100">
{% if app.logo_url %} {% if app.logo_url %}
<img src="{{ app.logo_url }}" class="card-img-top" alt="{{ app.name }} logo" style="max-height: 150px; object-fit: contain; padding: 1rem;"> <img src="{{ app.logo_url }}" class="card-img-top" alt="{{ app.name }} logo" style="max-height: 150px; object-fit: contain; padding: 1rem;" onerror="this.src='https://via.placeholder.com/150?text=No+Logo';">
{% else %} {% else %}
<img src="https://via.placeholder.com/150?text=No+Logo" class="card-img-top" alt="No Logo" style="max-height: 150px; object-fit: contain; padding: 1rem;"> <img src="https://via.placeholder.com/150?text=No+Logo" class="card-img-top" alt="No Logo" style="max-height: 150px; object-fit: contain; padding: 1rem;">
{% endif %} {% endif %}
@ -26,6 +26,14 @@
<h5 class="card-title">{{ app.name }}</h5> <h5 class="card-title">{{ app.name }}</h5>
<p class="card-text flex-grow-1">{{ app.description | truncate(100) }}</p> <p class="card-text flex-grow-1">{{ app.description | truncate(100) }}</p>
<p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p> <p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p>
<div class="d-flex flex-wrap gap-2 mb-2">
{% if app.website %}
<a href="{{ app.website }}" class="btn btn-primary btn-sm" aria-label="Visit {{ app.name }} website">Website</a>
{% endif %}
{% if app.launch_url %}
<a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
{% endif %}
</div>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a> <a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>
</div> </div>
</div> </div>

View File

@ -10,4 +10,4 @@ services:
environment: environment:
- FLASK_APP=app - FLASK_APP=app
- FLASK_ENV=development - FLASK_ENV=development
command: ["flask", "run", "--host=0.0.0.0"] command: ["flask", "run", "--host=0.0.0.0", "--port=5009"]