Add files via upload

This commit is contained in:
Joshua Hare 2025-04-15 18:09:35 +10:00 committed by GitHub
parent 1d5909861e
commit b97d1b175d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 1991 additions and 0 deletions

12
Dockerfile Normal file
View File

@ -0,0 +1,12 @@
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ app/
COPY static/ static/
COPY instance/ instance/
copy uploads app/uploads/
COPY config.py .
COPY .env .
EXPOSE 5000
CMD ["flask", "run", "--host=0.0.0.0"]

63
app/__init__.py Normal file
View File

@ -0,0 +1,63 @@
#app/__init__.py
import os
import hashlib
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager
from flask_oauthlib.client import OAuth
from config import Config
from datetime import datetime
db = SQLAlchemy()
login_manager = LoginManager()
oauth = OAuth()
def create_app():
app = Flask(__name__, instance_relative_config=True, static_folder='/app/static')
app.config.from_object(Config)
try:
os.makedirs(app.instance_path, exist_ok=True)
os.makedirs('/app/uploads', exist_ok=True)
except OSError:
pass
db.init_app(app)
login_manager.init_app(app)
login_manager.login_view = 'auth.login'
oauth.init_app(app)
from app.models import User
@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))
@app.template_filter('md5')
def md5_filter(s):
if s:
return hashlib.md5(s.encode('utf-8').lower().strip()).hexdigest()
return ''
@app.template_filter('datetimeformat')
def datetimeformat(value):
if value == 'now':
return datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
return value
@app.template_filter('rejectattr')
def rejectattr_filter(d, attr):
if not isinstance(d, dict):
return {}
return {k: v for k, v in d.items() if k != attr}
from app.routes import gallery_bp
from app.auth import auth_bp
app.register_blueprint(gallery_bp)
app.register_blueprint(auth_bp, url_prefix='/auth')
with app.app_context():
db.create_all()
return app

177
app/auth.py Normal file
View File

@ -0,0 +1,177 @@
#app/auth.py
import os
from flask import Blueprint, render_template, redirect, url_for, flash, request, session
from flask_login import login_user, logout_user, login_required, current_user
from app import db, oauth
from app.models import User
from app.forms import LoginForm, RegisterForm
from werkzeug.security import generate_password_hash
auth_bp = Blueprint('auth', __name__)
# OAuth setup
google = None
github = None
# Initialize Google OAuth only if valid credentials are provided
if os.getenv('GOOGLE_CLIENT_ID') and os.getenv('GOOGLE_CLIENT_SECRET') and \
os.getenv('GOOGLE_CLIENT_ID') != 'your-google-client-id' and \
os.getenv('GOOGLE_CLIENT_SECRET') != 'your-google-client-secret':
google = oauth.remote_app(
'google',
consumer_key=os.getenv('GOOGLE_CLIENT_ID'),
consumer_secret=os.getenv('GOOGLE_CLIENT_SECRET'),
request_token_params={'scope': 'email profile'},
base_url='https://www.googleapis.com/oauth2/v1/',
request_token_url=None,
access_token_method='POST',
access_token_url='https://accounts.google.com/o/oauth2/token',
authorize_url='https://accounts.google.com/o/oauth2/auth',
)
# Initialize GitHub OAuth only if valid credentials are provided
if os.getenv('GITHUB_CLIENT_ID') and os.getenv('GITHUB_CLIENT_SECRET'):
github = oauth.remote_app(
'github',
consumer_key=os.getenv('GITHUB_CLIENT_ID'),
consumer_secret=os.getenv('GITHUB_CLIENT_SECRET'),
request_token_params={'scope': 'user:email'},
base_url='https://api.github.com/',
request_token_url=None,
access_token_method='POST',
access_token_url='https://github.com/login/oauth/access_token',
authorize_url='https://github.com/login/oauth/authorize',
)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('gallery.gallery'))
form = LoginForm()
if form.validate_on_submit():
user = User.query.filter_by(email=form.email.data).first()
if user and user.check_password(form.password.data):
login_user(user)
flash('Logged in successfully!', 'success')
return redirect(url_for('gallery.gallery'))
flash('Invalid email or password.', 'danger')
return render_template('login.html', form=form)
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
if current_user.is_authenticated:
return redirect(url_for('gallery.gallery'))
form = RegisterForm()
if form.validate_on_submit():
if User.query.filter_by(email=form.email.data).first():
flash('Email already registered.', 'danger')
return render_template('register_user.html', form=form)
if User.query.filter_by(username=form.username.data).first():
flash('Username already taken.', 'danger')
return render_template('register_user.html', form=form)
user = User(
username=form.username.data,
email=form.email.data
)
user.set_password(form.password.data)
db.session.add(user)
db.session.commit()
login_user(user)
flash('Registration successful! You are now logged in.', 'success')
return redirect(url_for('gallery.gallery'))
return render_template('register_user.html', form=form)
@auth_bp.route('/login/google')
def login_google():
if not google:
flash('Google login is not configured.', 'danger')
return redirect(url_for('auth.login'))
return google.authorize(callback=url_for('auth.google_authorized', _external=True))
@auth_bp.route('/login/google/authorized')
def google_authorized():
if not google:
flash('Google login is not configured.', 'danger')
return redirect(url_for('auth.login'))
resp = google.authorized_response()
if resp is None or resp.get('access_token') is None:
flash('Google login failed.', 'danger')
return redirect(url_for('auth.login'))
session['google_token'] = (resp['access_token'], '')
user_info = google.get('userinfo').data
user = User.query.filter_by(oauth_provider='google', oauth_id=user_info['id']).first()
if not user:
email = user_info.get('email')
if not email:
flash('Google account has no verified email.', 'danger')
return redirect(url_for('auth.login'))
user = User(
username=email.split('@')[0],
email=email,
oauth_provider='google',
oauth_id=user_info['id']
)
db.session.add(user)
db.session.commit()
login_user(user)
flash('Logged in with Google!', 'success')
return redirect(url_for('gallery.gallery'))
@auth_bp.route('/login/github')
def login_github():
if not github:
flash('GitHub login is not configured.', 'danger')
return redirect(url_for('auth.login'))
return github.authorize(callback=url_for('auth.github_authorized', _external=True))
@auth_bp.route('/login/github/authorized')
def github_authorized():
if not github:
flash('GitHub login is not configured.', 'danger')
return redirect(url_for('auth.login'))
resp = github.authorized_response()
if resp is None or resp.get('access_token') is None:
flash('GitHub login failed.', 'danger')
return redirect(url_for('auth.login'))
session['github_token'] = (resp['access_token'], '')
user_info = github.get('user').data
emails = github.get('user/emails').data
primary_email = None
for email in emails:
if email.get('primary') and email.get('verified'):
primary_email = email['email']
break
if not primary_email:
primary_email = f"{user_info['login']}@github.com"
user = User.query.filter_by(oauth_provider='github', oauth_id=str(user_info['id'])).first()
if not user:
user = User(
username=user_info['login'],
email=primary_email,
oauth_provider='github',
oauth_id=str(user_info['id'])
)
db.session.add(user)
db.session.commit()
login_user(user)
flash('Logged in with GitHub!', 'success')
return redirect(url_for('gallery.gallery'))
@auth_bp.route('/logout')
@login_required
def logout():
logout_user()
flash('Logged out successfully.', 'success')
return redirect(url_for('gallery.gallery'))
# Define token getters only if OAuth is configured
if google:
@google.tokengetter
def get_google_oauth_token():
return session.get('google_token')
if github:
@github.tokengetter
def get_github_oauth_token():
return session.get('github_token')

82
app/forms.py Normal file
View File

@ -0,0 +1,82 @@
#app/forms.py
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import StringField, TextAreaField, SubmitField, PasswordField, SelectField, SelectMultipleField
from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional, ValidationError
import re
from app.models import ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport
from app import db
def validate_url_or_path(form, field):
if not field.data:
return
# Validate file paths like /static/uploads/<uuid>_<filename>.jpg|png
if field.data.startswith('/app/uploads/'):
# Allow UUIDs (hex with hyphens), underscores, mixed-case filenames
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 re.match(path_pattern, field.data, re.I):
return
# Validate URLs
url_pattern = r'^(https?:\/\/)?([\w\-]+\.)+[\w\-]+(\/[\w\-\.]*)*\/?(\?[^\s]*)?(#[^\s]*)?$'
if not re.match(url_pattern, field.data):
raise ValidationError('Invalid URL or file path.')
class SmartAppForm(FlaskForm):
name = StringField('App Name', validators=[DataRequired(), Length(min=3, max=100)])
description = TextAreaField('Description', validators=[DataRequired(), Length(min=10, max=500)])
developer = StringField('Developer/Organization', validators=[DataRequired(), Length(min=3, max=100)])
contact_email = StringField('Contact Email', validators=[DataRequired(), Email()])
logo_url = StringField('Logo URL', validators=[Optional(), validate_url_or_path], render_kw={"placeholder": "https://example.com/logo.png or leave blank to upload"})
logo_upload = FileField('Upload Logo', validators=[FileAllowed(['jpg', 'png'], 'Images only!')])
launch_url = StringField('Launch URL', validators=[DataRequired(), Length(max=200)])
client_id = StringField('Client ID', validators=[DataRequired(), Length(min=3, max=100)])
scopes = TextAreaField('Scopes (comma-separated)', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "patient/Patient.read,launch/patient"})
website = StringField('Company Website', validators=[Optional(), Length(max=200)], render_kw={"placeholder": "https://example.com"})
designed_for = SelectField('Designed For', coerce=int, validators=[DataRequired()])
designed_for_new = StringField('Add New Designed For', validators=[Optional(), Length(max=100)])
application_type = SelectField('Application Type', coerce=int, validators=[DataRequired()])
application_type_new = StringField('Add New Application Type', validators=[Optional(), Length(max=100)])
fhir_compatibility = SelectField('FHIR Compatibility', coerce=int, validators=[DataRequired()])
fhir_compatibility_new = StringField('Add New FHIR Compatibility', validators=[Optional(), Length(max=100)])
categories = SelectMultipleField('Categories', coerce=int, validators=[DataRequired()])
categories_new = StringField('Add New Category', validators=[Optional(), Length(max=100)])
specialties = SelectMultipleField('Specialties', coerce=int, validators=[DataRequired()])
specialties_new = StringField('Add New Speciality', validators=[Optional(), Length(max=100)])
licensing_pricing = SelectField('Licensing & Pricing', coerce=int, validators=[DataRequired()])
licensing_pricing_new = StringField('Add New Licensing/Pricing', validators=[Optional(), Length(max=100)])
os_support = SelectMultipleField('OS Support', coerce=int, validators=[DataRequired()])
os_support_new = StringField('Add New OS Support', validators=[Optional(), Length(max=100)])
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!')])
ehr_support = SelectMultipleField('EHR Support', coerce=int, validators=[DataRequired()])
ehr_support_new = StringField('Add New EHR Support', validators=[Optional(), Length(max=100)])
submit = SubmitField('Register App')
def __init__(self, *args, **kwargs):
super(SmartAppForm, self).__init__(*args, **kwargs)
self.application_type.choices = [(t.id, t.name) for t in ApplicationType.query.all()]
self.categories.choices = [(c.id, c.name) for c in Category.query.all()]
self.os_support.choices = [(o.id, o.name) for o in OSSupport.query.all()]
self.fhir_compatibility.choices = [(f.id, f.name) for f in FHIRSupport.query.all()]
self.specialties.choices = [(s.id, s.name) for s in Speciality.query.all()]
self.licensing_pricing.choices = [(p.id, p.name) for p in PricingLicense.query.all()]
self.designed_for.choices = [(d.id, d.name) for d in DesignedFor.query.all()]
self.ehr_support.choices = [(e.id, e.name) for e in EHRSupport.query.all()]
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired()])
submit = SubmitField('Log In')
class RegisterForm(FlaskForm):
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=80)])
email = StringField('Email', validators=[DataRequired(), Email()])
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
submit = SubmitField('Register')
class GalleryFilterForm(FlaskForm):
categories = StringField('Categories', validators=[Length(max=500)], render_kw={"placeholder": "e.g., Clinical, Billing"})
fhir_compatibility = StringField('FHIR Compatibility', validators=[Length(max=200)], render_kw={"placeholder": "e.g., R4, US Core"})
submit = SubmitField('Filter')

90
app/models.py Normal file
View File

@ -0,0 +1,90 @@
#app/models.py
from app import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=True)
password_hash = db.Column(db.String(255))
oauth_provider = db.Column(db.String(50))
oauth_id = db.Column(db.String(100))
created_at = db.Column(db.DateTime, default=datetime.utcnow)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
@property
def is_authenticated(self):
return True
@property
def is_active(self):
return True
@property
def is_anonymous(self):
return False
def get_id(self):
return str(self.id)
class ApplicationType(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class OSSupport(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class FHIRSupport(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class Speciality(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class PricingLicense(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class DesignedFor(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class EHRSupport(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class SmartApp(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text, nullable=False)
developer = db.Column(db.String(100), nullable=False)
contact_email = db.Column(db.String(120), nullable=False)
logo_url = db.Column(db.String(200))
launch_url = db.Column(db.String(200), nullable=False)
client_id = db.Column(db.String(100), nullable=False)
scopes = db.Column(db.Text)
website = db.Column(db.String(200))
designed_for_id = db.Column(db.Integer, db.ForeignKey('designed_for.id'))
application_type_id = db.Column(db.Integer, db.ForeignKey('application_type.id'))
fhir_compatibility_id = db.Column(db.Integer, db.ForeignKey('fhir_support.id'))
categories = db.Column(db.Text) # Comma-separated Category IDs
specialties = db.Column(db.Text) # Comma-separated Speciality IDs
licensing_pricing_id = db.Column(db.Integer, db.ForeignKey('pricing_license.id'))
os_support = db.Column(db.Text) # Comma-separated OSSupport IDs
app_images = db.Column(db.Text)
ehr_support = db.Column(db.Text) # Comma-separated EHRSupport IDs
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
registration_date = db.Column(db.DateTime, default=datetime.utcnow)
last_updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

475
app/routes.py Normal file
View File

@ -0,0 +1,475 @@
#app/routes.py
from flask import Blueprint, render_template, redirect, url_for, flash, request
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__)
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('/')
def index():
return redirect(url_for('gallery.gallery'))
@gallery_bp.route('/gallery', methods=['GET', 'POST'])
def gallery():
form = GalleryFilterForm()
query = SmartApp.query
filter_params = {}
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/<int:app_id>')
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)
# Ensure upload folder exists
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)
# Handle new filter options
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)
# 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}")
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"/app/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:
app_images.extend([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 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"/app/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/<int:app_id>', 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:
form.app_image_urls.data = '\n'.join(app.app_images.split(',') if app.app_images else [])
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)
# Ensure upload folder exists
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()
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('edit_app.html', form=form, app=app)
logo_url = f"/app/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('edit_app.html', form=form, app=app)
elif not logo_url:
logo_url = app.logo_url
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()])
if form.app_image_uploads.data:
file = form.app_image_uploads.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 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('edit_app.html', form=form, app=app)
app_images.append(f"/app/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('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 or None
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/<int:app_id>', 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'))

View File

@ -0,0 +1,58 @@
<!---app/templates/app_detail.html--->
{% extends "base.html" %}
{% block title %}{{ app.name }}{% endblock %}
{% block content %}
<div class="container mt-4">
<h1>{{ app.name }}</h1>
<div class="card shadow-sm">
<div class="card-body">
{% if app.logo_url %}
<img src="{{ app.logo_url }}" alt="{{ app.name }} logo" style="max-height: 200px; object-fit: contain; margin-bottom: 1rem;" onerror="this.style.display='none';">
{% else %}
<img src="https://via.placeholder.com/200?text=No+Logo" alt="No Logo" style="max-height: 200px; object-fit: contain; margin-bottom: 1rem;">
{% endif %}
<p><strong>Description:</strong> {{ app.description }}</p>
<p><strong>Developer:</strong> {{ app.developer }}</p>
<p><strong>Contact Email:</strong> {{ app.contact_email }}</p>
<p><strong>Launch URL:</strong> <a href="{{ app.launch_url }}">{{ app.launch_url }}</a></p>
<p><strong>Client ID:</strong> {{ app.client_id }}</p>
<p><strong>Scopes:</strong> {{ app.scopes }}</p>
{% if app.website %}
<p><strong>Website:</strong> <a href="{{ app.website }}">{{ app.website }}</a></p>
{% endif %}
{% if app_categories %}
<p><strong>Categories:</strong> {{ app_categories | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app_specialties %}
<p><strong>Specialties:</strong> {{ app_specialties | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app_os_supports %}
<p><strong>OS Support:</strong> {{ app_os_supports | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app_ehr_supports %}
<p><strong>EHR Support:</strong> {{ app_ehr_supports | map(attribute='name') | join(', ') }}</p>
{% endif %}
{% if app.app_images %}
<h5>Additional Images:</h5>
<div class="row">
{% for img_url in app.app_images.split(',') %}
<div class="col-md-4 mb-3">
<img src="{{ img_url }}" alt="App Image" style="max-height: 150px; object-fit: contain;" onerror="this.style.display='none';">
</div>
{% endfor %}
</div>
{% endif %}
{% if current_user.is_authenticated and current_user.id == app.user_id %}
<a href="{{ url_for('gallery.edit_app', app_id=app.id) }}" class="btn btn-primary">Edit App</a>
<form action="{{ url_for('gallery.delete_app', app_id=app.id) }}" method="POST" style="display:inline;">
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this app?');">Delete App</button>
</form>
{% endif %}
</div>
</div>
</div>
{% endblock %}

183
app/templates/base.html Normal file
View File

@ -0,0 +1,183 @@
<!---app/templates/base.html--->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SMARTFLARE - Smart App Gallery - {% block title %}{% endblock %}</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
<style>
.navbar-light {
background-color: #ffffff !important;
border-bottom: 1px solid #e0e0e0;
}
.navbar-brand {
font-weight: bold;
color: #007bff !important;
}
.nav-link {
color: #333 !important;
transition: color 0.2s ease;
}
.nav-link:hover {
color: #007bff !important;
}
.nav-link.active {
color: #007bff !important;
font-weight: bold;
border-bottom: 2px solid #007bff;
}
.dropdown-menu {
animation: fadeIn 0.3s ease;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #e9ecef;
}
.dropdown-item.active {
background-color: #007bff;
color: white !important;
}
.app-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-radius: 10px;
overflow: hidden;
}
.app-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
}
.card-img-top {
background-color: #f8f9fa;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.dark-mode .navbar-light {
background-color: #343a40 !important;
border-bottom: 1px solid #495057;
}
.dark-mode .nav-link {
color: #f8f9fa !important;
}
.dark-mode .nav-link:hover, .dark-mode .nav-link.active {
color: #4dabf7 !important;
border-bottom: 2px solid #4dabf7;
}
.dark-mode .dropdown-menu {
background-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dark-mode .dropdown-item {
color: #f8f9fa;
}
.dark-mode .dropdown-item:hover {
background-color: #6c757d;
}
.dark-mode .dropdown-item.active {
background-color: #4dabf7;
}
.dark-mode .app-card {
background-color: #343a40;
color: #f8f9fa;
}
.dark-mode .card-img-top {
background-color: #495057;
}
.dark-mode .text-muted {
color: #adb5bd !important;
}
.dark-mode .navbar-brand {
color: #4dabf7 !important;
}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-light">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('gallery.gallery') }}">SMART App Gallery</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link" href="{{ url_for('gallery.gallery') }}">Gallery</a>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('gallery.register') }}">Register App</a>
</li>
{% endif %}
</ul>
<ul class="navbar-nav">
<li class="nav-item">
<span class="nav-link theme-toggle" onclick="toggleTheme()">Toggle Theme</span>
</li>
{% if current_user.is_authenticated %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{% if current_user.email and current_user.email | md5 %}
<img src="https://www.gravatar.com/avatar/{{ current_user.email | md5 }}?s=30&d=identicon" class="user-avatar" alt="User Avatar">
{% else %}
<div class="initials-avatar">{{ current_user.username[:2] | upper }}</div>
{% endif %}
</a>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
<li><a class="dropdown-item" href="{{ url_for('gallery.my_listings') }}">My Listings</a></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
</ul>
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{{ url_for('auth.login') }}">Login</a>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<script>
function toggleTheme() {
const html = document.documentElement;
const currentTheme = html.getAttribute('data-theme');
if (currentTheme === 'dark') {
html.removeAttribute('data-theme');
localStorage.setItem('theme', 'light');
} else {
html.setAttribute('data-theme', 'dark');
localStorage.setItem('theme', 'dark');
}
}
document.addEventListener('DOMContentLoaded', () => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.setAttribute('data-theme', 'dark');
}
});
</script>
</body>
</html>

214
app/templates/edit_app.html Normal file
View File

@ -0,0 +1,214 @@
<!--app/templates/edit_app.html-->
{% extends "base.html" %}
{% block content %}
<h1>Edit App: {{ app.name }}</h1>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
{% for error in form.name.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows=4) }}
{% for error in form.description.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.developer.label(class="form-label") }}
{{ form.developer(class="form-control") }}
{% for error in form.developer.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.contact_email.label(class="form-label") }}
{{ form.contact_email(class="form-control") }}
{% for error in form.contact_email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
{{ form.logo_url(class="form-control") }}
<small class="form-text text-muted">Enter a URL or upload a new logo below. Leave blank to keep current logo.</small>
{% for error in form.logo_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% if app.logo_url %}
<p class="mt-2"><strong>Current Logo:</strong></p>
<img src="{{ app.logo_url }}" alt="Current Logo" style="max-height: 100px;">
{% endif %}
</div>
<div class="mb-3">
{{ form.logo_upload.label(class="form-label") }}
{{ form.logo_upload(class="form-control") }}
{% for error in form.logo_upload.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.launch_url.label(class="form-label") }}
{{ form.launch_url(class="form-control") }}
{% for error in form.launch_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.client_id.label(class="form-label") }}
{{ form.client_id(class="form-control") }}
{% for error in form.client_id.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.scopes.label(class="form-label") }}
{{ form.scopes(class="form-control", rows=3) }}
{% for error in form.scopes.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% if app.scopes and app.scopes.strip() %}
<p class="mt-2"><strong>Current Scopes:</strong>
{% for scope in app.scopes.split(',') %}
{% if scope.strip() %}
<span class="badge bg-primary me-1">{{ scope.strip() }}</span>
{% endif %}
{% endfor %}
</p>
{% else %}
<p class="mt-2 text-muted">No scopes defined.</p>
{% endif %}
</div>
<div class="mb-3">
{{ form.website.label(class="form-label") }}
{{ form.website(class="form-control") }}
{% for error in form.website.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.designed_for.label(class="form-label") }}
{{ form.designed_for(class="form-control") }}
{% for error in form.designed_for.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.designed_for_new.label(class="form-label mt-2") }}
{{ form.designed_for_new(class="form-control") }}
{% for error in form.designed_for_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.application_type.label(class="form-label") }}
{{ form.application_type(class="form-control") }}
{% for error in form.application_type.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.application_type_new.label(class="form-label mt-2") }}
{{ form.application_type_new(class="form-control") }}
{% for error in form.application_type_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.fhir_compatibility.label(class="form-label") }}
{{ form.fhir_compatibility(class="form-control") }}
{% for error in form.fhir_compatibility.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.fhir_compatibility_new.label(class="form-label mt-2") }}
{{ form.fhir_compatibility_new(class="form-control") }}
{% for error in form.fhir_compatibility_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.categories.label(class="form-label") }}
{{ form.categories(class="form-control") }}
{% for error in form.categories.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.categories_new.label(class="form-label mt-2") }}
{{ form.categories_new(class="form-control") }}
{% for error in form.categories_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.specialties.label(class="form-label") }}
{{ form.specialties(class="form-control") }}
{% for error in form.specialties.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.specialties_new.label(class="form-label mt-2") }}
{{ form.specialties_new(class="form-control") }}
{% for error in form.specialties_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.licensing_pricing.label(class="form-label") }}
{{ form.licensing_pricing(class="form-control") }}
{% for error in form.licensing_pricing.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.licensing_pricing_new.label(class="form-label mt-2") }}
{{ form.licensing_pricing_new(class="form-control") }}
{% for error in form.licensing_pricing_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.os_support.label(class="form-label") }}
{{ form.os_support(class="form-control") }}
{% for error in form.os_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.os_support_new.label(class="form-label mt-2") }}
{{ form.os_support_new(class="form-control") }}
{% for error in form.os_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.app_image_urls.label(class="form-label") }}
{{ form.app_image_urls(class="form-control", rows=3) }}
{% for error in form.app_image_urls.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% if app.app_images %}
<p class="mt-2"><strong>Current Images:</strong></p>
{% for image in app.app_images.split(',') %}
<img src="{{ image }}" alt="App Image" style="max-height: 100px; margin: 5px;">
{% endfor %}
{% endif %}
</div>
<div class="mb-3">
{{ form.app_image_uploads.label(class="form-label") }}
{{ form.app_image_uploads(class="form-control") }}
{% for error in form.app_image_uploads.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.ehr_support.label(class="form-label") }}
{{ form.ehr_support(class="form-control") }}
{% for error in form.ehr_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.ehr_support_new.label(class="form-label mt-2") }}
{{ form.ehr_support_new(class="form-control") }}
{% for error in form.ehr_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-secondary">Cancel</a>
</form>
{% endblock %}

247
app/templates/gallery.html Normal file
View File

@ -0,0 +1,247 @@
<!---app/templates/gallery.html--->
{% extends "base.html" %}
{% block title %}Gallery{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Sticky Top Filter Bar -->
<nav class="navbar navbar-expand-lg navbar-light sticky-top shadow-sm mb-4">
<div class="container-fluid">
<span class="navbar-brand d-flex align-items-center">
<img src="{{ url_for('static', filename='smartflare.png') }}" alt="SMARTFLARE" height="40" class="me-2" onerror="this.style.display='none';">
Filter Apps
</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#filterNavbar" aria-controls="filterNavbar" aria-expanded="false" aria-label="Toggle filters">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="filterNavbar">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link {% if not filter_params %}active{% endif %}" href="{{ url_for('gallery.gallery') }}">All Apps</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('application_type') %}active{% endif %}" href="#" id="appTypeDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Application Type
</a>
<ul class="dropdown-menu" aria-labelledby="appTypeDropdown">
{% for app_type in application_types %}
<li>
<a class="dropdown-item {% if app_type.id in filter_params.get('application_type', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', application_type=app_type.id, **(filter_params | rejectattr('application_type'))) }}">{{ app_type.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('category') %}active{% endif %}" href="#" id="categoryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Categories
</a>
<ul class="dropdown-menu" aria-labelledby="categoryDropdown">
{% for category in categories %}
<li>
<a class="dropdown-item {% if category.id in filter_params.get('category', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', category=category.id, **(filter_params | rejectattr('category'))) }}">{{ category.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('os_support') %}active{% endif %}" href="#" id="osSupportDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
OS Support
</a>
<ul class="dropdown-menu" aria-labelledby="osSupportDropdown">
{% for os in os_supports %}
<li>
<a class="dropdown-item {% if os.id in filter_params.get('os_support', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', os_support=os.id, **(filter_params | rejectattr('os_support'))) }}">{{ os.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('fhir_support') %}active{% endif %}" href="#" id="fhirSupportDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
FHIR Support
</a>
<ul class="dropdown-menu" aria-labelledby="fhirSupportDropdown">
{% for fhir in fhir_supports %}
<li>
<a class="dropdown-item {% if fhir.id in filter_params.get('fhir_support', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', fhir_support=fhir.id, **(filter_params | rejectattr('fhir_support'))) }}">{{ fhir.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('speciality') %}active{% endif %}" href="#" id="specialityDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Speciality
</a>
<ul class="dropdown-menu" aria-labelledby="specialityDropdown">
{% for speciality in specialties %}
<li>
<a class="dropdown-item {% if speciality.id in filter_params.get('speciality', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', speciality=speciality.id, **(filter_params | rejectattr('speciality'))) }}">{{ speciality.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('pricing_license') %}active{% endif %}" href="#" id="pricingDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Pricing/License
</a>
<ul class="dropdown-menu" aria-labelledby="pricingDropdown">
{% for pricing in pricing_licenses %}
<li>
<a class="dropdown-item {% if pricing.id in filter_params.get('pricing_license', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', pricing_license=pricing.id, **(filter_params | rejectattr('pricing_license'))) }}">{{ pricing.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('designed_for') %}active{% endif %}" href="#" id="designedForDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Designed For
</a>
<ul class="dropdown-menu" aria-labelledby="designedForDropdown">
{% for designed in designed_fors %}
<li>
<a class="dropdown-item {% if designed.id in filter_params.get('designed_for', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', designed_for=designed.id, **(filter_params | rejectattr('designed_for'))) }}">{{ designed.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('ehr_support') %}active{% endif %}" href="#" id="ehrSupportDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
EHR Support
</a>
<ul class="dropdown-menu" aria-labelledby="ehrSupportDropdown">
{% for ehr in ehr_supports %}
<li>
<a class="dropdown-item {% if ehr.id in filter_params.get('ehr_support', []) %}active{% endif %}" href="{{ url_for('gallery.gallery', ehr_support=ehr.id, **(filter_params | rejectattr('ehr_support'))) }}">{{ ehr.name }}</a>
</li>
{% endfor %}
</ul>
</li>
</ul>
</div>
</div>
</nav>
<!-- App Cards -->
<div class="container mt-4">
<p>Explore SMART on FHIR apps. Filter above to find the perfect app.</p>
{% if apps %}
<div class="row">
{% for app in apps %}
<div class="col-md-4 mb-3">
<div class="card app-card shadow-sm h-100">
{% 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';">
{% 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;">
{% endif %}
<div class="card-body d-flex flex-column">
<h5 class="card-title">{{ app.name }}</h5>
<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>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p class="text-muted">No apps match your filters. Try adjusting the filters above.</p>
{% endif %}
</div>
</div>
<!-- Custom CSS for Pretty UI -->
<style>
.navbar-light {
background-color: #ffffff !important;
border-bottom: 1px solid #e0e0e0;
}
.navbar-brand {
font-weight: bold;
color: #007bff !important;
}
.nav-link {
color: #333 !important;
transition: color 0.2s ease;
}
.nav-link:hover {
color: #007bff !important;
}
.nav-link.active {
color: #007bff !important;
font-weight: bold;
border-bottom: 2px solid #007bff;
}
.dropdown-menu {
animation: fadeIn 0.3s ease;
border: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.dropdown-item {
padding: 0.5rem 1rem;
transition: background-color 0.2s ease;
}
.dropdown-item:hover {
background-color: #e9ecef;
}
.dropdown-item.active {
background-color: #007bff;
color: white !important;
}
.app-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border-radius: 10px;
overflow: hidden;
}
.app-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
}
.card-img-top {
background-color: #f8f9fa;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
.dark-mode .navbar-light {
background-color: #343a40 !important;
border-bottom: 1px solid #495057;
}
.dark-mode .nav-link {
color: #f8f9fa !important;
}
.dark-mode .nav-link:hover, .dark-mode .nav-link.active {
color: #4dabf7 !important;
border-bottom: 2px solid #4dabf7;
}
.dark-mode .dropdown-menu {
background-color: #495057;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.dark-mode .dropdown-item {
color: #f8f9fa;
}
.dark-mode .dropdown-item:hover {
background-color: #6c757d;
}
.dark-mode .dropdown-item.active {
background-color: #4dabf7;
}
.dark-mode .app-card {
background-color: #343a40;
color: #f8f9fa;
}
.dark-mode .card-img-top {
background-color: #495057;
}
.dark-mode .text-muted {
color: #adb5bd !important;
}
.dark-mode .navbar-brand {
color: #4dabf7 !important;
}
</style>
{% endblock %}

32
app/templates/login.html Normal file
View File

@ -0,0 +1,32 @@
<!---app/templates/login.html--->
{% extends "base.html" %}
{% block content %}
<h1>Login</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
<div class="mt-3">
<p>Or login with:</p>
<a href="{{ url_for('auth.login_google') }}" class="btn btn-outline-primary">Google</a>
<a href="{{ url_for('auth.login_github') }}" class="btn btn-outline-dark">GitHub</a>
</div>
<p class="mt-3">Don't have an account? <a href="{{ url_for('auth.register') }}">Register</a></p>
{% endblock %}

View File

@ -0,0 +1,33 @@
<!--app/templates/my_listings.html-->
{% extends "base.html" %}
{% block content %}
<h1>My Listings</h1>
{% if apps %}
<div class="row">
{% for app in apps %}
<div class="col-md-4 mb-3">
<div class="card">
{% if app.logo_url %}
<img src="{{ app.logo_url }}" class="card-img-top" alt="{{ app.name }} logo" style="max-height: 150px; object-fit: contain;">
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ app.name }}</h5>
<p class="card-text">{{ app.description | truncate(100) }}</p>
<p class="card-text"><small class="text-muted">By {{ app.developer }}</small></p>
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary">View Details</a>
<a href="{{ url_for('gallery.edit_app', app_id=app.id) }}" class="btn btn-warning btn-sm">Edit</a>
<form action="{{ url_for('gallery.delete_app', app_id=app.id) }}" method="POST" class="d-inline">
<button type="submit" class="btn btn-danger btn-sm" onclick="return confirm('Are you sure you want to delete {{ app.name }}?')">Delete</button>
</form>
</div>
</div>
</div>
{% endfor %}
</div>
{% else %}
<p>You haven't registered any apps yet.</p>
<a href="{{ url_for('gallery.register') }}" class="btn btn-primary">Register an App</a>
{% endif %}
{% endblock %}

193
app/templates/register.html Normal file
View File

@ -0,0 +1,193 @@
<!---app/templates/register.html--->
{% extends "base.html" %}
{% block content %}
<h1>Register New App</h1>
<form method="POST" enctype="multipart/form-data">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.name.label(class="form-label") }}
{{ form.name(class="form-control") }}
{% for error in form.name.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.description.label(class="form-label") }}
{{ form.description(class="form-control", rows=4) }}
{% for error in form.description.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.developer.label(class="form-label") }}
{{ form.developer(class="form-control") }}
{% for error in form.developer.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.contact_email.label(class="form-label") }}
{{ form.contact_email(class="form-control") }}
{% for error in form.contact_email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.logo_url.label(class="form-label") }}
{{ form.logo_url(class="form-control") }}
<small class="form-text text-muted">Enter a URL or upload a logo below.</small>
{% for error in form.logo_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.logo_upload.label(class="form-label") }}
{{ form.logo_upload(class="form-control") }}
{% for error in form.logo_upload.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.launch_url.label(class="form-label") }}
{{ form.launch_url(class="form-control") }}
{% for error in form.launch_url.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.client_id.label(class="form-label") }}
{{ form.client_id(class="form-control") }}
{% for error in form.client_id.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.scopes.label(class="form-label") }}
{{ form.scopes(class="form-control", rows=3) }}
{% for error in form.scopes.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.website.label(class="form-label") }}
{{ form.website(class="form-control") }}
{% for error in form.website.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.designed_for.label(class="form-label") }}
{{ form.designed_for(class="form-control") }}
{% for error in form.designed_for.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.designed_for_new.label(class="form-label mt-2") }}
{{ form.designed_for_new(class="form-control") }}
{% for error in form.designed_for_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.application_type.label(class="form-label") }}
{{ form.application_type(class="form-control") }}
{% for error in form.application_type.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.application_type_new.label(class="form-label mt-2") }}
{{ form.application_type_new(class="form-control") }}
{% for error in form.application_type_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.fhir_compatibility.label(class="form-label") }}
{{ form.fhir_compatibility(class="form-control") }}
{% for error in form.fhir_compatibility.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.fhir_compatibility_new.label(class="form-label mt-2") }}
{{ form.fhir_compatibility_new(class="form-control") }}
{% for error in form.fhir_compatibility_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.categories.label(class="form-label") }}
{{ 을 form.categories(class="form-control") }}
{% for error in form.categories.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.categories_new.label(class="form-label mt-2") }}
{{ form.categories_new(class="form-control") }}
{% for error in form.categories_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.specialties.label(class="form-label") }}
{{ form.specialties(class="form-control") }}
{% for error in form.specialties.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.specialties_new.label(class="form-label mt-2") }}
{{ form.specialties_new(class="form-control") }}
{% for error in form.specialties_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.licensing_pricing.label(class="form-label") }}
{{ form.licensing_pricing(class="form-control") }}
{% for error in form.licensing_pricing.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.licensing_pricing_new.label(class="form-label mt-2") }}
{{ form.licensing_pricing_new(class="form-control") }}
{% for error in form.licensing_pricing_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.os_support.label(class="form-label") }}
{{ form.os_support(class="form-control") }}
{% for error in form.os_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.os_support_new.label(class="form-label mt-2") }}
{{ form.os_support_new(class="form-control") }}
{% for error in form.os_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.app_image_urls.label(class="form-label") }}
{{ form.app_image_urls(class="form-control", rows=3) }}
{% for error in form.app_image_urls.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.app_image_uploads.label(class="form-label") }}
{{ form.app_image_uploads(class="form-control") }}
{% for error in form.app_image_uploads.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.ehr_support.label(class="form-label") }}
{{ form.ehr_support(class="form-control") }}
{% for error in form.ehr_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{{ form.ehr_support_new.label(class="form-label mt-2") }}
{{ form.ehr_support_new(class="form-control") }}
{% for error in form.ehr_support_new.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-secondary">Cancel</a>
</form>
{% endblock %}

View File

@ -0,0 +1,40 @@
<!--app/templates/register_user.html-->
{% extends "base.html" %}
{% block content %}
<h1>Register</h1>
<form method="POST">
{{ form.hidden_tag() }}
<div class="mb-3">
{{ form.username.label(class="form-label") }}
{{ form.username(class="form-control") }}
{% for error in form.username.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.email.label(class="form-label") }}
{{ form.email(class="form-control") }}
{% for error in form.email.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.password.label(class="form-label") }}
{{ form.password(class="form-control") }}
{% for error in form.password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.confirm_password.label(class="form-label") }}
{{ form.confirm_password(class="form-control") }}
{% for error in form.confirm_password.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
</form>
<p class="mt-3">Already have an account? <a href="{{ url_for('auth.login') }}">Login</a></p>
{% endblock %}

8
config.py Normal file
View File

@ -0,0 +1,8 @@
#config.py
import os
class Config:
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(BASE_DIR, "instance", "smart_app_gallery.db")}'
SQLALCHEMY_TRACK_MODIFICATIONS = False
SECRET_KEY = 'your-secret-key' # Replace with a secure key in production

13
docker-compose.yml Normal file
View File

@ -0,0 +1,13 @@
version: '3.8'
services:
app:
build: .
ports:
- "5000:5000"
volumes:
- ./instance:/app/instance
- ./uploads:/app/uploads
environment:
- FLASK_APP=app
- FLASK_ENV=development
command: ["flask", "run", "--host=0.0.0.0"]

Binary file not shown.

11
requirements.txt Normal file
View File

@ -0,0 +1,11 @@
Flask==2.3.3
Flask-SQLAlchemy==3.0.5
Werkzeug==2.3.7
requests==2.31.0
Flask-WTF==1.2.1
WTForms==3.1.2
email-validator==2.2.0
Flask-Login==0.6.3
Flask-OAuthlib==0.9.6
python-dotenv==1.0.1
bcrypt==4.2.0

60
seed.py Normal file
View File

@ -0,0 +1,60 @@
from app import db, create_app
from app.models import ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport
app = create_app()
def seed_table(model, names):
for name in names:
if not model.query.filter_by(name=name).first():
db.session.add(model(name=name))
db.session.commit()
with app.app_context():
# Ensure tables are created
db.create_all()
try:
# Application Type
app_types = ['Bulk Data', 'SMART', 'SMART Health Cards']
seed_table(ApplicationType, app_types)
# Categories
categories = [
'Care Coordination', 'Clinical Research', 'Data Visualization', 'Disease Management',
'Genomics', 'Medication', 'Patient Engagement', 'Population Health', 'Risk Calculation',
'FHIR Tools', 'COVID-19', 'Telehealth'
]
seed_table(Category, categories)
# OS Support
os_supports = ['iOS', 'Android', 'Web', 'Mac', 'Windows', 'Linux']
seed_table(OSSupport, os_supports)
# FHIR Support
fhir_supports = ['DSTU 1', 'DSTU 2', 'STU 3', 'R4']
seed_table(FHIRSupport, fhir_supports)
# Speciality
specialties = [
'Anesthesiology', 'Cardiology', 'Gastrointestinal', 'Infectious Disease', 'Neurology',
'Obstetrics', 'Oncology', 'Pediatrics', 'Pulmonary', 'Renal', 'Rheumatology', 'Trauma',
'Primary care'
]
seed_table(Speciality, specialties)
# Pricing/License
pricings = ['Open Source', 'Free', 'Per User', 'Site-Based', 'Other']
seed_table(PricingLicense, pricings)
# Designed For
designed_fors = ['Clinicians', 'Patients', 'Patients & Clinicians', 'IT']
seed_table(DesignedFor, designed_fors)
# EHR Support
ehr_supports = ['Allscripts', 'Athena Health', 'Epic', 'Cerner']
seed_table(EHRSupport, ehr_supports)
print("Database seeded successfully!")
except Exception as e:
print(f"Seeding failed: {str(e)}")
db.session.rollback()

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 224 KiB

BIN
static/smartflare.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB