mirror of
https://github.com/Sudo-JHare/SMARTFLARE-Smart-App-Gallery.git
synced 2025-07-29 18:25:35 +00:00
Add files via upload
This commit is contained in:
parent
1d5909861e
commit
b97d1b175d
12
Dockerfile
Normal file
12
Dockerfile
Normal 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
63
app/__init__.py
Normal 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
177
app/auth.py
Normal 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
82
app/forms.py
Normal 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
90
app/models.py
Normal 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
475
app/routes.py
Normal 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'))
|
58
app/templates/app_detail.html
Normal file
58
app/templates/app_detail.html
Normal 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
183
app/templates/base.html
Normal 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
214
app/templates/edit_app.html
Normal 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
247
app/templates/gallery.html
Normal 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
32
app/templates/login.html
Normal 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 %}
|
33
app/templates/my_listings.html
Normal file
33
app/templates/my_listings.html
Normal 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
193
app/templates/register.html
Normal 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 %}
|
40
app/templates/register_user.html
Normal file
40
app/templates/register_user.html
Normal 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
8
config.py
Normal 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
13
docker-compose.yml
Normal 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"]
|
BIN
instance/smart_app_gallery.db
Normal file
BIN
instance/smart_app_gallery.db
Normal file
Binary file not shown.
11
requirements.txt
Normal file
11
requirements.txt
Normal 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
60
seed.py
Normal 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
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 224 KiB |
BIN
static/smartflare.png
Normal file
BIN
static/smartflare.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 448 KiB |
Loading…
x
Reference in New Issue
Block a user