mirror of
https://github.com/Sudo-JHare/FhirPad.git
synced 2025-06-15 12:39:59 +00:00
Add files via upload
This commit is contained in:
parent
212089b4ac
commit
9a5e24323a
@ -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"]
|
35
app/forms.py
35
app/forms.py
@ -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')
|
@ -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)
|
103
app/routes.py
103
app/routes.py
@ -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)
|
55
app/templates/admin_edit_user.html
Normal file
55
app/templates/admin_edit_user.html
Normal 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 %}
|
35
app/templates/admin_users.html
Normal file
35
app/templates/admin_users.html
Normal 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 %}
|
@ -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">
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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 %}
|
||||
|
@ -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 %}
|
@ -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>
|
||||
|
@ -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
20
seed.py
@ -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)}")
|
||||
|
Loading…
x
Reference in New Issue
Block a user