# 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 from app.forms import SmartAppForm, GalleryFilterForm from sqlalchemy import or_, and_ import os import logging from werkzeug.utils import secure_filename import uuid # Setup logging logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) gallery_bp = Blueprint('gallery', __name__) # Absolute path to the upload folder inside the container UPLOAD_FOLDER = '/app/uploads/' ALLOWED_EXTENSIONS = {'jpg', 'png'} def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @gallery_bp.route('/uploads/') def uploaded_file(filename): absolute_upload_folder = os.path.abspath(UPLOAD_FOLDER) logger.debug(f"Attempting to serve file: {filename} from {absolute_upload_folder}") try: return send_from_directory(absolute_upload_folder, filename) except FileNotFoundError: logger.error(f"File not found: {os.path.join(absolute_upload_folder, filename)}") abort(404) @gallery_bp.route('/') def index(): return redirect(url_for('gallery.gallery')) @gallery_bp.route('/gallery', methods=['GET', 'POST']) def gallery(): form = GalleryFilterForm() 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) fhir_support_ids = request.args.getlist('fhir_support', type=int) speciality_ids = request.args.getlist('speciality', type=int) pricing_license_ids = request.args.getlist('pricing_license', type=int) designed_for_ids = request.args.getlist('designed_for', type=int) ehr_support_ids = request.args.getlist('ehr_support', type=int) if application_type_ids: query = query.filter(SmartApp.application_type_id.in_(application_type_ids)) filter_params['application_type'] = application_type_ids if category_ids: query = query.filter(or_(*[SmartApp.categories.contains(str(cid)) for cid in category_ids])) filter_params['category'] = category_ids if os_support_ids: query = query.filter(or_(*[SmartApp.os_support.contains(str(oid)) for oid in os_support_ids])) filter_params['os_support'] = os_support_ids if fhir_support_ids: query = query.filter(SmartApp.fhir_compatibility_id.in_(fhir_support_ids)) filter_params['fhir_support'] = fhir_support_ids if speciality_ids: query = query.filter(or_(*[SmartApp.specialties.contains(str(sid)) for sid in speciality_ids])) filter_params['speciality'] = speciality_ids if pricing_license_ids: query = query.filter(SmartApp.licensing_pricing_id.in_(pricing_license_ids)) filter_params['pricing_license'] = pricing_license_ids if designed_for_ids: query = query.filter(SmartApp.designed_for_id.in_(designed_for_ids)) filter_params['designed_for'] = designed_for_ids if ehr_support_ids: query = query.filter(or_(*[SmartApp.ehr_support.contains(str(eid)) for eid in ehr_support_ids])) filter_params['ehr_support'] = ehr_support_ids apps = query.all() for app in apps: logger.debug(f"App ID: {app.id}, logo_url: {app.logo_url}") return render_template( 'gallery.html', apps=apps, form=form, filter_params=filter_params, application_types=ApplicationType.query.all(), categories=Category.query.all(), os_supports=OSSupport.query.all(), fhir_supports=FHIRSupport.query.all(), specialties=Speciality.query.all(), pricing_licenses=PricingLicense.query.all(), designed_fors=DesignedFor.query.all(), ehr_supports=EHRSupport.query.all() ) @gallery_bp.route('/gallery/') def app_detail(app_id): app = SmartApp.query.get_or_404(app_id) logger.debug(f"App Detail ID: {app_id}, logo_url: {app.logo_url}, app_images: {app.app_images}") app_categories = [] if app.categories: category_ids = [int(cid) for cid in app.categories.split(',') if cid] app_categories = Category.query.filter(Category.id.in_(category_ids)).all() app_specialties = [] if app.specialties: speciality_ids = [int(sid) for sid in app.specialties.split(',') if sid] app_specialties = Speciality.query.filter(Speciality.id.in_(speciality_ids)).all() app_os_supports = [] if app.os_support: os_ids = [int(oid) for oid in app.os_support.split(',') if oid] app_os_supports = OSSupport.query.filter(OSSupport.id.in_(os_ids)).all() app_ehr_supports = [] if app.ehr_support: ehr_ids = [int(eid) for eid in app.ehr_support.split(',') if eid] app_ehr_supports = EHRSupport.query.filter(EHRSupport.id.in_(ehr_ids)).all() return render_template( 'app_detail.html', app=app, app_categories=app_categories, app_specialties=app_specialties, app_os_supports=app_os_supports, app_ehr_supports=app_ehr_supports ) @gallery_bp.route('/gallery/register', methods=['GET', 'POST']) @login_required def register(): form = SmartAppForm() if form.validate_on_submit(): scopes = form.scopes.data valid_scopes = all( scope.strip().startswith(('patient/', 'user/', 'launch', 'openid', 'fhirUser', 'offline_access', 'online_access')) for scope in scopes.split(',') if scope.strip() ) if not valid_scopes or not scopes.strip(): flash('Invalid SMART scopes. Use formats like patient/Patient.read, launch/patient.', 'danger') return render_template('register.html', form=form) try: os.makedirs(UPLOAD_FOLDER, exist_ok=True) logger.debug(f"Ensured {UPLOAD_FOLDER} exists") except Exception as e: logger.error(f"Failed to create {UPLOAD_FOLDER}: {e}") flash('Error creating upload directory.', 'danger') return render_template('register.html', form=form) if form.application_type_new.data: app_type = ApplicationType(name=form.application_type_new.data) db.session.add(app_type) db.session.commit() form.application_type.data = app_type.id if form.categories_new.data: category = Category(name=form.categories_new.data) db.session.add(category) db.session.commit() 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() form.os_support.data.append(os_support.id) if form.fhir_compatibility_new.data: fhir = FHIRSupport(name=form.fhir_compatibility_new.data) db.session.add(fhir) db.session.commit() form.fhir_compatibility.data = fhir.id if form.specialties_new.data: speciality = Speciality(name=form.specialties_new.data) db.session.add(speciality) db.session.commit() form.specialties.data.append(speciality.id) if form.licensing_pricing_new.data: pricing = PricingLicense(name=form.licensing_pricing_new.data) db.session.add(pricing) db.session.commit() form.licensing_pricing.data = pricing.id if form.designed_for_new.data: designed = DesignedFor(name=form.designed_for_new.data) db.session.add(designed) db.session.commit() form.designed_for.data = designed.id if form.ehr_support_new.data: ehr = EHRSupport(name=form.ehr_support_new.data) db.session.add(ehr) db.session.commit() form.ehr_support.data.append(ehr.id) 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}") save_path = os.path.join(UPLOAD_FOLDER, filename) logger.debug(f"Attempting to save logo to {save_path}") try: file.save(save_path) if os.path.exists(save_path): logger.debug(f"Successfully saved logo to {save_path}") else: logger.error(f"Failed to save logo to {save_path}") flash('Failed to save logo.', 'danger') return render_template('register.html', form=form) 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) app_images = [] 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://'))]) if form.app_image_uploads.data: file = form.app_image_uploads.data if file and allowed_file(file.filename): filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") save_path = os.path.join(UPLOAD_FOLDER, filename) logger.debug(f"Attempting to save app image to {save_path}") try: file.save(save_path) if os.path.exists(save_path): logger.debug(f"Successfully saved app image to {save_path}") else: 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) 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') return render_template('register.html', form=form) app = SmartApp( name=form.name.data, description=form.description.data, developer=form.developer.data, contact_email=form.contact_email.data, logo_url=logo_url or None, launch_url=form.launch_url.data, client_id=form.client_id.data, scopes=scopes, website=form.website.data or None, designed_for_id=form.designed_for.data, application_type_id=form.application_type.data, fhir_compatibility_id=form.fhir_compatibility.data, categories=','.join(map(str, form.categories.data)) if form.categories.data else None, 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, ehr_support=','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None, user_id=current_user.id ) db.session.add(app) try: db.session.commit() logger.debug(f"Registered app ID: {app.id}, logo_url: {app.logo_url}, app_images: {app.app_images}") except Exception as e: logger.error(f"Error committing app to database: {e}") db.session.rollback() flash('Error saving app to database.', 'danger') return render_template('register.html', form=form) flash('App registered successfully!', 'success') return redirect(url_for('gallery.gallery')) return render_template('register.html', form=form) @gallery_bp.route('/gallery/edit/', methods=['GET', 'POST']) @login_required def edit_app(app_id): app = SmartApp.query.get_or_404(app_id) if app.user_id != current_user.id: flash('You can only edit your own apps.', 'danger') return redirect(url_for('gallery.app_detail', app_id=app_id)) form = SmartAppForm(obj=app) if not form.is_submitted(): if app.categories: form.categories.data = [int(cid) for cid in app.categories.split(',') if cid] if app.specialties: form.specialties.data = [int(sid) for sid in app.specialties.split(',') if sid] if app.os_support: 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] if app.app_images: 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 = '' if form.validate_on_submit(): scopes = form.scopes.data valid_scopes = all( scope.strip().startswith(('patient/', 'user/', 'launch', 'openid', 'fhirUser', 'offline_access', 'online_access')) for scope in scopes.split(',') if scope.strip() ) if not valid_scopes or not scopes.strip(): flash('Invalid SMART scopes. Use formats like patient/Patient.read, launch/patient.', 'danger') return render_template('edit_app.html', form=form, app=app) try: os.makedirs(UPLOAD_FOLDER, exist_ok=True) logger.debug(f"Ensured {UPLOAD_FOLDER} exists") except Exception as e: logger.error(f"Failed to create {UPLOAD_FOLDER}: {e}") flash('Error creating upload directory.', 'danger') return render_template('edit_app.html', form=form, app=app) if form.application_type_new.data: app_type = ApplicationType(name=form.application_type_new.data) db.session.add(app_type) db.session.commit() form.application_type.data = app_type.id if form.categories_new.data: category = Category(name=form.categories_new.data) db.session.add(category) db.session.commit() 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 = [] form.os_support.data.append(os_support.id) if form.fhir_compatibility_new.data: fhir = FHIRSupport(name=form.fhir_compatibility_new.data) db.session.add(fhir) db.session.commit() form.fhir_compatibility.data = fhir.id if form.specialties_new.data: speciality = Speciality(name=form.specialties_new.data) db.session.add(speciality) db.session.commit() 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) db.session.add(pricing) db.session.commit() form.licensing_pricing.data = pricing.id if form.designed_for_new.data: designed = DesignedFor(name=form.designed_for_new.data) db.session.add(designed) db.session.commit() form.designed_for.data = designed.id if form.ehr_support_new.data: 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 = [] form.ehr_support.data.append(ehr.id) 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}") 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}") 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) 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: logo_url = app.logo_url app_images = [url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()] if form.app_image_uploads.data: file = form.app_image_uploads.data if file and allowed_file(file.filename): filename = secure_filename(f"{uuid.uuid4()}_{file.filename}") 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}") 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) 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) 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 app.launch_url = form.launch_url.data app.client_id = form.client_id.data app.scopes = scopes app.website = form.website.data or None app.designed_for_id = form.designed_for.data app.application_type_id = form.application_type.data app.fhir_compatibility_id = form.fhir_compatibility.data app.categories = ','.join(map(str, form.categories.data)) if form.categories.data else None 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 app.ehr_support = ','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None try: db.session.commit() logger.debug(f"Updated app ID: {app.id}, logo_url: {app.logo_url}, app_images: {app.app_images}") except Exception as e: logger.error(f"Error committing app update to database: {e}") db.session.rollback() flash('Error updating app in database.', 'danger') return render_template('edit_app.html', form=form, app=app) flash('App updated successfully!', 'success') return redirect(url_for('gallery.app_detail', app_id=app_id)) return render_template('edit_app.html', form=form, app=app) @gallery_bp.route('/gallery/delete/', methods=['POST']) @login_required def delete_app(app_id): app = SmartApp.query.get_or_404(app_id) if app.user_id != current_user.id: flash('You can only delete your own apps.', 'danger') return redirect(url_for('gallery.app_detail', app_id=app_id)) db.session.delete(app) db.session.commit() flash(f'App "{app.name}" deleted successfully.', 'success') return redirect(url_for('gallery.gallery')) @gallery_bp.route('/my-listings') @login_required def my_listings(): apps = SmartApp.query.filter_by(user_id=current_user.id).all() return render_template('my_listings.html', apps=apps) @gallery_bp.route('/test/add') @login_required def add_test_app(): logo_url = "https://via.placeholder.com/150" app_images = "https://via.placeholder.com/300" test_app = SmartApp( name="Test App", description="A sample SMART on FHIR app.", developer="Test Developer", contact_email="test@example.com", logo_url=logo_url, launch_url="https://example.com/launch", client_id="test-client-id", scopes="patient/Patient.read,launch/patient", website="https://example.com", designed_for_id=DesignedFor.query.filter_by(name="Clinicians").first().id if DesignedFor.query.filter_by(name="Clinicians").first() else None, application_type_id=ApplicationType.query.filter_by(name="SMART").first().id if ApplicationType.query.filter_by(name="SMART").first() else None, fhir_compatibility_id=FHIRSupport.query.filter_by(name="R4").first().id if FHIRSupport.query.filter_by(name="R4").first() else None, categories=','.join(str(c.id) for c in Category.query.filter(Category.name.in_(["Clinical", "Patient Engagement"])).all()) if Category.query.filter(Category.name.in_(["Clinical", "Patient Engagement"])).all() else None, specialties=','.join(str(s.id) for s in Speciality.query.filter(Speciality.name.in_(["Cardiology", "General Practice"])).all()) if Speciality.query.filter(Speciality.name.in_(["Cardiology", "General Practice"])).all() else None, licensing_pricing_id=PricingLicense.query.filter_by(name="Free").first().id if PricingLicense.query.filter_by(name="Free").first() else None, os_support=','.join(str(o.id) for o in OSSupport.query.filter(OSSupport.name.in_(["Web", "iOS", "Android"])).all()) if OSSupport.query.filter(OSSupport.name.in_(["Web", "iOS", "Android"])).all() else None, app_images=app_images, ehr_support=','.join(str(e.id) for e in EHRSupport.query.filter(EHRSupport.name.in_(["Epic", "Cerner"])).all()) if EHRSupport.query.filter(EHRSupport.name.in_(["Epic", "Cerner"])).all() else None, user_id=current_user.id ) db.session.add(test_app) db.session.commit() logger.debug(f"Test app ID: {test_app.id}, logo_url: {test_app.logo_url}") flash('Test app added successfully!', 'success') return redirect(url_for('gallery.gallery'))