mirror of
https://github.com/Sudo-JHare/FHIRFLARE-IG-Toolkit.git
synced 2025-06-14 12:10:03 +00:00
Revert "Add files via upload"
This reverts commit f172b00737092fdf4040bffd2f8c6c2742eb7a44.
This commit is contained in:
parent
f172b00737
commit
5efae0c64e
60
Dockerfile
60
Dockerfile
@ -1,11 +1,57 @@
|
||||
FROM python:3.11-slim
|
||||
# Base image with Python and Java
|
||||
FROM tomcat:10.1-jdk17
|
||||
|
||||
# Install build dependencies, Node.js 18, and coreutils (for stdbuf)
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3 python3-pip python3-venv curl coreutils \
|
||||
&& curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install specific versions of GoFSH and SUSHI
|
||||
# REMOVED pip install fhirpath from this line
|
||||
RUN npm install -g gofsh fsh-sushi
|
||||
|
||||
# Set up Python environment
|
||||
WORKDIR /app
|
||||
RUN python3 -m venv /app/venv
|
||||
ENV PATH="/app/venv/bin:$PATH"
|
||||
|
||||
# ADDED: Uninstall old fhirpath just in case it's in requirements.txt
|
||||
RUN pip uninstall -y fhirpath || true
|
||||
# ADDED: Install the new fhirpathpy library
|
||||
RUN pip install --no-cache-dir fhirpathpy
|
||||
|
||||
# Copy Flask files
|
||||
COPY requirements.txt .
|
||||
# Install requirements (including Pydantic - check version compatibility if needed)
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app/ app/
|
||||
COPY app.py .
|
||||
COPY services.py .
|
||||
COPY forms.py .
|
||||
COPY package.py .
|
||||
COPY templates/ templates/
|
||||
COPY static/ static/
|
||||
COPY instance/ instance/
|
||||
COPY config.py .
|
||||
COPY .env .
|
||||
EXPOSE 5009
|
||||
CMD ["flask", "run", "--host=0.0.0.0", "--port=5009"]
|
||||
COPY tests/ tests/
|
||||
|
||||
# Ensure /tmp, /app/h2-data, /app/static/uploads, and /app/logs are writable
|
||||
RUN mkdir -p /tmp /app/h2-data /app/static/uploads /app/logs && chmod 777 /tmp /app/h2-data /app/static/uploads /app/logs
|
||||
|
||||
# Copy pre-built HAPI WAR and configuration
|
||||
COPY hapi-fhir-jpaserver/target/ROOT.war /usr/local/tomcat/webapps/
|
||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/conf/
|
||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /app/config/application.yaml
|
||||
COPY hapi-fhir-jpaserver/target/classes/application.yaml /usr/local/tomcat/webapps/app/config/application.yaml
|
||||
COPY hapi-fhir-jpaserver/custom/ /usr/local/tomcat/webapps/custom/
|
||||
|
||||
# Install supervisord
|
||||
RUN pip install supervisor
|
||||
|
||||
# Configure supervisord
|
||||
COPY supervisord.conf /etc/supervisord.conf
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 5000 8080
|
||||
|
||||
# Start supervisord
|
||||
CMD ["supervisord", "-c", "/etc/supervisord.conf"]
|
@ -1,61 +0,0 @@
|
||||
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
|
205
app/auth.py
205
app/auth.py
@ -1,205 +0,0 @@
|
||||
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, ChangePasswordForm
|
||||
from werkzeug.security import generate_password_hash
|
||||
|
||||
auth_bp = Blueprint('auth', __name__)
|
||||
|
||||
google = None
|
||||
github = None
|
||||
|
||||
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',
|
||||
)
|
||||
|
||||
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:
|
||||
if current_user.force_password_change:
|
||||
return redirect(url_for('auth.change_password'))
|
||||
return redirect(url_for('gallery.landing'))
|
||||
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)
|
||||
if user.force_password_change:
|
||||
flash('Please change your password before continuing.', 'warning')
|
||||
return redirect(url_for('auth.change_password'))
|
||||
flash('Logged in successfully!', 'success')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
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:
|
||||
if current_user.force_password_change:
|
||||
return redirect(url_for('auth.change_password'))
|
||||
return redirect(url_for('gallery.landing'))
|
||||
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,
|
||||
is_admin=False,
|
||||
force_password_change=False
|
||||
)
|
||||
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.landing'))
|
||||
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'],
|
||||
is_admin=False,
|
||||
force_password_change=False
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
if user.force_password_change:
|
||||
flash('Please change your password before continuing.', 'warning')
|
||||
return redirect(url_for('auth.change_password'))
|
||||
flash('Logged in with Google!', 'success')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
|
||||
@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']),
|
||||
is_admin=False,
|
||||
force_password_change=False
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
login_user(user)
|
||||
if user.force_password_change:
|
||||
flash('Please change your password before continuing.', 'warning')
|
||||
return redirect(url_for('auth.change_password'))
|
||||
flash('Logged in with GitHub!', 'success')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
|
||||
@auth_bp.route('/change-password', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def change_password():
|
||||
form = ChangePasswordForm()
|
||||
if form.validate_on_submit():
|
||||
if not current_user.check_password(form.current_password.data):
|
||||
flash('Current password is incorrect.', 'danger')
|
||||
return render_template('change_password.html', form=form)
|
||||
current_user.set_password(form.new_password.data)
|
||||
current_user.force_password_change = False
|
||||
db.session.commit()
|
||||
flash('Password changed successfully!', 'success')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
return render_template('change_password.html', form=form)
|
||||
|
||||
@auth_bp.route('/logout')
|
||||
@login_required
|
||||
def logout():
|
||||
logout_user()
|
||||
flash('Logged out successfully.', 'success')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
|
||||
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')
|
98
app/forms.py
98
app/forms.py
@ -1,98 +0,0 @@
|
||||
from flask_wtf import FlaskForm
|
||||
from flask_wtf.file import FileField, FileAllowed, MultipleFileField
|
||||
from wtforms import StringField, TextAreaField, SubmitField, PasswordField, SelectField, SelectMultipleField, BooleanField
|
||||
from wtforms.validators import DataRequired, Email, Length, EqualTo, Optional, ValidationError
|
||||
import re
|
||||
from app.models import Category, OSSupport, FHIRSupport, PricingLicense, DesignedFor, User
|
||||
from app import db
|
||||
|
||||
def validate_url_or_path(form, field):
|
||||
if not field.data:
|
||||
return
|
||||
# Allow upload paths like /uploads/<uuid>_<filename>.jpg|png
|
||||
if field.data.startswith('/uploads/'):
|
||||
path_pattern = r'^/uploads/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}_[\w\-\.]+\.(jpg|png)$'
|
||||
if re.match(path_pattern, field.data, re.I):
|
||||
return
|
||||
# Allow external 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.')
|
||||
|
||||
def validate_username(form, field):
|
||||
if form.user and field.data != form.user.username: # Check if username changed
|
||||
existing_user = User.query.filter_by(username=field.data).first()
|
||||
if existing_user:
|
||||
raise ValidationError('Username already taken.')
|
||||
|
||||
def validate_email(form, field):
|
||||
if form.user and field.data != form.user.email: # Check if email changed
|
||||
existing_user = User.query.filter_by(email=field.data).first()
|
||||
if existing_user:
|
||||
raise ValidationError('Email already registered.')
|
||||
|
||||
class FHIRAppForm(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)])
|
||||
website = StringField('Company Website', validators=[Optional(), Length(max=200)], render_kw={"placeholder": "https://example.com"})
|
||||
designed_for = SelectField('Designed For', coerce=int, validators=[DataRequired()])
|
||||
fhir_compatibility = SelectField('FHIR Compatibility', coerce=int, validators=[DataRequired()])
|
||||
categories = SelectMultipleField('Categories', coerce=int, validators=[DataRequired()])
|
||||
licensing_pricing = SelectField('Licensing & Pricing', coerce=int, validators=[DataRequired()])
|
||||
os_support = SelectMultipleField('OS Support', coerce=int, validators=[DataRequired()])
|
||||
app_image_urls = TextAreaField('App Image URLs (one per line)', validators=[Optional(), Length(max=1000)], render_kw={"placeholder": "e.g., https://example.com/image1.png"})
|
||||
app_image_uploads = MultipleFileField('Upload App Images', validators=[FileAllowed(['jpg', 'png'], 'Images only!')])
|
||||
submit = SubmitField('Register App')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FHIRAppForm, self).__init__(*args, **kwargs)
|
||||
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.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()]
|
||||
|
||||
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')
|
||||
|
||||
class CategoryForm(FlaskForm):
|
||||
name = StringField('Category Name', validators=[DataRequired(), Length(min=3, max=100)])
|
||||
submit = SubmitField('Save Category')
|
||||
|
||||
class ChangePasswordForm(FlaskForm):
|
||||
current_password = PasswordField('Current Password', validators=[DataRequired()])
|
||||
new_password = PasswordField('New Password', validators=[DataRequired(), Length(min=6)])
|
||||
confirm_password = PasswordField('Confirm New Password', validators=[DataRequired(), EqualTo('new_password')])
|
||||
submit = SubmitField('Change Password')
|
||||
|
||||
class UserEditForm(FlaskForm):
|
||||
username = StringField('Username', validators=[DataRequired(), Length(min=3, max=80), validate_username])
|
||||
email = StringField('Email', validators=[DataRequired(), Email(), validate_email])
|
||||
is_admin = BooleanField('Admin Status')
|
||||
force_password_change = BooleanField('Force Password Change')
|
||||
reset_password = PasswordField('Reset Password', validators=[Optional(), Length(min=6)])
|
||||
confirm_reset_password = PasswordField('Confirm Reset Password', validators=[Optional(), EqualTo('reset_password')])
|
||||
submit = SubmitField('Save Changes')
|
||||
|
||||
def __init__(self, user=None, *args, **kwargs):
|
||||
super(UserEditForm, self).__init__(*args, **kwargs)
|
||||
self.user = user # Store the user object for validation
|
@ -1,74 +0,0 @@
|
||||
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))
|
||||
is_admin = db.Column(db.Boolean, default=False)
|
||||
force_password_change = db.Column(db.Boolean, default=False)
|
||||
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 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 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 FHIRApp(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)
|
||||
website = db.Column(db.String(200))
|
||||
designed_for_id = db.Column(db.Integer, db.ForeignKey('designed_for.id'))
|
||||
fhir_compatibility_id = db.Column(db.Integer, db.ForeignKey('fhir_support.id'))
|
||||
categories = db.Column(db.Text) # Comma-separated Category 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)
|
||||
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)
|
392
app/routes.py
392
app/routes.py
@ -1,392 +0,0 @@
|
||||
from flask import Blueprint, render_template, redirect, url_for, flash, request, send_from_directory, abort
|
||||
from flask_login import login_required, current_user
|
||||
from app import db
|
||||
from app.models import FHIRApp, Category, OSSupport, FHIRSupport, PricingLicense, DesignedFor, User
|
||||
from app.forms import FHIRAppForm, GalleryFilterForm, CategoryForm, UserEditForm
|
||||
from sqlalchemy import or_
|
||||
import os
|
||||
import logging
|
||||
from werkzeug.utils import secure_filename
|
||||
import uuid
|
||||
|
||||
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('/uploads/<path:filename>')
|
||||
def uploaded_file(filename):
|
||||
absolute_upload_folder = os.path.abspath(UPLOAD_FOLDER)
|
||||
logger.debug(f"Attempting to serve file: {filename} from {absolute_upload_folder}")
|
||||
try:
|
||||
return send_from_directory(absolute_upload_folder, filename)
|
||||
except FileNotFoundError:
|
||||
logger.error(f"File not found: {os.path.join(absolute_upload_folder, filename)}")
|
||||
abort(404)
|
||||
|
||||
@gallery_bp.route('/')
|
||||
def index():
|
||||
return redirect(url_for('gallery.landing'))
|
||||
|
||||
@gallery_bp.route('/landing')
|
||||
def landing():
|
||||
featured_apps = FHIRApp.query.order_by(FHIRApp.registration_date.desc()).limit(6).all()
|
||||
categories = Category.query.all()
|
||||
return render_template('landing.html', featured_apps=featured_apps, categories=categories)
|
||||
|
||||
@gallery_bp.route('/gallery', methods=['GET', 'POST'])
|
||||
def gallery():
|
||||
form = GalleryFilterForm()
|
||||
query = FHIRApp.query
|
||||
filter_params = {}
|
||||
|
||||
search_term = request.args.get('search', '').strip()
|
||||
if search_term:
|
||||
query = query.filter(
|
||||
or_(
|
||||
FHIRApp.name.ilike(f'%{search_term}%'),
|
||||
FHIRApp.description.ilike(f'%{search_term}%'),
|
||||
FHIRApp.developer.ilike(f'%{search_term}%')
|
||||
)
|
||||
)
|
||||
filter_params['search'] = search_term
|
||||
|
||||
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)
|
||||
pricing_license_ids = request.args.getlist('pricing_license', type=int)
|
||||
designed_for_ids = request.args.getlist('designed_for', type=int)
|
||||
|
||||
if category_ids:
|
||||
query = query.filter(or_(*[FHIRApp.categories.contains(str(cid)) for cid in category_ids]))
|
||||
filter_params['category'] = category_ids
|
||||
if os_support_ids:
|
||||
query = query.filter(or_(*[FHIRApp.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(FHIRApp.fhir_compatibility_id.in_(fhir_support_ids))
|
||||
filter_params['fhir_support'] = fhir_support_ids
|
||||
if pricing_license_ids:
|
||||
query = query.filter(FHIRApp.licensing_pricing_id.in_(pricing_license_ids))
|
||||
filter_params['pricing_license'] = pricing_license_ids
|
||||
if designed_for_ids:
|
||||
query = query.filter(FHIRApp.designed_for_id.in_(designed_for_ids))
|
||||
filter_params['designed_for'] = designed_for_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,
|
||||
categories=Category.query.all(),
|
||||
os_supports=OSSupport.query.all(),
|
||||
fhir_supports=FHIRSupport.query.all(),
|
||||
pricing_licenses=PricingLicense.query.all(),
|
||||
designed_fors=DesignedFor.query.all()
|
||||
)
|
||||
|
||||
@gallery_bp.route('/gallery/<int:app_id>')
|
||||
def app_detail(app_id):
|
||||
app = FHIRApp.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_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()
|
||||
|
||||
return render_template(
|
||||
'app_detail.html',
|
||||
app=app,
|
||||
app_categories=app_categories,
|
||||
app_os_supports=app_os_supports
|
||||
)
|
||||
|
||||
@gallery_bp.route('/gallery/register', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def register():
|
||||
form = FHIRAppForm()
|
||||
if form.validate_on_submit():
|
||||
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)
|
||||
|
||||
logo_url = form.logo_url.data
|
||||
if form.logo_upload.data:
|
||||
file = form.logo_upload.data
|
||||
if allowed_file(file.filename):
|
||||
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||
save_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
logger.debug(f"Attempting to save logo to {save_path}")
|
||||
try:
|
||||
file.save(save_path)
|
||||
if os.path.exists(save_path):
|
||||
logger.debug(f"Successfully saved logo to {save_path}")
|
||||
else:
|
||||
logger.error(f"Failed to save logo to {save_path}")
|
||||
flash('Failed to save logo.', 'danger')
|
||||
return render_template('register.html', form=form)
|
||||
logo_url = f"/uploads/{filename}"
|
||||
logger.debug(f"Set logo_url to {logo_url}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving logo to {save_path}: {e}")
|
||||
flash('Error saving logo.', 'danger')
|
||||
return render_template('register.html', form=form)
|
||||
|
||||
app_images = []
|
||||
if form.app_image_urls.data:
|
||||
app_images.extend([url.strip() for url in form.app_image_urls.data.splitlines() if url.strip().startswith(('http://', 'https://'))])
|
||||
if form.app_image_uploads.data:
|
||||
for file in form.app_image_uploads.data:
|
||||
if file and allowed_file(file.filename):
|
||||
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||
save_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
logger.debug(f"Attempting to save app image to {save_path}")
|
||||
try:
|
||||
file.save(save_path)
|
||||
if os.path.exists(save_path):
|
||||
logger.debug(f"Successfully saved app image to {save_path}")
|
||||
else:
|
||||
logger.error(f"Failed to save app image to {save_path}")
|
||||
flash('Failed to save app image.', 'danger')
|
||||
return render_template('register.html', form=form)
|
||||
app_images.append(f"/uploads/{filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving app image to {save_path}: {e}")
|
||||
flash('Error saving app image.', 'danger')
|
||||
return render_template('register.html', form=form)
|
||||
|
||||
app = FHIRApp(
|
||||
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,
|
||||
website=form.website.data or None,
|
||||
designed_for_id=form.designed_for.data,
|
||||
fhir_compatibility_id=form.fhir_compatibility.data,
|
||||
categories=','.join(map(str, form.categories.data)) if form.categories.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,
|
||||
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 = FHIRApp.query.get_or_404(app_id)
|
||||
if not current_user.is_admin and 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 = FHIRAppForm(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.os_support:
|
||||
form.os_support.data = [int(oid) for oid in app.os_support.split(',') if oid]
|
||||
if app.app_images:
|
||||
current_images = [img for img in app.app_images.split(',') if img.startswith(('http://', 'https://', '/uploads/'))]
|
||||
form.app_image_urls.data = '\n'.join(current_images)
|
||||
else:
|
||||
form.app_image_urls.data = ''
|
||||
|
||||
if form.validate_on_submit():
|
||||
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)
|
||||
|
||||
logo_url = form.logo_url.data
|
||||
if form.logo_upload.data:
|
||||
file = form.logo_upload.data
|
||||
if allowed_file(file.filename):
|
||||
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||
save_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
logger.debug(f"Attempting to save updated logo to {save_path}")
|
||||
try:
|
||||
file.save(save_path)
|
||||
if os.path.exists(save_path):
|
||||
logger.debug(f"Successfully saved updated logo to {save_path}")
|
||||
else:
|
||||
logger.error(f"Failed to save updated logo to {save_path}")
|
||||
flash('Failed to save logo.', 'danger')
|
||||
return render_template('edit_app.html', form=form, app=app)
|
||||
logo_url = f"/uploads/{filename}"
|
||||
logger.debug(f"Set logo_url to {logo_url}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving updated logo to {save_path}: {e}")
|
||||
flash('Error saving logo.', 'danger')
|
||||
return render_template('edit_app.html', form=form, app=app)
|
||||
elif not logo_url:
|
||||
logo_url = app.logo_url
|
||||
|
||||
app_images = [url.strip() for url in form.app_image_urls.data.splitlines() if url.strip()]
|
||||
if form.app_image_uploads.data:
|
||||
for file in form.app_image_uploads.data:
|
||||
if file and allowed_file(file.filename):
|
||||
filename = secure_filename(f"{uuid.uuid4()}_{file.filename}")
|
||||
save_path = os.path.join(UPLOAD_FOLDER, filename)
|
||||
logger.debug(f"Attempting to save updated app image to {save_path}")
|
||||
try:
|
||||
file.save(save_path)
|
||||
if os.path.exists(save_path):
|
||||
logger.debug(f"Successfully saved updated app image to {save_path}")
|
||||
else:
|
||||
logger.error(f"Failed to save updated app image to {save_path}")
|
||||
flash('Failed to save app image.', 'danger')
|
||||
return render_template('edit_app.html', form=form, app=app)
|
||||
app_images.append(f"/uploads/{filename}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving updated app image to {save_path}: {e}")
|
||||
flash('Error saving app image.', 'danger')
|
||||
return render_template('edit_app.html', form=form, app=app)
|
||||
|
||||
app.name = form.name.data
|
||||
app.description = form.description.data
|
||||
app.developer = form.developer.data
|
||||
app.contact_email = form.contact_email.data
|
||||
app.logo_url = logo_url
|
||||
app.launch_url = form.launch_url.data
|
||||
app.website = form.website.data or None
|
||||
app.designed_for_id = form.designed_for.data
|
||||
app.fhir_compatibility_id = form.fhir_compatibility.data
|
||||
app.categories = ','.join(map(str, form.categories.data)) if form.categories.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
|
||||
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 = FHIRApp.query.get_or_404(app_id)
|
||||
if not current_user.is_admin and 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 = FHIRApp.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template('my_listings.html', apps=apps)
|
||||
|
||||
@gallery_bp.route('/admin/categories', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def manage_categories():
|
||||
if not current_user.is_admin:
|
||||
flash('Admin access required.', 'danger')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
form = CategoryForm()
|
||||
if form.validate_on_submit():
|
||||
category = Category(name=form.name.data)
|
||||
db.session.add(category)
|
||||
db.session.commit()
|
||||
flash('Category added successfully!', 'success')
|
||||
return redirect(url_for('gallery.manage_categories'))
|
||||
categories = Category.query.all()
|
||||
return render_template('admin_categories.html', form=form, categories=categories)
|
||||
|
||||
@gallery_bp.route('/admin/categories/delete/<int:category_id>', methods=['POST'])
|
||||
@login_required
|
||||
def delete_category(category_id):
|
||||
if not current_user.is_admin:
|
||||
flash('Admin access required.', 'danger')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
category = Category.query.get_or_404(category_id)
|
||||
db.session.delete(category)
|
||||
db.session.commit()
|
||||
flash(f'Category "{category.name}" deleted successfully.', 'success')
|
||||
return redirect(url_for('gallery.manage_categories'))
|
||||
|
||||
@gallery_bp.route('/admin/apps')
|
||||
@login_required
|
||||
def admin_apps():
|
||||
if not current_user.is_admin:
|
||||
flash('Admin access required.', 'danger')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
apps = FHIRApp.query.all()
|
||||
return render_template('admin_apps.html', apps=apps)
|
||||
|
||||
@gallery_bp.route('/admin/users', methods=['GET'])
|
||||
@login_required
|
||||
def admin_users():
|
||||
if not current_user.is_admin:
|
||||
flash('Admin access required.', 'danger')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
users = User.query.all()
|
||||
return render_template('admin_users.html', users=users)
|
||||
|
||||
@gallery_bp.route('/admin/users/edit/<int:user_id>', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
def edit_user(user_id):
|
||||
if not current_user.is_admin:
|
||||
flash('Admin access required.', 'danger')
|
||||
return redirect(url_for('gallery.landing'))
|
||||
user = User.query.get_or_404(user_id)
|
||||
form = UserEditForm(user=user, obj=user)
|
||||
if form.validate_on_submit():
|
||||
user.username = form.username.data
|
||||
user.email = form.email.data
|
||||
user.is_admin = form.is_admin.data
|
||||
user.force_password_change = form.force_password_change.data
|
||||
if form.reset_password.data:
|
||||
user.set_password(form.reset_password.data)
|
||||
try:
|
||||
db.session.commit()
|
||||
flash(f'User "{user.username}" updated successfully.', 'success')
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating user: {e}")
|
||||
db.session.rollback()
|
||||
flash('Error updating user.', 'danger')
|
||||
return redirect(url_for('gallery.admin_users'))
|
||||
return render_template('admin_edit_user.html', form=form, user=user)
|
@ -1,37 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Manage All Apps{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Manage All Apps</h1>
|
||||
{% if apps %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Developer</th>
|
||||
<th>Submitted By</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for app in apps %}
|
||||
<tr>
|
||||
<td>{{ app.name }}</td>
|
||||
<td>{{ app.developer }}</td>
|
||||
<td>{{ app.user.username }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary btn-sm">View</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>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No apps registered yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,42 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Manage Categories{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Manage Categories</h1>
|
||||
<form method="POST" class="mb-4">
|
||||
{{ 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>
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
</form>
|
||||
{% if categories %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for category in categories %}
|
||||
<tr>
|
||||
<td>{{ category.name }}</td>
|
||||
<td>
|
||||
<form action="{{ url_for('gallery.delete_category', category_id=category.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 {{ category.name }}?')">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No categories defined yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,55 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Edit User{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Edit User: {{ user.username }}</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.is_admin.label(class="form-check-label") }}
|
||||
{{ form.is_admin(class="form-check-input") }}
|
||||
{% for error in form.is_admin.errors %}
|
||||
<span class="text-danger">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.force_password_change.label(class="form-check-label") }}
|
||||
{{ form.force_password_change(class="form-check-input") }}
|
||||
{% for error in form.force_password_change.errors %}
|
||||
<span class="text-danger">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.reset_password.label(class="form-label") }}
|
||||
{{ form.reset_password(class="form-control") }}
|
||||
<small class="form-text text-muted">Leave blank to keep the current password.</small>
|
||||
{% for error in form.reset_password.errors %}
|
||||
<span class="text-danger">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.confirm_reset_password.label(class="form-label") }}
|
||||
{{ form.confirm_reset_password(class="form-control") }}
|
||||
{% for error in form.confirm_reset_password.errors %}
|
||||
<span class="text-danger">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('gallery.admin_users') }}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,35 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Manage Users{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Manage Users</h1>
|
||||
{% if users %}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Admin</th>
|
||||
<th>Force Password Change</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for user in users %}
|
||||
<tr>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>{{ 'Yes' if user.is_admin else 'No' }}</td>
|
||||
<td>{{ 'Yes' if user.force_password_change else 'No' }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('gallery.edit_user', user_id=user.id) }}" class="btn btn-warning btn-sm">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No users found.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,248 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}{{ app.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="card shadow-sm mx-auto" style="max-width: 900px; border: none; border-radius: 10px; animation: fadeIn 0.5s;">
|
||||
<div class="card-body p-4">
|
||||
{% if app.logo_url %}
|
||||
<img src="{{ app.logo_url }}" alt="{{ app.name }} logo" class="d-block mx-auto mb-3" style="width: 100px; height: 100px; object-fit: contain;" onerror="this.src='https://via.placeholder.com/100?text=No+Logo';">
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/100?text=No+Logo" alt="No Logo" class="d-block mx-auto mb-3" style="width: 100px; height: 100px; object-fit: contain;">
|
||||
{% endif %}
|
||||
<h1 class="text-center mb-2">{{ app.name }}</h1>
|
||||
<h5 class="text-center text-muted mb-3">
|
||||
<a href="{{ url_for('gallery.gallery', search=app.developer) }}" class="text-decoration-none">{{ app.developer }}</a>
|
||||
</h5>
|
||||
<div class="d-flex justify-content-center gap-2 mb-4">
|
||||
{% if app.website %}
|
||||
<a href="{{ app.website }}" class="btn btn-primary" target="_blank" rel="noopener noreferrer" aria-label="Visit {{ app.name }} website">Website</a>
|
||||
{% else %}
|
||||
<button class="btn btn-primary" disabled>Website</button>
|
||||
{% endif %}
|
||||
{% if app.launch_url %}
|
||||
<a href="{{ app.launch_url }}" class="btn btn-outline-primary" target="_blank" rel="noopener noreferrer" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<hr class="my-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-6">
|
||||
{% if app.app_images %}
|
||||
<div id="appImagesCarousel" class="carousel slide mb-4" data-bs-ride="carousel">
|
||||
<div class="carousel-inner">
|
||||
{% for img_url in app.app_images.split(',') %}
|
||||
<div class="carousel-item {% if loop.first %}active{% endif %}">
|
||||
<img src="{{ img_url }}" class="d-block w-100" alt="App Screenshot {{ loop.index }}" style="max-height: 300px; object-fit: contain;" onerror="this.src='https://via.placeholder.com/300?text=No+Image';">
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button class="carousel-control-prev" type="button" data-bs-target="#appImagesCarousel" data-bs-slide="prev">
|
||||
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Previous</span>
|
||||
</button>
|
||||
<button class="carousel-control-next" type="button" data-bs-target="#appImagesCarousel" data-bs-slide="next">
|
||||
<span class="carousel-control-next-icon" aria-hidden="true"></span>
|
||||
<span class="visually-hidden">Next</span>
|
||||
</button>
|
||||
</div>
|
||||
{% else %}
|
||||
<img src="https://via.placeholder.com/300?text=No+Image" class="d-block w-100 mb-4" alt="No Image" style="max-height: 300px; object-fit: contain;">
|
||||
{% endif %}
|
||||
<section class="mb-3">
|
||||
<h5>Description</h5>
|
||||
<hr class="small-hr">
|
||||
<p>{{ app.description or 'No description provided.' }}</p>
|
||||
</section>
|
||||
<section class="mb-3">
|
||||
<h5>Contact Email</h5>
|
||||
<hr class="small-hr">
|
||||
<p>{{ app.contact_email or 'Not specified.' }}</p>
|
||||
</section>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<section class="mb-3">
|
||||
<h5>Website</h5>
|
||||
<hr class="small-hr">
|
||||
<p>
|
||||
{% if app.website %}
|
||||
<a href="{{ app.website }}" class="text-decoration-none" target="_blank" rel="noopener noreferrer">{{ app.website }}</a>
|
||||
{% else %}
|
||||
Not specified.
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
{% if app.website %}
|
||||
<section class="mb-3">
|
||||
<h5>Security and Privacy Policy</h5>
|
||||
<hr class="small-hr">
|
||||
<p><a href="{{ app.website }}/security-and-privacy" class="text-decoration-none" target="_blank" rel="noopener noreferrer">{{ app.website }}/security-and-privacy</a></p>
|
||||
</section>
|
||||
{% endif %}
|
||||
<section class="mb-3">
|
||||
<h5>Launch URL</h5>
|
||||
<hr class="small-hr">
|
||||
<p><a href="{{ app.launch_url }}" class="text-decoration-none" target="_blank" rel="noopener noreferrer">{{ app.launch_url }}</a></p>
|
||||
</section>
|
||||
<section class="mb-3">
|
||||
<div class="row g-2">
|
||||
<div class="col-6">
|
||||
<h5>Designed For</h5>
|
||||
<hr class="small-hr">
|
||||
<p>
|
||||
{% if app.designed_for %}
|
||||
<a href="{{ url_for('gallery.gallery', designed_for=app.designed_for_id) }}" class="text-decoration-none">{{ app.designed_for.name }}</a>
|
||||
{% else %}
|
||||
Not specified.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-6">
|
||||
<h5>FHIR Compatibility</h5>
|
||||
<.ttf class="small-hr">
|
||||
<p>
|
||||
{% if app.fhir_compatibility %}
|
||||
<a href="{{ url_for('gallery.gallery', fhir_support=app.fhir_compatibility_id) }}" class="text-decoration-none">{{ app.fhir_compatibility.name }}</a>
|
||||
{% else %}
|
||||
Not specified.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section class="mb-3">
|
||||
<h5>OS Support</h5>
|
||||
<hr class="small-hr">
|
||||
<p>
|
||||
{% if app_os_supports %}
|
||||
{% for os in app_os_supports %}
|
||||
<a href="{{ url_for('gallery.gallery', os_support=os.id) }}" class="text-decoration-none">{{ os.name }}</a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
Not specified.
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-3">
|
||||
<h5>Categories</h5>
|
||||
<hr class="small-hr">
|
||||
<p>
|
||||
{% if app_categories %}
|
||||
{% for category in app_categories %}
|
||||
<a href="{{ url_for('gallery.gallery', category=category.id) }}" class="text-decoration-none">{{ category.name }}</a>{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
Not specified.
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
<section class="mb-3">
|
||||
<h5>Licensing & Pricing</h5>
|
||||
<hr class="small-hr">
|
||||
<p>
|
||||
{% if app.licensing_pricing %}
|
||||
<a href="{{ url_for('gallery.gallery', pricing_license=app.licensing_pricing_id) }}" class="text-decoration-none">{{ app.licensing_pricing.name }}</a>
|
||||
{% else %}
|
||||
Not specified.
|
||||
{% endif %}
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% if current_user.is_authenticated and (current_user.is_admin or current_user.id == app.user_id) %}
|
||||
<div class="d-flex justify-content-center gap-2 mt-4">
|
||||
<a href="{{ url_for('gallery.edit_app', app_id=app.id) }}" class="btn btn-primary" aria-label="Edit {{ app.name }}">Edit App</a>
|
||||
<form action="{{ url_for('gallery.delete_app', app_id=app.id) }}" method="POST">
|
||||
<button type="submit" class="btn btn-danger" onclick="return confirm('Are you sure you want to delete this app?');" aria-label="Delete {{ app.name }}">Delete App</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 d-flex justify-content-center">
|
||||
<a href="mailto:support@fhirpad.com?subject=Report Listing: {{ app.name }}" class="text-danger text-decoration-none" aria-label="Report issue with {{ app.name }} listing">
|
||||
<i class="fas fa-exclamation-circle me-1"></i> Report Listing
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<style>
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
html[data-theme="dark"] .card {
|
||||
background-color: #343a40;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .text-muted {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
html[data-theme="dark"] hr {
|
||||
border-top: 1px solid #495057;
|
||||
}
|
||||
.small-hr {
|
||||
border-top: 1px solid #e0e0e0;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
html[data-theme="dark"] .small-hr {
|
||||
border-top: 1px solid #495057;
|
||||
}
|
||||
.carousel-control-prev,
|
||||
.carousel-control-next {
|
||||
width: 10%;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.btn-primary {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
html[data-theme="dark"] .btn-primary {
|
||||
background-color: #4dabf7;
|
||||
border-color: #4dabf7;
|
||||
}
|
||||
.btn-outline-primary {
|
||||
color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
html[data-theme="dark"] .btn-outline-primary {
|
||||
color: #4dabf7;
|
||||
border-color: #4dabf7;
|
||||
}
|
||||
.btn-outline-primary:hover {
|
||||
THESE BUTTONS ARE CURRENTLY DISABLED ON THE DETAIL PAGE
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
}
|
||||
html[data-theme="dark"] .btn-outline-primary:hover {
|
||||
background-color: #4dabf7;
|
||||
color: white;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.carousel + section {
|
||||
margin-top: 0;
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
.carousel {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
h5 {
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
p {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.row.g-2 > .col-6 {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
@ -1,478 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>FHIRPAD - FHIR App Platform - {% block title %}{% endblock %}</title>
|
||||
<link href="{{ url_for('static', filename='bootstrap.min.css') }}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css" integrity="sha512-z3gLpd7yknf1YoNbCzqRKc4qyor8gaKU1qmn+CShxbuBusANI9QpRohGBreCFkKxLhei6S9CQXFEbbKuqLg0DA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
background-color: #f8f9fa;
|
||||
color: #212529;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
margin: 0;
|
||||
}
|
||||
main {
|
||||
flex-grow: 1;
|
||||
}
|
||||
h1, h5 {
|
||||
font-weight: 600;
|
||||
}
|
||||
.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 {
|
||||
list-style: none !important;
|
||||
position: absolute;
|
||||
background-color: #ffffff;
|
||||
border: none;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 0;
|
||||
min-width: 160px;
|
||||
z-index: 1000;
|
||||
}
|
||||
.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;
|
||||
background-color: #ffffff;
|
||||
color: #212529;
|
||||
}
|
||||
.app-card:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15) !important;
|
||||
}
|
||||
.card-img-top {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
.card {
|
||||
background-color: #ffffff;
|
||||
}
|
||||
.text-muted {
|
||||
color: #6c757d !important;
|
||||
}
|
||||
.form-check-input:checked {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
.form-check-label i {
|
||||
font-size: 1.2rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
.navbar-controls {
|
||||
gap: 0.5rem;
|
||||
padding-right: 0;
|
||||
}
|
||||
.navbar-search input,
|
||||
.view-toggle {
|
||||
height: 38px;
|
||||
}
|
||||
.navbar-search {
|
||||
width: 200px;
|
||||
}
|
||||
.nav-link.login-link {
|
||||
line-height: 38px;
|
||||
padding: 0 0.5rem;
|
||||
}
|
||||
footer {
|
||||
background-color: #ffffff;
|
||||
height: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 1rem;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
display: inline-flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
}
|
||||
.footer-left a,
|
||||
.footer-right a {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.footer-left a:hover,
|
||||
.footer-right a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.initials-avatar {
|
||||
display: inline-block;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border-radius: 50%;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 30px;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.user-avatar {
|
||||
display: inline-block;
|
||||
border-radius: 50%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(-10px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
html[data-theme="dark"] body {
|
||||
background-color: #212529;
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .navbar-light {
|
||||
background-color: #343a40 !important;
|
||||
border-bottom: 1px solid #495057;
|
||||
}
|
||||
html[data-theme="dark"] .navbar-brand {
|
||||
color: #4dabf7 !important;
|
||||
}
|
||||
html[data-theme="dark"] .nav-link {
|
||||
color: #f8f9fa !important;
|
||||
}
|
||||
html[data-theme="dark"] .nav-link:hover,
|
||||
html[data-theme="dark"] .nav-link.active {
|
||||
color: #4dabf7 !important;
|
||||
border-bottom-color: #4dabf7;
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-menu {
|
||||
background-color: #495057;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-item {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-item:hover {
|
||||
background-color: #6c757d;
|
||||
}
|
||||
html[data-theme="dark"] .dropdown-item.active {
|
||||
background-color: #4dabf7;
|
||||
color: white !important;
|
||||
}
|
||||
html[data-theme="dark"] .app-card {
|
||||
background-color: #343a40;
|
||||
color: #f8f9fa;
|
||||
border: 1px solid #495057;
|
||||
}
|
||||
html[data-theme="dark"] .card {
|
||||
background-color: #343a40;
|
||||
border: 1px solid #495057;
|
||||
}
|
||||
html[data-theme="dark"] .card-img-top {
|
||||
background-color: #495057;
|
||||
}
|
||||
html[data-theme="dark"] .text-muted {
|
||||
color: #adb5bd !important;
|
||||
}
|
||||
html[data-theme="dark"] h1,
|
||||
html[data-theme="dark"] h5 {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .form-label {
|
||||
color: #f8f9fa;
|
||||
}
|
||||
html[data-theme="dark"] .form-control {
|
||||
background-color: #495057;
|
||||
color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
html[data-theme="dark"] .form-control::placeholder {
|
||||
color: #adb5bd;
|
||||
}
|
||||
html[data-theme="dark"] .form-select {
|
||||
background-color: #495057;
|
||||
color: #f8f9fa;
|
||||
border-color: #6c757d;
|
||||
}
|
||||
html[data-theme="dark"] .alert-danger {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-color: #dc3545;
|
||||
}
|
||||
html[data-theme="dark"] .alert-success {
|
||||
background-color: #198754;
|
||||
color: white;
|
||||
border-color: #198754;
|
||||
}
|
||||
html[data-theme="dark"] .form-check-input:checked {
|
||||
background-color: #4dabf7;
|
||||
border-color: #4dabf7;
|
||||
}
|
||||
html[data-theme="dark"] footer {
|
||||
background-color: #343a40;
|
||||
border-top: 1px solid #495057;
|
||||
}
|
||||
html[data-theme="dark"] .footer-left a,
|
||||
html[data-theme="dark"] .footer-right a {
|
||||
color: #4dabf7;
|
||||
}
|
||||
@media (max-width: 576px) {
|
||||
.navbar-controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
.navbar-search {
|
||||
width: 100%;
|
||||
}
|
||||
.navbar-search input,
|
||||
.view-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
.navbar-controls .nav-link,
|
||||
.navbar-controls .form-check,
|
||||
.navbar-controls .dropdown {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
footer {
|
||||
height: auto;
|
||||
padding: 0.5rem;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.footer-left,
|
||||
.footer-right {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.footer-left a,
|
||||
.footer-right a {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-lg navbar-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="{{ url_for('gallery.landing') }}">FHIRPAD - FHIR App Platform</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 d-flex justify-content-between" id="navbarNav">
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'gallery.landing' %}active{% endif %}" href="{{ url_for('gallery.landing') }}">Home</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'gallery.gallery' %}active{% endif %}" href="{{ url_for('gallery.gallery') }}">Gallery</a>
|
||||
</li>
|
||||
{% if current_user.is_authenticated %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'gallery.register' %}active{% endif %}" href="{{ url_for('gallery.register') }}">Register App</a>
|
||||
</li>
|
||||
{% if current_user.is_admin %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'gallery.manage_categories' %}active{% endif %}" href="{{ url_for('gallery.manage_categories') }}">Manage Categories</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'gallery.admin_apps' %}active{% endif %}" href="{{ url_for('gallery.admin_apps') }}">Manage Apps</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link {% if request.endpoint == 'gallery.admin_users' %}active{% endif %}" href="{{ url_for('gallery.admin_users') }}">Manage Users</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="navbar-controls d-flex align-items-center ms-auto">
|
||||
{% if request.endpoint == 'gallery.gallery' %}
|
||||
<form class="navbar-search d-flex" method="GET" action="{{ url_for('gallery.gallery') }}">
|
||||
<input class="form-control form-control-sm me-2" type="search" name="search" placeholder="Search apps..." aria-label="Search apps by name, description, or developer" value="{{ request.args.get('search', '') }}">
|
||||
<button class="btn btn-outline-primary btn-sm" type="submit"><i class="fas fa-search"></i></button>
|
||||
</form>
|
||||
<button class="btn btn-outline-secondary btn-sm view-toggle" id="viewToggle" onclick="toggleView()" aria-label="Toggle between grid and list view">
|
||||
<i class="fas fa-th"></i> <span id="viewText">Grid View</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
<div class="form-check form-switch">
|
||||
<input class="form-check-input" type="checkbox" id="themeToggle" onchange="toggleTheme()" aria-label="Toggle dark mode" {% if request.cookies.get('theme') == 'dark' %}checked{% endif %}>
|
||||
<label class="form-check-label" for="themeToggle">
|
||||
<i class="fas fa-sun"></i>
|
||||
<i class="fas fa-moon d-none"></i>
|
||||
</label>
|
||||
</div>
|
||||
{% if current_user.is_authenticated and current_user.username %}
|
||||
<div class="dropdown">
|
||||
<a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
{% if current_user.email %}
|
||||
<img src="https://www.gravatar.com/avatar/{{ current_user.email | md5 }}?s=30&d=identicon" class="user-avatar" alt="User Avatar" onerror="this.style.display='none';this.nextElementSibling.style.display='inline-block';">
|
||||
{% endif %}
|
||||
<span class="initials-avatar" style="{% if current_user.email %}display: none;{% endif %}">{{ current_user.username[:2] | upper }}</span>
|
||||
{{ current_user.username }}
|
||||
</a>
|
||||
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown">
|
||||
<li><a class="dropdown-item {% if request.endpoint == 'gallery.my_listings' %}active{% endif %}" href="{{ url_for('gallery.my_listings') }}">My Listings</a></li>
|
||||
{% if current_user.force_password_change %}
|
||||
<li><a class="dropdown-item {% if request.endpoint == 'auth.change_password' %}active{% endif %}" href="{{ url_for('auth.change_password') }}">Change Password</a></li>
|
||||
{% endif %}
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
{% else %}
|
||||
<a class="nav-link login-link {% if request.endpoint == 'auth.login' %}active{% endif %}" href="{{ url_for('auth.login') }}">Login</a>
|
||||
<a class="nav-link login-link {% if request.endpoint == 'auth.register' %}active{% endif %}" href="{{ url_for('auth.register') }}">Register</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="flex-grow-1">
|
||||
<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>
|
||||
</main>
|
||||
<footer class="main-footer" role="contentinfo">
|
||||
<div class="container-fluid">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="footer-left">
|
||||
<a href="https://www.hl7.org/fhir/" target="_blank" rel="noreferrer" aria-label="Visit FHIR website">FHIR</a>
|
||||
<a href="/about" aria-label="Learn about FHIRPAD">What is FHIRPAD</a>
|
||||
<a href="mailto:support@fhirpad.com" aria-label="Contact FHIRPAD support">Contact Us</a>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<a href="/documents/privacy.pdf" aria-label="View Privacy Policy">Privacy</a>
|
||||
<a href="/documents/terms.pdf" aria-label="View Terms of Use">Terms of Use</a>
|
||||
<a href="/documents/disclaimer.pdf" aria-label="View Disclaimer">Disclaimer</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="{{ url_for('static', filename='bootstrap.bundle.min.js') }}"></script>
|
||||
<script>
|
||||
function toggleTheme() {
|
||||
const html = document.documentElement;
|
||||
const toggle = document.getElementById('themeToggle');
|
||||
const sunIcon = document.querySelector('.fa-sun');
|
||||
const moonIcon = document.querySelector('.fa-moon');
|
||||
if (toggle.checked) {
|
||||
html.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem('theme', 'dark');
|
||||
sunIcon.classList.add('d-none');
|
||||
moonIcon.classList.remove('d-none');
|
||||
} else {
|
||||
html.removeAttribute('data-theme');
|
||||
localStorage.setItem('theme', 'light');
|
||||
sunIcon.classList.remove('d-none');
|
||||
moonIcon.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleView() {
|
||||
const container = document.getElementById('appContainer');
|
||||
const table = document.getElementById('appTable');
|
||||
const toggle = document.getElementById('viewToggle');
|
||||
const viewText = document.getElementById('viewText');
|
||||
if (!container || !table) return;
|
||||
if (container.classList.contains('d-none')) {
|
||||
container.classList.remove('d-none');
|
||||
table.classList.add('d-none');
|
||||
viewText.textContent = 'Grid View';
|
||||
toggle.querySelector('i').className = 'fas fa-th';
|
||||
localStorage.setItem('view', 'grid');
|
||||
} else {
|
||||
container.classList.add('d-none');
|
||||
table.classList.remove('d-none');
|
||||
viewText.textContent = 'List View';
|
||||
toggle.querySelector('i').className = 'fas fa-list';
|
||||
localStorage.setItem('view', 'list');
|
||||
}
|
||||
}
|
||||
|
||||
function removeFilter(key, value) {
|
||||
const url = new URL(window.location);
|
||||
const params = new URLSearchParams(url.search);
|
||||
if (key === 'search') {
|
||||
params.delete('search');
|
||||
} else {
|
||||
const values = params.getAll(key).filter(v => v !== value.toString());
|
||||
params.delete(key);
|
||||
values.forEach(v => params.append(key, v));
|
||||
}
|
||||
window.location.href = `${url.pathname}?${params.toString()}`;
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
const themeToggle = document.getElementById('themeToggle');
|
||||
const sunIcon = document.querySelector('.fa-sun');
|
||||
const moonIcon = document.querySelector('.fa-moon');
|
||||
if (savedTheme === 'dark' && themeToggle) {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
themeToggle.checked = true;
|
||||
sunIcon.classList.add('d-none');
|
||||
moonIcon.classList.remove('d-none');
|
||||
}
|
||||
const savedView = localStorage.getItem('view');
|
||||
if (savedView === 'list' && document.getElementById('viewToggle')) {
|
||||
toggleView();
|
||||
}
|
||||
const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||
tooltips.forEach(t => new bootstrap.Tooltip(t));
|
||||
const currentPath = window.location.pathname;
|
||||
document.querySelectorAll('#navbarNav .nav-link').forEach(link => {
|
||||
if (link.getAttribute('href') === currentPath) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
document.querySelectorAll('#userDropdown + .dropdown-menu .dropdown-item').forEach(link => {
|
||||
if (link.getAttribute('href') === currentPath) {
|
||||
link.classList.add('active');
|
||||
} else {
|
||||
link.classList.remove('active');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -1,34 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<h1>Change Password</h1>
|
||||
{% if current_user.force_password_change %}
|
||||
<p class="text-warning">You must change your password before continuing.</p>
|
||||
{% endif %}
|
||||
<form method="POST">
|
||||
{{ form.hidden_tag() }}
|
||||
<div class="mb-3">
|
||||
{{ form.current_password.label(class="form-label") }}
|
||||
{{ form.current_password(class="form-control") }}
|
||||
{% for error in form.current_password.errors %}
|
||||
<span class="text-danger">{{ error }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
{{ form.new_password.label(class="form-label") }}
|
||||
{{ form.new_password(class="form-control") }}
|
||||
{% for error in form.new_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>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,126 +0,0 @@
|
||||
{% 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.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 %}
|
||||
</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 %}
|
||||
</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 %}
|
||||
</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 %}
|
||||
</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 %}
|
||||
</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", multiple=True) }}
|
||||
<small class="form-text text-muted">Select multiple images to upload.</small>
|
||||
{% for error in form.app_image_uploads.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 %}
|
@ -1,178 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Gallery{% endblock %}
|
||||
{% macro generate_filter_url(endpoint, filter_key, filter_value, filter_params, search) %}
|
||||
{% set params = {} %}
|
||||
{% for key, values in filter_params.items() %}
|
||||
{% if key != filter_key %}
|
||||
{% for value in values %}
|
||||
{% if params[key] is not defined %}
|
||||
{% set _ = params.update({key: [value]}) %}
|
||||
{% else %}
|
||||
{% set _ = params[key].append(value) %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% set _ = params.update({filter_key: filter_value}) %}
|
||||
{% if search %}
|
||||
{% set _ = params.update({'search': search}) %}
|
||||
{% endif %}
|
||||
{{ url_for(endpoint, **params) }}
|
||||
{% endmacro %}
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
<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='fhirpad.png') }}" alt="FHIRPAD" 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 and not request.args.get('search') %}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('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="{{ generate_filter_url('gallery.gallery', 'category', category.id, filter_params, request.args.get('search', '')) }}">{{ 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="{{ generate_filter_url('gallery.gallery', 'os_support', os.id, filter_params, request.args.get('search', '')) }}">{{ 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="{{ generate_filter_url('gallery.gallery', 'fhir_support', fhir.id, filter_params, request.args.get('search', '')) }}">{{ fhir.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="{{ generate_filter_url('gallery.gallery', 'pricing_license', pricing.id, filter_params, request.args.get('search', '')) }}">{{ 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="{{ generate_filter_url('gallery.gallery', 'designed_for', designed.id, filter_params, request.args.get('search', '')) }}">{{ designed.name }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="container mt-4">
|
||||
<p>Explore FHIR-based apps. Filter to find the perfect app.</p>
|
||||
{% if filter_params or request.args.get('search') %}
|
||||
<div class="mb-3">
|
||||
<h5>Active Filters:</h5>
|
||||
{% for key, values in filter_params.items() %}
|
||||
{% for value in values %}
|
||||
{% set item = {'category': categories, 'os_support': os_supports, 'fhir_support': fhir_supports, 'pricing_license': pricing_licenses, 'designed_for': designed_fors}[key] | selectattr('id', 'equalto', value) | first %}
|
||||
{% if item %}
|
||||
<span class="badge bg-primary me-1 filter-chip" data-key="{{ key }}" data-value="{{ value }}" data-bs-toggle="tooltip" title="Click to remove filter">{{ item.name }} <i class="fas fa-times ms-1" onclick="removeFilter('{{ key }}', {{ value }})"></i></span>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
{% if request.args.get('search') %}
|
||||
<span class="badge bg-primary me-1 filter-chip" data-key="search" data-value="{{ request.args.get('search') | urlencode }}" data-bs-toggle="tooltip" title="Click to clear search">Search: {{ request.args.get('search') }} <i class="fas fa-times ms-1" onclick="removeFilter('search', '{{ request.args.get('search') | urlencode }}')"></i></span>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-sm btn-outline-danger ms-2">Clear All</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if apps %}
|
||||
<div id="appContainer" class="row">
|
||||
{% for app in apps %}
|
||||
<div class="app-item col-md-4 mb-3" style="animation: fadeIn 0.5s;">
|
||||
<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.src='https://via.placeholder.com/150?text=No+Logo';">
|
||||
{% 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>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
{% if app.website %}
|
||||
<a href="{{ app.website }}" class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Visit {{ app.name }} website">Website</a>
|
||||
{% endif %}
|
||||
{% if app.launch_url %}
|
||||
<a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="appTable" class="d-none">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Developer</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for app in apps %}
|
||||
<tr style="animation: fadeIn 0.5s;">
|
||||
<td>{{ app.name }}</td>
|
||||
<td>{{ app.description | truncate(100) }}</td>
|
||||
<td>{{ app.developer }}</td>
|
||||
<td>
|
||||
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary btn-sm">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No apps match your filters or search. Try adjusting the criteria.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,61 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}FHIRPAD Home{% endblock %}
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="text-center mb-5">
|
||||
<img src="{{ url_for('static', filename='fhirpad.png') }}" alt="FHIRPAD" height="60" class="mb-3" onerror="this.style.display='none';">
|
||||
<h1>Welcome to FHIRPAD</h1>
|
||||
<p class="lead">Discover and share FHIR-based applications to enhance healthcare interoperability.</p>
|
||||
<a href="{{ url_for('gallery.register') }}" class="btn btn-primary btn-lg">Submit Your App</a>
|
||||
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-outline-primary btn-lg">Browse Apps</a>
|
||||
</div>
|
||||
|
||||
<!-- Featured Apps -->
|
||||
<h2 class="mb-4">Featured Apps</h2>
|
||||
{% if featured_apps %}
|
||||
<div class="row">
|
||||
{% for app in featured_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.src='https://via.placeholder.com/150?text=No+Logo';">
|
||||
{% 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>
|
||||
<div class="d-flex flex-wrap gap-2 mb-2">
|
||||
{% if app.website %}
|
||||
<a href="{{ app.website }}" class="btn btn-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Visit {{ app.name }} website">Website</a>
|
||||
{% endif %}
|
||||
{% if app.launch_url %}
|
||||
<a href="{{ app.launch_url }}" class="btn btn-outline-primary btn-sm" target="_blank" rel="noopener noreferrer" aria-label="Try {{ app.name }} with sandbox data">Try App</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{{ url_for('gallery.app_detail', app_id=app.id) }}" class="btn btn-primary mt-auto">View Details</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No featured apps yet. Be the first to submit one!</p>
|
||||
{% endif %}
|
||||
|
||||
<!-- Categories -->
|
||||
<h2 class="mt-5 mb-4">Explore by Category</h2>
|
||||
{% if categories %}
|
||||
<div class="row">
|
||||
{% for category in categories %}
|
||||
<div class="col-md-3 mb-3">
|
||||
<a href="{{ url_for('gallery.gallery', category=category.id) }}" class="btn btn-outline-secondary w-100">{{ category.name }}</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">No categories defined yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
@ -1,28 +0,0 @@
|
||||
{% 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 %}
|
@ -1,30 +0,0 @@
|
||||
{% 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 %}
|
@ -1,116 +0,0 @@
|
||||
{% 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 %}
|
||||
provocateur = provocateur + 1
|
||||
<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.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 %}
|
||||
</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 %}
|
||||
</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 %}
|
||||
</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 %}
|
||||
</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 %}
|
||||
</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>
|
||||
{{ form.submit(class="btn btn-primary") }}
|
||||
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-secondary">Cancel</a>
|
||||
</form>
|
||||
{% endblock %}
|
@ -1,37 +0,0 @@
|
||||
{% 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 %}
|
@ -1,3 +0,0 @@
|
||||
commands:
|
||||
docker build -t smartflare .
|
||||
docker run -d -p 5000:5000 -v "$(pwd)/uploads:/app/uploads"-v "$(pwd)/instance:/app/instance" --name smartflare-app2 smartflare
|
@ -1,7 +0,0 @@
|
||||
import os
|
||||
|
||||
class Config:
|
||||
BASE_DIR = os.path.abspath(os.path.dirname(__file__))
|
||||
SQLALCHEMY_DATABASE_URI = f'sqlite:///{os.path.join(BASE_DIR, "instance", "fhirpad.db")}'
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
SECRET_KEY = os.getenv('FHIRPAD_SECRET_KEY', 'your-secure-secret-key')
|
@ -1,13 +1,20 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
fhirpad:
|
||||
build: .
|
||||
ports:
|
||||
- "5009:5009"
|
||||
volumes:
|
||||
- ./instance:/app/instance
|
||||
- ./uploads:/app/uploads
|
||||
environment:
|
||||
- FLASK_APP=app
|
||||
- FLASK_ENV=development
|
||||
command: ["flask", "run", "--host=0.0.0.0", "--port=5009"]
|
||||
version: '3.8'
|
||||
services:
|
||||
fhirflare:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "5000:5000"
|
||||
- "8080:8080" # Keep port exposed, even if Tomcat isn't running useful stuff in Lite
|
||||
volumes:
|
||||
- ./instance:/app/instance
|
||||
- ./static/uploads:/app/static/uploads
|
||||
- ./instance/hapi-h2-data/:/app/h2-data # Keep volume mounts consistent
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- FLASK_APP=app.py
|
||||
- FLASK_ENV=development
|
||||
- NODE_PATH=/usr/lib/node_modules
|
||||
- APP_MODE=lite
|
||||
command: supervisord -c /etc/supervisord.conf
|
||||
|
Binary file not shown.
@ -4,9 +4,10 @@ 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
|
||||
flask-migrate==4.0.7
|
||||
Pytest
|
||||
pyyaml==6.0.1
|
||||
fhir.resources==8.0.0
|
||||
Flask-Migrate==4.1.0
|
||||
cachetools
|
||||
beautifulsoup4
|
||||
feedparser==6.0.11
|
59
seed.py
59
seed.py
@ -1,59 +0,0 @@
|
||||
from app import db, create_app
|
||||
from app.models import Category, OSSupport, FHIRSupport, PricingLicense, DesignedFor, User
|
||||
|
||||
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()
|
||||
|
||||
def seed_admin_user():
|
||||
if not User.query.filter_by(username='admin').first():
|
||||
admin = User(
|
||||
username='admin',
|
||||
email='admin@fhirpad.com',
|
||||
is_admin=True,
|
||||
force_password_change=True
|
||||
)
|
||||
admin.set_password('admin123')
|
||||
db.session.add(admin)
|
||||
db.session.commit()
|
||||
print("Default admin user created: username=admin, password=admin123")
|
||||
|
||||
with app.app_context():
|
||||
db.create_all()
|
||||
|
||||
try:
|
||||
# Seed admin user
|
||||
seed_admin_user()
|
||||
|
||||
# Seed categories
|
||||
categories = [
|
||||
'Care Coordination', 'Clinical Research', 'Data Visualization', ' Disease Management',
|
||||
'Genomics', 'Medication Management', 'Patient Engagement', 'Population Health',
|
||||
'Risk Calculation', 'FHIR Tools', 'Telehealth'
|
||||
]
|
||||
seed_table(Category, categories)
|
||||
|
||||
# Seed OS support
|
||||
os_supports = ['iOS', 'Android', 'Web', 'Mac', 'Windows', 'Linux']
|
||||
seed_table(OSSupport, os_supports)
|
||||
|
||||
# Seed FHIR support
|
||||
fhir_supports = ['DSTU 2', 'STU 3', 'R4', 'R5']
|
||||
seed_table(FHIRSupport, fhir_supports)
|
||||
|
||||
# Seed pricing/licenses
|
||||
pricings = ['Open Source', 'Free', 'Per User', 'Site-Based', 'Subscription']
|
||||
seed_table(PricingLicense, pricings)
|
||||
|
||||
# Seed designed for
|
||||
designed_fors = ['Clinicians', 'Patients', 'Patients & Clinicians', 'IT Professionals']
|
||||
seed_table(DesignedFor, designed_fors)
|
||||
|
||||
print("Database seeded successfully!")
|
||||
except Exception as e:
|
||||
print(f"Seeding failed: {str(e)}")
|
||||
db.session.rollback()
|
7
static/bootstrap.bundle.min.js
vendored
7
static/bootstrap.bundle.min.js
vendored
File diff suppressed because one or more lines are too long
6
static/bootstrap.min.css
vendored
6
static/bootstrap.min.css
vendored
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 248 KiB After Width: | Height: | Size: 222 KiB |
Binary file not shown.
Before Width: | Height: | Size: 819 KiB |
Loading…
x
Reference in New Issue
Block a user