Add files via upload

This commit is contained in:
Joshua Hare 2025-05-11 19:38:55 +10:00 committed by GitHub
parent 212089b4ac
commit 9a5e24323a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 187 additions and 312 deletions

View File

@ -6,7 +6,6 @@ COPY app/ app/
COPY static/ static/
COPY instance/ instance/
COPY config.py .
COPY seed.py .
COPY .env .
EXPOSE 5009
CMD ["flask", "run", "--host=0.0.0.0" "--port=5009"]
CMD ["flask", "run", "--host=0.0.0.0"]

View File

@ -1,9 +1,9 @@
from flask_wtf import FlaskForm
from flask_wtf.file import FileField, FileAllowed
from wtforms import StringField, TextAreaField, SubmitField, PasswordField, SelectField, SelectMultipleField
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 ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport
from app.models import Category, OSSupport, FHIRSupport, PricingLicense, DesignedFor, User
from app import db
def validate_url_or_path(form, field):
@ -17,6 +17,18 @@ def validate_url_or_path(form, field):
if not re.match(url_pattern, field.data):
raise ValidationError('Invalid URL or file path.')
def validate_username(form, field):
if field.data != form._obj.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 field.data != form._obj.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)])
@ -25,31 +37,23 @@ class FHIRAppForm(FlaskForm):
logo_url = StringField('Logo URL', validators=[Optional(), validate_url_or_path], render_kw={"placeholder": "https://example.com/logo.png or leave blank to upload"})
logo_upload = FileField('Upload Logo', validators=[FileAllowed(['jpg', 'png'], 'Images only!')])
launch_url = StringField('Launch URL', validators=[DataRequired(), Length(max=200)])
client_id = StringField('Client ID', validators=[DataRequired(), Length(min=3, max=100)])
scopes = TextAreaField('Scopes (comma-separated)', validators=[DataRequired(), Length(max=500)], render_kw={"placeholder": "patient/Patient.read,launch/patient"})
website = StringField('Company Website', validators=[Optional(), Length(max=200)], render_kw={"placeholder": "https://example.com"})
designed_for = SelectField('Designed For', coerce=int, validators=[DataRequired()])
application_type = SelectField('Application Type', coerce=int, validators=[DataRequired()])
fhir_compatibility = SelectField('FHIR Compatibility', coerce=int, validators=[DataRequired()])
categories = SelectMultipleField('Categories', coerce=int, validators=[DataRequired()])
specialties = SelectMultipleField('Specialties', 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 = FileField('Upload App Images', validators=[FileAllowed(['jpg', 'png'], 'Images only!')])
ehr_support = SelectMultipleField('EHR Support', coerce=int, validators=[DataRequired()])
submit = SubmitField('Register App')
def __init__(self, *args, **kwargs):
super(FHIRAppForm, self).__init__(*args, **kwargs)
self.application_type.choices = [(t.id, t.name) for t in ApplicationType.query.all()]
self.categories.choices = [(c.id, c.name) for c in Category.query.all()]
self.os_support.choices = [(o.id, o.name) for o in OSSupport.query.all()]
self.fhir_compatibility.choices = [(f.id, f.name) for f in FHIRSupport.query.all()]
self.specialties.choices = [(s.id, s.name) for s in Speciality.query.all()]
self.licensing_pricing.choices = [(p.id, p.name) for p in PricingLicense.query.all()]
self.designed_for.choices = [(d.id, d.name) for d in DesignedFor.query.all()]
self.ehr_support.choices = [(e.id, e.name) for e in EHRSupport.query.all()]
class LoginForm(FlaskForm):
email = StringField('Email', validators=[DataRequired(), Email()])
@ -76,4 +80,13 @@ 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')
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')

View File

@ -34,10 +34,6 @@ class User(db.Model):
def get_id(self):
return str(self.id)
class ApplicationType(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class Category(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
@ -50,10 +46,6 @@ class FHIRSupport(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class Speciality(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class PricingLicense(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
@ -62,10 +54,6 @@ class DesignedFor(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class EHRSupport(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True, nullable=False)
class FHIRApp(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
@ -74,18 +62,13 @@ class FHIRApp(db.Model):
contact_email = db.Column(db.String(120), nullable=False)
logo_url = db.Column(db.String(200))
launch_url = db.Column(db.String(200), nullable=False)
client_id = db.Column(db.String(100), nullable=False)
scopes = db.Column(db.Text)
website = db.Column(db.String(200))
designed_for_id = db.Column(db.Integer, db.ForeignKey('designed_for.id'))
application_type_id = db.Column(db.Integer, db.ForeignKey('application_type.id'))
fhir_compatibility_id = db.Column(db.Integer, db.ForeignKey('fhir_support.id'))
categories = db.Column(db.Text) # Comma-separated Category IDs
specialties = db.Column(db.Text) # Comma-separated Speciality IDs
licensing_pricing_id = db.Column(db.Integer, db.ForeignKey('pricing_license.id'))
os_support = db.Column(db.Text) # Comma-separated OSSupport IDs
app_images = db.Column(db.Text)
ehr_support = db.Column(db.Text) # Comma-separated EHRSupport IDs
user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False)
registration_date = db.Column(db.DateTime, default=datetime.utcnow)
last_updated = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

View File

@ -1,8 +1,8 @@
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, ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport
from app.forms import FHIRAppForm, GalleryFilterForm, CategoryForm
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
@ -57,18 +57,12 @@ def gallery():
)
filter_params['search'] = search_term
application_type_ids = request.args.getlist('application_type', type=int)
category_ids = request.args.getlist('category', type=int)
os_support_ids = request.args.getlist('os_support', type=int)
fhir_support_ids = request.args.getlist('fhir_support', type=int)
speciality_ids = request.args.getlist('speciality', type=int)
pricing_license_ids = request.args.getlist('pricing_license', type=int)
designed_for_ids = request.args.getlist('designed_for', type=int)
ehr_support_ids = request.args.getlist('ehr_support', type=int)
if application_type_ids:
query = query.filter(FHIRApp.application_type_id.in_(application_type_ids))
filter_params['application_type'] = application_type_ids
if category_ids:
query = query.filter(or_(*[FHIRApp.categories.contains(str(cid)) for cid in category_ids]))
filter_params['category'] = category_ids
@ -78,18 +72,12 @@ def gallery():
if fhir_support_ids:
query = query.filter(FHIRApp.fhir_compatibility_id.in_(fhir_support_ids))
filter_params['fhir_support'] = fhir_support_ids
if speciality_ids:
query = query.filter(or_(*[FHIRApp.specialties.contains(str(sid)) for sid in speciality_ids]))
filter_params['speciality'] = speciality_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
if ehr_support_ids:
query = query.filter(or_(*[FHIRApp.ehr_support.contains(str(eid)) for eid in ehr_support_ids]))
filter_params['ehr_support'] = ehr_support_ids
apps = query.all()
for app in apps:
@ -99,14 +87,11 @@ def gallery():
apps=apps,
form=form,
filter_params=filter_params,
application_types=ApplicationType.query.all(),
categories=Category.query.all(),
os_supports=OSSupport.query.all(),
fhir_supports=FHIRSupport.query.all(),
specialties=Speciality.query.all(),
pricing_licenses=PricingLicense.query.all(),
designed_fors=DesignedFor.query.all(),
ehr_supports=EHRSupport.query.all()
designed_fors=DesignedFor.query.all()
)
@gallery_bp.route('/gallery/<int:app_id>')
@ -118,28 +103,16 @@ def app_detail(app_id):
category_ids = [int(cid) for cid in app.categories.split(',') if cid]
app_categories = Category.query.filter(Category.id.in_(category_ids)).all()
app_specialties = []
if app.specialties:
speciality_ids = [int(sid) for sid in app.specialties.split(',') if sid]
app_specialties = Speciality.query.filter(Speciality.id.in_(speciality_ids)).all()
app_os_supports = []
if app.os_support:
os_ids = [int(oid) for oid in app.os_support.split(',') if oid]
app_os_supports = OSSupport.query.filter(OSSupport.id.in_(os_ids)).all()
app_ehr_supports = []
if app.ehr_support:
ehr_ids = [int(eid) for eid in app.ehr_support.split(',') if eid]
app_ehr_supports = EHRSupport.query.filter(EHRSupport.id.in_(ehr_ids)).all()
return render_template(
'app_detail.html',
app=app,
app_categories=app_categories,
app_specialties=app_specialties,
app_os_supports=app_os_supports,
app_ehr_supports=app_ehr_supports
app_os_supports=app_os_supports
)
@gallery_bp.route('/gallery/register', methods=['GET', 'POST'])
@ -147,16 +120,6 @@ def app_detail(app_id):
def register():
form = FHIRAppForm()
if form.validate_on_submit():
scopes = form.scopes.data
valid_scopes = all(
scope.strip().startswith(('patient/', 'user/', 'launch', 'openid', 'fhirUser', 'offline_access', 'online_access'))
for scope in scopes.split(',')
if scope.strip()
)
if not valid_scopes or not scopes.strip():
flash('Invalid FHIR scopes. Use formats like patient/Patient.read, launch/patient.', 'danger')
return render_template('register.html', form=form)
try:
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
logger.debug(f"Ensured {UPLOAD_FOLDER} exists")
@ -217,18 +180,13 @@ def register():
contact_email=form.contact_email.data,
logo_url=logo_url or None,
launch_url=form.launch_url.data,
client_id=form.client_id.data,
scopes=scopes,
website=form.website.data or None,
designed_for_id=form.designed_for.data,
application_type_id=form.application_type.data,
fhir_compatibility_id=form.fhir_compatibility.data,
categories=','.join(map(str, form.categories.data)) if form.categories.data else None,
specialties=','.join(map(str, form.specialties.data)) if form.specialties.data else None,
licensing_pricing_id=form.licensing_pricing.data,
os_support=','.join(map(str, form.os_support.data)) if form.os_support.data else None,
app_images=','.join(app_images) if app_images else None,
ehr_support=','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None,
user_id=current_user.id
)
db.session.add(app)
@ -256,12 +214,8 @@ def edit_app(app_id):
if not form.is_submitted():
if app.categories:
form.categories.data = [int(cid) for cid in app.categories.split(',') if cid]
if app.specialties:
form.specialties.data = [int(sid) for sid in app.specialties.split(',') if sid]
if app.os_support:
form.os_support.data = [int(oid) for oid in app.os_support.split(',') if oid]
if app.ehr_support:
form.ehr_support.data = [int(eid) for eid in app.ehr_support.split(',') if eid]
if app.app_images:
current_images = [img for img in app.app_images.split(',') if img.startswith(('http://', 'https://', '/uploads/'))]
form.app_image_urls.data = '\n'.join(current_images)
@ -269,16 +223,6 @@ def edit_app(app_id):
form.app_image_urls.data = ''
if form.validate_on_submit():
scopes = form.scopes.data
valid_scopes = all(
scope.strip().startswith(('patient/', 'user/', 'launch', 'openid', 'fhirUser', 'offline_access', 'online_access'))
for scope in scopes.split(',')
if scope.strip()
)
if not valid_scopes or not scopes.strip():
flash('Invalid FHIR scopes. Use formats like patient/Patient.read, launch/patient.', 'danger')
return render_template('edit_app.html', form=form, app=app)
try:
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
logger.debug(f"Ensured {UPLOAD_FOLDER} exists")
@ -338,18 +282,13 @@ def edit_app(app_id):
app.contact_email = form.contact_email.data
app.logo_url = logo_url
app.launch_url = form.launch_url.data
app.client_id = form.client_id.data
app.scopes = scopes
app.website = form.website.data or None
app.designed_for_id = form.designed_for.data
app.application_type_id = form.application_type.data
app.fhir_compatibility_id = form.fhir_compatibility.data
app.categories = ','.join(map(str, form.categories.data)) if form.categories.data else None
app.specialties = ','.join(map(str, form.specialties.data)) if form.specialties.data else None
app.licensing_pricing_id = form.licensing_pricing.data
app.os_support = ','.join(map(str, form.os_support.data)) if form.os_support.data else None
app.app_images = ','.join(app_images) if app_images else None
app.ehr_support = ','.join(map(str, form.ehr_support.data)) if form.ehr_support.data else None
try:
db.session.commit()
logger.debug(f"Updated app ID: {app.id}, logo_url: {app.logo_url}, app_images: {app.app_images}")
@ -417,3 +356,37 @@ def admin_apps():
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(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)

View File

@ -0,0 +1,55 @@
{% 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 %}

View File

@ -0,0 +1,35 @@
{% 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 %}

View File

@ -55,16 +55,6 @@
<hr class="small-hr">
<p>{{ app.contact_email or 'Not specified.' }}</p>
</section>
<section class="mb-3">
<h5>Client ID</h5>
<hr class="small-hr">
<p>{{ app.client_id or 'Not specified.' }}</p>
</section>
<section class="mb-3">
<h5>Scopes</h5>
<hr class="small-hr">
<p>{{ app.scopes or 'Not specified.' }}</p>
</section>
</div>
<div class="col-md-6">
<section class="mb-3">
@ -103,21 +93,6 @@
{% endif %}
</p>
</div>
<div class="col-6">
<h5>Application Type</h5>
<hr class="small-hr">
<p>
{% if app.application_type %}
<a href="{{ url_for('gallery.gallery', application_type=app.application_type_id) }}" class="text-decoration-none">{{ app.application_type.name }}</a>
{% else %}
Not specified.
{% endif %}
</p>
</div>
</div>
</section>
<section class="mb-3">
<div class="row g-2">
<div class="col-6">
<h5>FHIR Compatibility</h5>
<hr class="small-hr">
@ -129,34 +104,8 @@
{% endif %}
</p>
</div>
<div class="col-6">
<h5>EHR Support</h5>
<hr class="small-hr">
<p>
{% if app_ehr_supports %}
{% for ehr in app_ehr_supports %}
<a href="{{ url_for('gallery.gallery', ehr_support=ehr.id) }}" class="text-decoration-none">{{ ehr.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
Not specified.
{% endif %}
</p>
</div>
</div>
</section>
<section class="mb-3">
<h5>Specialties</h5>
<hr class="small-hr">
<p>
{% if app_specialties %}
{% for speciality in app_specialties %}
<a href="{{ url_for('gallery.gallery', speciality=speciality.id) }}" class="text-decoration-none">{{ speciality.name }}</a>{% if not loop.last %}, {% endif %}
{% endfor %}
{% else %}
Not specified.
{% endif %}
</p>
</section>
<section class="mb-3">
<h5>OS Support</h5>
<hr class="small-hr">

View File

@ -306,6 +306,9 @@
<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>

View File

@ -57,31 +57,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.client_id.label(class="form-label") }}
{{ form.client_id(class="form-control") }}
{% for error in form.client_id.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.scopes.label(class="form-label") }}
{{ form.scopes(class="form-control", rows=3) }}
{% for error in form.scopes.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
{% if app.scopes and app.scopes.strip() %}
<p class="mt-2"><strong>Current Scopes:</strong>
{% for scope in app.scopes.split(',') %}
{% if scope.strip() %}
<span class="badge bg-primary me-1">{{ scope.strip() }}</span>
{% endif %}
{% endfor %}
</p>
{% else %}
<p class="mt-2 text-muted">No scopes defined.</p>
{% endif %}
</div>
<div class="mb-3">
{{ form.website.label(class="form-label") }}
{{ form.website(class="form-control") }}
@ -96,13 +71,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.application_type.label(class="form-label") }}
{{ form.application_type(class="form-control") }}
{% for error in form.application_type.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.fhir_compatibility.label(class="form-label") }}
{{ form.fhir_compatibility(class="form-control") }}
@ -117,13 +85,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.specialties.label(class="form-label") }}
{{ form.specialties(class="form-control") }}
{% for error in form.specialties.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.licensing_pricing.label(class="form-label") }}
{{ form.licensing_pricing(class="form-control") }}
@ -158,13 +119,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.ehr_support.label(class="form-label") }}
{{ form.ehr_support(class="form-control") }}
{% for error in form.ehr_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</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>

View File

@ -35,18 +35,6 @@
<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('application_type') %}active{% endif %}" href="#" id="appTypeDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Application Type
</a>
<ul class="dropdown-menu" aria-labelledby="appTypeDropdown">
{% for app_type in application_types %}
<li>
<a class="dropdown-item {% if app_type.id in filter_params.get('application_type', []) %}active{% endif %}" href="{{ generate_filter_url('gallery.gallery', 'application_type', app_type.id, filter_params, request.args.get('search', '')) }}">{{ app_type.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('category') %}active{% endif %}" href="#" id="categoryDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Categories
@ -66,7 +54,7 @@
<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>
<a class="dropdown-item {% if os.id in filter_params.get('os_support', []) %}active{% endif %}" href="{{ generate_filter_url('gallery.galler', 'os_support', os.id, filter_params, request.args.get('search', '')) }}">{{ os.name }}</a>
</li>
{% endfor %}
</ul>
@ -83,18 +71,6 @@
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('speciality') %}active{% endif %}" href="#" id="specialityDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Speciality
</a>
<ul class="dropdown-menu" aria-labelledby="specialityDropdown">
{% for speciality in specialties %}
<li>
<a class="dropdown-item {% if speciality.id in filter_params.get('speciality', []) %}active{% endif %}" href="{{ generate_filter_url('gallery.gallery', 'speciality', speciality.id, filter_params, request.args.get('search', '')) }}">{{ speciality.name }}</a>
</li>
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('pricing_license') %}active{% endif %}" href="#" id="pricingDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
Pricing/License
@ -119,18 +95,6 @@
{% endfor %}
</ul>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle {% if filter_params.get('ehr_support') %}active{% endif %}" href="#" id="ehrSupportDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false">
EHR Support
</a>
<ul class="dropdown-menu" aria-labelledby="ehrSupportDropdown">
{% for ehr in ehr_supports %}
<li>
<a class="dropdown-item {% if ehr.id in filter_params.get('ehr_support', []) %}active{% endif %}" href="{{ generate_filter_url('gallery.gallery', 'ehr_support', ehr.id, filter_params, request.args.get('search', '')) }}">{{ ehr.name }}</a>
</li>
{% endfor %}
</ul>
</li>
</ul>
</div>
</div>
@ -142,7 +106,7 @@
<h5>Active Filters:</h5>
{% for key, values in filter_params.items() %}
{% for value in values %}
{% set item = {'application_type': application_types, 'category': categories, 'os_support': os_supports, 'fhir_support': fhir_supports, 'speciality': specialties, 'pricing_license': pricing_licenses, 'designed_for': designed_fors, 'ehr_support': ehr_supports}[key] | selectattr('id', 'equalto', value) | first %}
{% 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 %}

View File

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

View File

@ -43,6 +43,7 @@
{{ 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>
@ -53,20 +54,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.client_id.label(class="form-label") }}
{{ form.client_id(class="form-control") }}
{% for error in form.client_id.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.scopes.label(class="form-label") }}
{{ form.scopes(class="form-control", rows=3) }}
{% for error in form.scopes.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.website.label(class="form-label") }}
{{ form.website(class="form-control") }}
@ -81,13 +68,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.application_type.label(class="form-label") }}
{{ form.application_type(class="form-control") }}
{% for error in form.application_type.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.fhir_compatibility.label(class="form-label") }}
{{ form.fhir_compatibility(class="form-control") }}
@ -102,13 +82,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.specialties.label(class="form-label") }}
{{ form.specialties(class="form-control") }}
{% for error in form.specialties.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.licensing_pricing.label(class="form-label") }}
{{ form.licensing_pricing(class="form-control") }}
@ -137,13 +110,6 @@
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
<div class="mb-3">
{{ form.ehr_support.label(class="form-label") }}
{{ form.ehr_support(class="form-control") }}
{% for error in form.ehr_support.errors %}
<span class="text-danger">{{ error }}</span>
{% endfor %}
</div>
{{ form.submit(class="btn btn-primary") }}
<a href="{{ url_for('gallery.gallery') }}" class="btn btn-secondary">Cancel</a>
</form>

View File

@ -8,4 +8,5 @@ email-validator==2.2.0
Flask-Login==0.6.3
Flask-OAuthlib==0.9.6
python-dotenv==1.0.1
bcrypt==4.2.0
bcrypt==4.2.0
flask-migrate==4.0.7

20
seed.py
View File

@ -1,5 +1,5 @@
from app import db, create_app
from app.models import ApplicationType, Category, OSSupport, FHIRSupport, Speciality, PricingLicense, DesignedFor, EHRSupport, User
from app.models import Category, OSSupport, FHIRSupport, PricingLicense, DesignedFor, User
app = create_app()
@ -29,13 +29,9 @@ with app.app_context():
# Seed admin user
seed_admin_user()
# Seed application types
app_types = ['FHIR App', 'Bulk Data', 'Health Cards']
seed_table(ApplicationType, app_types)
# Seed categories
categories = [
'Care Coordination', 'Clinical Research', 'Data Visualization', 'Disease Management',
'Care Coordination', 'Clinical Research', 'Data Visualization', ' Disease Management',
'Genomics', 'Medication Management', 'Patient Engagement', 'Population Health',
'Risk Calculation', 'FHIR Tools', 'Telehealth'
]
@ -49,14 +45,6 @@ with app.app_context():
fhir_supports = ['DSTU 2', 'STU 3', 'R4', 'R5']
seed_table(FHIRSupport, fhir_supports)
# Seed specialties
specialties = [
'Anesthesiology', 'Cardiology', 'Gastroenterology', 'Infectious Disease', 'Neurology',
'Obstetrics', 'Oncology', 'Pediatrics', 'Pulmonology', 'Nephrology', 'Rheumatology',
'Trauma', 'Primary Care'
]
seed_table(Speciality, specialties)
# Seed pricing/licenses
pricings = ['Open Source', 'Free', 'Per User', 'Site-Based', 'Subscription']
seed_table(PricingLicense, pricings)
@ -65,10 +53,6 @@ with app.app_context():
designed_fors = ['Clinicians', 'Patients', 'Patients & Clinicians', 'IT Professionals']
seed_table(DesignedFor, designed_fors)
# Seed EHR support
ehr_supports = ['Allscripts', 'Athenahealth', 'Epic', 'Cerner', 'Meditech']
seed_table(EHRSupport, ehr_supports)
print("Database seeded successfully!")
except Exception as e:
print(f"Seeding failed: {str(e)}")