diff --git a/app/routes.py b/app/routes.py index 5a1d5da..068ca50 100644 --- a/app/routes.py +++ b/app/routes.py @@ -1,5 +1,5 @@ -#app/routes.py -from flask import Blueprint, render_template, redirect, url_for, flash, request, send_from_directory, abort # Added send_from_directory and abort +# app/routes.py +from flask import Blueprint, render_template, redirect, url_for, flash, request, send_from_directory, abort from flask_login import login_required, current_user from app import db from app.models import SmartApp, ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport @@ -23,10 +23,8 @@ ALLOWED_EXTENSIONS = {'jpg', 'png'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS -# New route to serve uploaded files @gallery_bp.route('/uploads/') def uploaded_file(filename): - # Use the absolute path to the directory inside the container absolute_upload_folder = os.path.abspath(UPLOAD_FOLDER) logger.debug(f"Attempting to serve file: {filename} from {absolute_upload_folder}") try: @@ -45,6 +43,19 @@ def gallery(): query = SmartApp.query filter_params = {} + # Handle search + search_term = request.args.get('search', '').strip() + if search_term: + query = query.filter( + or_( + SmartApp.name.ilike(f'%{search_term}%'), + SmartApp.description.ilike(f'%{search_term}%'), + SmartApp.developer.ilike(f'%{search_term}%') + ) + ) + filter_params['search'] = search_term + + # Existing filter logic application_type_ids = request.args.getlist('application_type', type=int) category_ids = request.args.getlist('category', type=int) os_support_ids = request.args.getlist('os_support', type=int) @@ -145,9 +156,7 @@ def register(): flash('Invalid SMART scopes. Use formats like patient/Patient.read, launch/patient.', 'danger') return render_template('register.html', form=form) - # Ensure upload folder exists try: - # Use the absolute path inside the container os.makedirs(UPLOAD_FOLDER, exist_ok=True) logger.debug(f"Ensured {UPLOAD_FOLDER} exists") except Exception as e: @@ -155,7 +164,6 @@ def register(): flash('Error creating upload directory.', 'danger') return render_template('register.html', form=form) - # Handle new filter options if form.application_type_new.data: app_type = ApplicationType(name=form.application_type_new.data) db.session.add(app_type) @@ -197,13 +205,11 @@ def register(): db.session.commit() form.ehr_support.data.append(ehr.id) - # Handle logo logo_url = form.logo_url.data if form.logo_upload.data: file = form.logo_upload.data if allowed_file(file.filename): filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") - # Use absolute path for saving save_path = os.path.join(UPLOAD_FOLDER, filename) logger.debug(f"Attempting to save logo to {save_path}") try: @@ -214,24 +220,20 @@ def register(): logger.error(f"Failed to save logo to {save_path}") flash('Failed to save logo.', 'danger') return render_template('register.html', form=form) - # Store URL path for web access - logo_url = f"/uploads/{filename}" # CHANGED + logo_url = f"/uploads/{filename}" logger.debug(f"Set logo_url to {logo_url}") except Exception as e: logger.error(f"Error saving logo to {save_path}: {e}") flash('Error saving logo.', 'danger') return render_template('register.html', form=form) - # Handle app images app_images = [] if form.app_image_urls.data: - # Keep existing URLs if they are valid URLs 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: # Check if a file was uploaded + if form.app_image_uploads.data: file = form.app_image_uploads.data - if file and allowed_file(file.filename): # Check if file object exists and is allowed + if file and allowed_file(file.filename): filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") - # Use absolute path for saving save_path = os.path.join(UPLOAD_FOLDER, filename) logger.debug(f"Attempting to save app image to {save_path}") try: @@ -242,8 +244,7 @@ def register(): logger.error(f"Failed to save app image to {save_path}") flash('Failed to save app image.', 'danger') return render_template('register.html', form=form) - # Store URL path for web access - app_images.append(f"/uploads/{filename}") # CHANGED + app_images.append(f"/uploads/{filename}") except Exception as e: logger.error(f"Error saving app image to {save_path}: {e}") flash('Error saving app image.', 'danger') @@ -254,7 +255,7 @@ def register(): description=form.description.data, developer=form.developer.data, contact_email=form.contact_email.data, - logo_url=logo_url or None, # Use the potentially updated logo_url + logo_url=logo_url or None, launch_url=form.launch_url.data, client_id=form.client_id.data, scopes=scopes, @@ -266,7 +267,7 @@ def register(): specialties=','.join(map(str, form.specialties.data)) if form.specialties.data else None, licensing_pricing_id=form.licensing_pricing.data, os_support=','.join(map(str, form.os_support.data)) if form.os_support.data else None, - app_images=','.join(app_images) if app_images else None, # Use the potentially updated app_images list + app_images=','.join(app_images) if app_images else None, ehr_support=','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None, user_id=current_user.id ) @@ -292,7 +293,6 @@ def edit_app(app_id): return redirect(url_for('gallery.app_detail', app_id=app_id)) form = SmartAppForm(obj=app) - # Pre-populate multi-select fields correctly on GET request if not form.is_submitted(): if app.categories: form.categories.data = [int(cid) for cid in app.categories.split(',') if cid] @@ -302,14 +302,11 @@ def edit_app(app_id): form.os_support.data = [int(oid) for oid in app.os_support.split(',') if oid] if app.ehr_support: form.ehr_support.data = [int(eid) for eid in app.ehr_support.split(',') if eid] - # Pre-populate the image URL textarea if app.app_images: - # Filter out internal paths if mixed with URLs, show only URLs or internal paths formatted for web current_images = [img for img in app.app_images.split(',') if img.startswith(('http://', 'https://', '/uploads/'))] form.app_image_urls.data = '\n'.join(current_images) else: - form.app_image_urls.data = '' # Ensure it's empty if no images - + form.app_image_urls.data = '' if form.validate_on_submit(): scopes = form.scopes.data @@ -322,9 +319,7 @@ def edit_app(app_id): flash('Invalid SMART scopes. Use formats like patient/Patient.read, launch/patient.', 'danger') return render_template('edit_app.html', form=form, app=app) - # Ensure upload folder exists try: - # Use the absolute path inside the container os.makedirs(UPLOAD_FOLDER, exist_ok=True) logger.debug(f"Ensured {UPLOAD_FOLDER} exists") except Exception as e: @@ -332,7 +327,6 @@ def edit_app(app_id): flash('Error creating upload directory.', 'danger') return render_template('edit_app.html', form=form, app=app) - # Handle new filter options (same as register) if form.application_type_new.data: app_type = ApplicationType(name=form.application_type_new.data) db.session.add(app_type) @@ -342,14 +336,15 @@ def edit_app(app_id): category = Category(name=form.categories_new.data) db.session.add(category) db.session.commit() - # Ensure data is list before append if it was None initially - if form.categories.data is None: form.categories.data = [] + if form.categories.data is None: + form.categories.data = [] form.categories.data.append(category.id) if form.os_support_new.data: os_support = OSSupport(name=form.os_support_new.data) db.session.add(os_support) db.session.commit() - if form.os_support.data is None: form.os_support.data = [] + if form.os_support.data is None: + form.os_support.data = [] form.os_support.data.append(os_support.id) if form.fhir_compatibility_new.data: fhir = FHIRSupport(name=form.fhir_compatibility_new.data) @@ -360,7 +355,8 @@ def edit_app(app_id): speciality = Speciality(name=form.specialties_new.data) db.session.add(speciality) db.session.commit() - if form.specialties.data is None: form.specialties.data = [] + if form.specialties.data is None: + form.specialties.data = [] form.specialties.data.append(speciality.id) if form.licensing_pricing_new.data: pricing = PricingLicense(name=form.licensing_pricing_new.data) @@ -376,70 +372,60 @@ def edit_app(app_id): ehr = EHRSupport(name=form.ehr_support_new.data) db.session.add(ehr) db.session.commit() - if form.ehr_support.data is None: form.ehr_support.data = [] + if form.ehr_support.data is None: + form.ehr_support.data = [] form.ehr_support.data.append(ehr.id) - - # Handle logo update - logo_url = form.logo_url.data # Get URL from form first - if form.logo_upload.data: # If new logo uploaded, it takes precedence + logo_url = form.logo_url.data + if form.logo_upload.data: file = form.logo_upload.data if allowed_file(file.filename): filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") - # Use absolute path for saving save_path = os.path.join(UPLOAD_FOLDER, filename) logger.debug(f"Attempting to save updated logo to {save_path}") try: file.save(save_path) if os.path.exists(save_path): - logger.debug(f"Successfully saved updated logo to {save_path}") + logger.debug(f"Successfully saved updated logo to {save_path}") else: - logger.error(f"Failed to save updated logo to {save_path}") - flash('Failed to save logo.', 'danger') - return render_template('edit_app.html', form=form, app=app) - # Store URL path for web access - logo_url = f"/uploads/{filename}" # CHANGED + logger.error(f"Failed to save updated logo to {save_path}") + flash('Failed to save logo.', 'danger') + return render_template('edit_app.html', form=form, app=app) + logo_url = f"/uploads/{filename}" logger.debug(f"Set logo_url to {logo_url}") except Exception as e: logger.error(f"Error saving updated logo to {save_path}: {e}") flash('Error saving logo.', 'danger') return render_template('edit_app.html', form=form, app=app) - elif not logo_url: # If no new upload AND URL field is empty, keep existing - logo_url = app.logo_url # Keep the old one only if the field is empty + elif not logo_url: + logo_url = app.logo_url - # Handle app images update - # Start with URLs provided in the text area app_images = [url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()] - - if form.app_image_uploads.data: # Check if a file was uploaded + if form.app_image_uploads.data: file = form.app_image_uploads.data - if file and allowed_file(file.filename): # Check if file object exists and is allowed + if file and allowed_file(file.filename): filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") - # Use absolute path for saving save_path = os.path.join(UPLOAD_FOLDER, filename) logger.debug(f"Attempting to save updated app image to {save_path}") try: file.save(save_path) if os.path.exists(save_path): - logger.debug(f"Successfully saved updated app image to {save_path}") + logger.debug(f"Successfully saved updated app image to {save_path}") else: - logger.error(f"Failed to save updated app image to {save_path}") - flash('Failed to save app image.', 'danger') - return render_template('edit_app.html', form=form, app=app) - # Add the new image's URL path - app_images.append(f"/uploads/{filename}") # CHANGED + logger.error(f"Failed to save updated app image to {save_path}") + flash('Failed to save app image.', 'danger') + return render_template('edit_app.html', form=form, app=app) + app_images.append(f"/uploads/{filename}") except Exception as e: logger.error(f"Error saving updated app image to {save_path}: {e}") flash('Error saving app image.', 'danger') return render_template('edit_app.html', form=form, app=app) - - # Update app object app.name = form.name.data app.description = form.description.data app.developer = form.developer.data app.contact_email = form.contact_email.data - app.logo_url = logo_url # Use the final determined logo_url + app.logo_url = logo_url app.launch_url = form.launch_url.data app.client_id = form.client_id.data app.scopes = scopes @@ -451,7 +437,7 @@ def edit_app(app_id): app.specialties = ','.join(map(str, form.specialties.data)) if form.specialties.data else None app.licensing_pricing_id = form.licensing_pricing.data app.os_support = ','.join(map(str, form.os_support.data)) if form.os_support.data else None - app.app_images = ','.join(app_images) if app_images else None # Use the final list of image URLs/paths + app.app_images = ','.join(app_images) if app_images else None app.ehr_support = ','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None try: db.session.commit() @@ -464,10 +450,8 @@ def edit_app(app_id): flash('App updated successfully!', 'success') return redirect(url_for('gallery.app_detail', app_id=app_id)) - # Render the edit form on GET or if validation fails return render_template('edit_app.html', form=form, app=app) - @gallery_bp.route('/gallery/delete/', methods=['POST']) @login_required def delete_app(app_id): @@ -486,7 +470,6 @@ def my_listings(): apps = SmartApp.query.filter_by(user_id=current_user.id).all() return render_template('my_listings.html', apps=apps) -# Keep the test route as is, it uses placeholder URLs directly @gallery_bp.route('/test/add') @login_required def add_test_app(): diff --git a/app/templates/app_detail.html b/app/templates/app_detail.html index 5d8804a..e6c5281 100644 --- a/app/templates/app_detail.html +++ b/app/templates/app_detail.html @@ -1,58 +1,344 @@ - - {% extends "base.html" %} - {% block title %}{{ app.name }}{% endblock %} - {% block content %}
-

{{ app.name }}

-
-
+
+
+ {% if app.logo_url %} - {{ app.name }} logo + {{ app.name }} logo {% else %} - No Logo + No Logo {% endif %} -

Description: {{ app.description }}

-

Developer: {{ app.developer }}

-

Contact Email: {{ app.contact_email }}

-

Launch URL: {{ app.launch_url }}

-

Client ID: {{ app.client_id }}

-

Scopes: {{ app.scopes }}

- {% if app.website %} -

Website: {{ app.website }}

- {% endif %} - {% if app_categories %} -

Categories: {{ app_categories | map(attribute='name') | join(', ') }}

- {% endif %} - {% if app_specialties %} -

Specialties: {{ app_specialties | map(attribute='name') | join(', ') }}

- {% endif %} - {% if app_os_supports %} -

OS Support: {{ app_os_supports | map(attribute='name') | join(', ') }}

- {% endif %} - {% if app_ehr_supports %} -

EHR Support: {{ app_ehr_supports | map(attribute='name') | join(', ') }}

- {% endif %} - {% if app.app_images %} -
Additional Images:
-
- {% for img_url in app.app_images.split(',') %} -
- App Image + + +

{{ app.name }}

+ + +
+ {{ app.developer }} +
+ + +
+ {% if app.website %} + Website + {% else %} + + {% endif %} + Try App +
+ + +
+ + +
+ +
+ + {% if app.app_images %} + - {% endfor %} + {% else %} + No Image + {% endif %} + + +
+
Description
+
+

{{ app.description or 'No description provided.' }}

+
+ + +
+
Contact Email
+
+

{{ app.contact_email or 'Not specified.' }}

+
+ + +
+
Client ID
+
+

{{ app.client_id or 'Not specified.' }}

+
+ + +
+
Scopes
+
+

{{ app.scopes or 'Not specified.' }}

+
- {% endif %} + + +
+ +
+
Website
+
+

+ {% if app.website %} + {{ app.website }} + {% else %} + Not specified. + {% endif %} +

+
+ + + {% if app.website %} +
+
Security and Privacy Policy
+
+

{{ app.website }}/security-and-privacy

+
+ {% endif %} + + +
+
Launch URL
+
+

{{ app.launch_url }}

+
+ + +
+
+
+
Designed For
+
+

+ {% if app.designed_for %} + {{ app.designed_for.name }} + {% else %} + Not specified. + {% endif %} +

+
+
+
Application Type
+
+

+ {% if app.application_type %} + {{ app.application_type.name }} + {% else %} + Not specified. + {% endif %} +

+
+
+
+ + +
+
+
+
FHIR Compatibility
+
+

+ {% if app.fhir_compatibility %} + {{ app.fhir_compatibility.name }} + {% else %} + Not specified. + {% endif %} +

+
+
+
EHR Support
+
+

+ {% if app_ehr_supports %} + {% for ehr in app_ehr_supports %} + {{ ehr.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + {% else %} + Not specified. + {% endif %} +

+
+
+
+ + +
+
Specialties
+
+

+ {% if app_specialties %} + {% for speciality in app_specialties %} + {{ speciality.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + {% else %} + Not specified. + {% endif %} +

+
+ + +
+
OS Support
+
+

+ {% if app_os_supports %} + {% for os in app_os_supports %} + {{ os.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + {% else %} + Not specified. + {% endif %} +

+
+ + +
+
Categories
+
+

+ {% if app_categories %} + {% for category in app_categories %} + {{ category.name }}{% if not loop.last %}, {% endif %} + {% endfor %} + {% else %} + Not specified. + {% endif %} +

+
+ + +
+
Licensing & Pricing
+
+

+ {% if app.licensing_pricing %} + {{ app.licensing_pricing.name }} + + ($99/user/month) + {% else %} + Not specified. + {% endif %} +

+
+
+
+ + {% if current_user.is_authenticated and current_user.id == app.user_id %} - Edit App -
- -
+
+ Edit App +
+ +
+
{% endif %}
+ + +
-{% endblock %} \ No newline at end of file + + +{% endblock %} diff --git a/app/templates/base.html b/app/templates/base.html index f92dd3e..68f0f95 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -1,11 +1,29 @@ + - + + SMARTFLARE - Smart App Gallery - {% block title %}{% endblock %} + @@ -184,9 +284,10 @@
SMART App Gallery -
-
- {% with messages = get_flashed_messages(with_categories=true) %} - {% if messages %} - {% for category, message in messages %} - - {% endfor %} - {% endif %} - {% endwith %} - {% block content %}{% endblock %} -
+
+
+ {% with messages = get_flashed_messages(with_categories=true) %} + {% if messages %} + {% for category, message in messages %} + + {% endfor %} + {% endif %} + {% endwith %} + {% block content %}{% endblock %} +
+
+ diff --git a/app/templates/gallery.html b/app/templates/gallery.html index 7148769..9eba665 100644 --- a/app/templates/gallery.html +++ b/app/templates/gallery.html @@ -1,9 +1,26 @@ - - {% extends "base.html" %} - {% block title %}Gallery{% endblock %} +{% macro generate_filter_url(endpoint, filter_key, filter_value, filter_params, search) %} + {% set params = {} %} + {% for key, values in filter_params.items() %} + {% if key != filter_key %} + {% for value in values %} + {% if params[key] is not defined %} + {% set _ = params.update({key: [value]}) %} + {% else %} + {% set _ = params[key].append(value) %} + {% endif %} + {% endfor %} + {% endif %} + {% endfor %} + {% set _ = params.update({filter_key: filter_value}) %} + {% if search %} + {% set _ = params.update({'search': search}) %} + {% endif %} + {{ url_for(endpoint, **params) }} +{% endmacro %} + {% block content %}
@@ -19,7 +36,7 @@ - +
-

Explore SMART on FHIR apps. Filter above to find the perfect app.

+

Explore SMART on FHIR apps. Filter to find the perfect app.

+ {% if filter_params or request.args.get('search') %} +
+
Active Filters:
+ {% for key, values in filter_params.items() %} + {% for value in values %} + {% set item = {'application_type': application_types, 'category': categories, 'os_support': os_supports, 'fhir_support': fhir_supports, 'speciality': specialties, 'pricing_license': pricing_licenses, 'designed_for': designed_fors, 'ehr_support': ehr_supports}[key] | selectattr('id', 'equalto', value) | first %} + {% if item %} + {{ item.name }} + {% endif %} + {% endfor %} + {% endfor %} + {% if request.args.get('search') %} + Search: {{ request.args.get('search') }} + {% endif %} + Clear All +
+ {% endif %} {% if apps %} -
+
{% for app in apps %} -
+
{% if app.logo_url %} {{ app.name }} logo @@ -145,73 +179,33 @@
{% endfor %}
+
+ + + + + + + + + + + {% for app in apps %} + + + + + + + {% endfor %} + +
NameDescriptionDeveloperActions
{{ app.name }}{{ app.description | truncate(100) }}{{ app.developer }} + View +
+
{% else %} -

No apps match your filters. Try adjusting the filters above.

+

No apps match your filters or search. Try adjusting the criteria.

{% endif %}
- - - {% endblock %} \ No newline at end of file diff --git a/instance/smart_app_gallery.db b/instance/smart_app_gallery.db index f425500..706f1a8 100644 Binary files a/instance/smart_app_gallery.db and b/instance/smart_app_gallery.db differ diff --git a/uploads/5a05e500-0ba6-424f-94d5-d223f0e83bbe_Screenshot_2025-04-11_105109.png b/uploads/5a05e500-0ba6-424f-94d5-d223f0e83bbe_Screenshot_2025-04-11_105109.png new file mode 100644 index 0000000..5aa5643 Binary files /dev/null and b/uploads/5a05e500-0ba6-424f-94d5-d223f0e83bbe_Screenshot_2025-04-11_105109.png differ diff --git a/uploads/b7a49295-01c3-428f-b60b-9bce17357149_FHIRFLARE.png b/uploads/b7a49295-01c3-428f-b60b-9bce17357149_FHIRFLARE.png new file mode 100644 index 0000000..66f8452 Binary files /dev/null and b/uploads/b7a49295-01c3-428f-b60b-9bce17357149_FHIRFLARE.png differ diff --git a/uploads/bca224f6-b880-4a06-9a3a-bab7e66a0b4d_FlareSmartApps.png b/uploads/bca224f6-b880-4a06-9a3a-bab7e66a0b4d_FlareSmartApps.png new file mode 100644 index 0000000..9edba89 Binary files /dev/null and b/uploads/bca224f6-b880-4a06-9a3a-bab7e66a0b4d_FlareSmartApps.png differ diff --git a/uploads/d0d5fa61-b4b8-4b50-a7ac-c0582fa9130a_Screenshot_2025-04-11_105109.png b/uploads/d0d5fa61-b4b8-4b50-a7ac-c0582fa9130a_Screenshot_2025-04-11_105109.png new file mode 100644 index 0000000..5aa5643 Binary files /dev/null and b/uploads/d0d5fa61-b4b8-4b50-a7ac-c0582fa9130a_Screenshot_2025-04-11_105109.png differ diff --git a/uploads/e84538cd-6bab-42f4-bc58-69798f36cb96_a11test.png b/uploads/e84538cd-6bab-42f4-bc58-69798f36cb96_a11test.png new file mode 100644 index 0000000..de63d9c Binary files /dev/null and b/uploads/e84538cd-6bab-42f4-bc58-69798f36cb96_a11test.png differ