diff --git a/.gitignore b/.gitignore index f824511cb52ed7707e74299f3af9851f1ad9e7d0..b335a5ef68188b49dfb54329b158ff7434804999 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ __pycache__/ play.py env.py +build_db.py .idea/ \ No newline at end of file diff --git a/application/__init__.py b/application/__init__.py index 3be7f7de387cc24c5cc9e1cef6e3585a5e4623e7..817bdcde7c4cbfa6a181d6adbc69b7041f3644f6 100644 --- a/application/__init__.py +++ b/application/__init__.py @@ -15,6 +15,9 @@ from flask_login import LoginManager #for dates and time from flask_moment import Moment +#for powerful styling tools +from flask_bootstrap import Bootstrap5 + #initiate flask app app = Flask(__name__) @@ -32,6 +35,8 @@ login.login_view = 'login' #sbring them to the endpoint for loggin in, ie the on moment = Moment(app) +bootstrap = Bootstrap5(app) + #import routes and models from application import routes, models #define here so no circular import errors diff --git a/application/forms.py b/application/forms.py index 41fff023de7a7f1d2a4fadd2e9f16e53fce1c981..00228b03c776b144887745ae9754c3d7eb00acd0 100644 --- a/application/forms.py +++ b/application/forms.py @@ -1,25 +1,25 @@ from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, BooleanField, SubmitField -from wtforms.validators import ValidationError, DataRequired, Email, EqualTo -from application.models import User +from wtforms.validators import ValidationError, DataRequired, Email, EqualTo, Optional +from application.models import * class LoginForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired()]) + username = StringField('Username', validators=[DataRequired()], render_kw={"placeholder": "Username", 'style': 'font-size: 20px'}) + password = PasswordField('Password', validators=[DataRequired()], render_kw={"placeholder": "Password", 'style': 'font-size: 20px'}) remember_me = BooleanField('Remember Me') submit = SubmitField('Sign In') class RegistrationForm(FlaskForm): - username = StringField('Username', validators=[DataRequired()]) - email = StringField('Email', validators=[DataRequired(), Email()]) - affiliation = StringField('Affiliation', validators=[DataRequired()]) - password = PasswordField('Password', validators=[DataRequired()]) - password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')]) - is_admin = BooleanField('Give Admin Privledges') + username = StringField('Username', validators=[DataRequired()], render_kw={'class': "form-control"}) + email = StringField('Email', validators=[DataRequired(), Email()], render_kw={'class': "form-control"}) + affiliation = StringField('Affiliation', validators=[DataRequired()], render_kw={'class': "form-control"}) + password = PasswordField('Password', validators=[DataRequired()], render_kw={'class': "form-control"}) + password2 = PasswordField('Repeat Password', validators=[DataRequired(), EqualTo('password')], render_kw={'class': "form-control"}) + is_admin = BooleanField('Admin Privledges') submit = SubmitField('Register') def validate_username(self, username): @@ -34,3 +34,42 @@ class RegistrationForm(FlaskForm): if user is not None: raise ValidationError('Please use a different email address') + +class RecordTest(FlaskForm): + + module = StringField('Module', validators=[Optional()], render_kw={"placeholder": "Module Serial Number", 'class': "form-control"}) + component = StringField('Component', validators=[Optional()], render_kw={"placeholder": "ETROC, LGAD, etc... Serial Number",'class': "form-control"}) + test_type = StringField('test_type', validators=[DataRequired()], render_kw={"placeholder":"What test did you perform?", 'class': "form-control"}) + location = StringField('location', validators=[Optional()], render_kw={"placeholder": "Where did this test take place?",'class': "form-control"}) + test_data = StringField('data', validators=[DataRequired()], render_kw={"placeholder": "Data in the format of a string",'class': "form-control"}) + submit = SubmitField('Record Test') + + def validate(self,extra_validators=None): + #not really sure how this bit works + if not super(RecordTest, self).validate(extra_validators): + return False + #this I get + if not self.module.data and not self.component.data: + msg = 'At least one serial number must be set' + self.module.errors.append(msg) + self.component.errors.append(msg) + return False + return True + + def validate_module(self, module): + module_query = Module.query.filter_by(module_serial_number=module.data).first() + if module_query is None: + raise ValidationError('Module Serial Number does not exist, please register it') + + def validate_component(self, component): + component_query = Component.query.filter_by(component_serial_number=component.data).first() + if component_query is None: + raise ValidationError('Component Serial Number does not exist, please register it') + + def validate_test_type(self, test_type): + test_type_query = TestType.query.filter_by(test_name=test_type.data).first() + if test_type_query is None: + raise ValidationError('Test type does not exist, please register it') + + + diff --git a/application/models.py b/application/models.py index 5589e0312f4c196e793b1399a20f8699fce890c6..b351868bed010220d0a9b392cfba0bd0c7e9ff19 100644 --- a/application/models.py +++ b/application/models.py @@ -40,11 +40,18 @@ class User(UserMixin, db.Model): email: Mapped[str] = mapped_column(db.String(50), nullable=False) affiliation: Mapped[str] = mapped_column(db.String(50), nullable=True) is_admin: Mapped[bool] = mapped_column(default=False, nullable=False) + is_active: Mapped[bool] = mapped_column(default=True, nullable=False) created_at: Mapped[datetime] = mapped_column(default=now_utc, nullable=False) + #Relationships + #'assembly' = the table for hte many part of the relationship (one user can have many assemblies) + #backref='user' = name of a field that will be added to the objects of the many class, ie user, assembly.user + #lazy='dynamic' = gives an object back instead of a direct SQL query + assemblies = db.relationship('Assembly', backref='user', lazy='dynamic') + tests = db.relationship('Test', backref='user', lazy='dynamic') + def set_password(self, password: str) -> None: #is salted - #not working... self.password_hash = generate_password_hash(password) #if need allow password = "..." to set password, checkout flask-sqlalchemy repo flask-sqlalchemy/examples/flaskr/flaskr/auth @@ -56,21 +63,142 @@ class User(UserMixin, db.Model): return f'<User {self.username}>' -# class module(db.Model): -# pass +class Assembly(db.Model): + __tablename__ = "assembly" + + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + user_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('user.id'), nullable=False) + #both module and component_id can be null, depends on the step, should we restrict the possibility of bothing being null? + module_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('module.id'), nullable=True) + component_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('component.id'), nullable=True) + assembly_type_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('assembly_type.id'), nullable=False) + + location: Mapped[str] = mapped_column(db.String(50), unique=False, nullable=True) + completed_at: Mapped[datetime] = mapped_column(default=now_utc, nullable=False) + + data: Mapped[str] = mapped_column(db.String, unique=False, nullable=True) + + def __repr__(self) -> str: + return f'<Assembly {self.id}>' + + +class Test(db.Model): + __tablename__ = "test" + + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + user_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('user.id'), nullable=False) + #both module and component_id can be null, depends on the step, should we restrict the possibility of bothing being null? + module_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('module.id'), nullable=True) + component_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('component.id'), nullable=True) + test_type_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('test_type.id'), nullable=False) + + location: Mapped[str] = mapped_column(db.String(50), unique=False, nullable=True) + completed_at: Mapped[datetime] = mapped_column(default=now_utc, nullable=False) + + data: Mapped[str] = mapped_column(db.String, unique=False, nullable=True) + + def __repr__(self) -> str: + return f'<Test {self.id}>' + + +#-------------SENSOR COMPONENTS------------------# +#module pcb table to track the module pcb +class Module(db.Model): + __tablename__ = "module" + + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + module_type_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('module_type.id'), nullable=False) + + module_serial_number: Mapped[str] = mapped_column(db.String(50), unique=True, nullable=False) + + current_location: Mapped[str] = mapped_column(db.String(50), unique=False, nullable=True) + + #Relationships + assemblies = db.relationship('Assembly', backref='module', lazy='dynamic') + tests = db.relationship('Test', backref='module', lazy='dynamic') + components = db.relationship('Component', backref='module', lazy='dynamic') + + def __repr__(self) -> str: + return f'<Module {self.module_serial_number}>' + + +#ETROCs, LGADs, Baseplates etc... +class Component(db.Model): + __tablename__ = "component" + + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + module_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('module.id'), nullable=True) #for production or throughput demo nullable should be False, every component should have an associated module pcb + + #this gets if its an ETROC, LGAD, etc, any version or type you want + component_type_id: Mapped[int] = mapped_column(db.Integer, db.ForeignKey('component_type.id'), nullable=False) + + component_serial_number: Mapped[str] = mapped_column(db.String(50), unique=True, nullable=False) + + location: Mapped[str] = mapped_column(db.String(50), unique=False, nullable=True) + + #Relationships + assemblies = db.relationship('Assembly', backref='component', lazy='dynamic') + tests = db.relationship('Test', backref='component', lazy='dynamic') -# class assembly(db.Model): -# pass + def __repr__(self) -> str: + return f'<Component {self.component_serial_number}>' +#-------------------------------------------------# -# class assembly_step(db.Model): -# pass -# class component(db.Model): -# pass +#----------Lookup Tables-----------# +class ModuleType(db.Model): + __tablename__ = "module_type" -# class component_type(db.Model): -# pass + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + module_name: Mapped[str] = mapped_column(db.String(50), unique=True, nullable=False) + + modules = db.relationship('Module', backref='module_type', lazy='dynamic') + + def __repr__(self) -> str: + return f'<Module Name {self.module_name}>' + + +class ComponentType(db.Model): + __tablename__ = "component_type" + + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + component_name: Mapped[str] = mapped_column(db.String(50), unique=True, nullable=False) + + #Relationships + components = db.relationship('Component', backref='component_type', lazy='dynamic') + + def __repr__(self) -> str: + return f'<Component Name {self.component_name}>' + + +class TestType(db.Model): + __tablename__ = "test_type" + + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + test_name: Mapped[str] = mapped_column(db.String(50), unique=True, nullable=False) + + #Relationships + tests = db.relationship('Test', backref='test_type', lazy='dynamic') + + def __repr__(self) -> str: + return f'<Test Name {self.test_name}>' + + +class AssemblyType(db.Model): + __tablename__ = "assembly_type" + + id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True) + assembly_name: Mapped[str] = mapped_column(db.String(50), unique=True, nullable=False) + + #Relationships + assemblies = db.relationship('Assembly', backref='assembly_type', lazy='dynamic') + + def __repr__(self) -> str: + return f'<Assembly Name {self.assembly_name}>' +#---------------------------------# +#consider a table for locations +#consider a table for ... diff --git a/application/routes.py b/application/routes.py index 94e9bf007954e75a06acc587b73cee749d6e5a9f..804c20a488bb78bd75ec7dd6c88be78d83dd031e 100644 --- a/application/routes.py +++ b/application/routes.py @@ -6,11 +6,11 @@ from flask import render_template, flash, redirect, url_for, request from werkzeug.urls import url_parse #class forms, uses flask-wtf -from application.forms import LoginForm, RegistrationForm +from application.forms import LoginForm, RegistrationForm, RecordTest #login functionalities form flask_login from flask_login import current_user, login_user, logout_user, login_required -from application.models import User +from application.models import * #-------------admin interface-----------------# #admin class @@ -37,16 +37,22 @@ def admin_required(cls): #new classes with same name or very different names? sign to maybe use flask-security @admin_required -class ModelView(ModelView): pass +class SecureModelView(ModelView): + def _password_hash_formatter(model): + # Format your string here e.g show first 20 characters + # can return any valid HTML e.g. a link to another view to show the detail or a popup window + return model.password_hash[:10] + '...' + + column_formatters = {'password_hash': _password_hash_formatter} @admin_required -class AdminIndexView(AdminIndexView): pass +class SecureAdminIndexView(AdminIndexView): pass @admin_required -class BaseView(BaseView): pass +class SecureBaseView(BaseView): pass #add custom views here that only inheret from MyModelView or MyBaseView, otherwise it is a security vulnerablity -class AdminRegisterUser(BaseView): #using basview because I already made a register user form +class AdminRegisterUser(SecureBaseView): #using basview because I already made a register user form @expose('/',methods=['GET','POST']) def register(self): form = RegistrationForm() @@ -62,16 +68,25 @@ class AdminRegisterUser(BaseView): #using basview because I already made a regis return redirect(url_for('register.register')) return self.render('admin/register.html', title='Register User', form=form) -class MainPage(BaseView): +class MainPage(SecureBaseView): @expose('/', methods=['GET','POST']) def return_main(self): return redirect(url_for('home')) #initialize extension admin -admin = Admin(app, name='Admin', index_view=AdminIndexView()) +admin = Admin(app, name='Admin', index_view=SecureAdminIndexView(), template_mode='bootstrap4') admin.add_view(AdminRegisterUser(name='Register User', endpoint='register')) -admin.add_view(ModelView(User, db.session)) + +#consider adding something to the views to see all column names, requires custom view for all +admin.add_view(SecureModelView(Assembly, db.session)) +admin.add_view(SecureModelView(Component, db.session)) +admin.add_view(SecureModelView(Module, db.session)) +admin.add_view(SecureModelView(ComponentType, db.session)) +admin.add_view(SecureModelView(ModuleType, db.session)) +admin.add_view(SecureModelView(TestType, db.session)) +admin.add_view(SecureModelView(AssemblyType, db.session)) + admin.add_view(MainPage(name='Return to Main Page')) #-----------user interface-----------------# @@ -99,8 +114,9 @@ def login(): user = User.query.filter_by(username=form.username.data).first() #check if username exists or their password is correct - if user is None or not user.check_password(form.password.data): #hashed password defined inside User class - flash('Invalid username or password') + if user is None or not user.check_password(form.password.data) or not user.is_active: #hashed password defined inside User class + if user is None or not user.check_password(form.password.data): flash('Invalid username or password') + elif not user.is_active: flash('Your account has been deactivated, contact an administrator') return redirect(url_for('login')) #if it is correct then for flask login_user, log them in @@ -128,4 +144,39 @@ def logout(): return redirect(url_for('home')) +@app.route('/test', methods=['GET','POST']) +@login_required +def record_test(): + form = RecordTest() + print('hi first') + if form.validate_on_submit(): + def check_empty(form_input): + #if empty string set to None so it is NULL in database + if form_input == '': + return None + else: + return form_input + + module_input = check_empty(form.module.data) + component_input = check_empty(form.component.data) + + test_type = TestType.query.filter_by(test_name=form.test_type.data).first() + + #TO DO: prob a better way to do this + if module_input is not None: + mod_obj = Module.query.filter_by(module_serial_number=module_input).first() + test = Test(test_type=test_type, user=current_user, location=check_empty(form.location.data), data=form.test_data.data, module=mod_obj) + if component_input is not None: + comp_obj = Component.query.filter_by(component_serial_number=component_input).first() + test = Test(test_type=test_type, user=current_user, location=check_empty(form.location.data), data=form.test_data.data, component=comp_obj) + + if module_input is not None and component_input is not None: + test = Test(test_type=test_type, user=current_user, location=check_empty(form.location.data), data=form.test_data.data, component=comp_obj, module=mod_obj) + + db.session.add(test) + db.session.commit() + + + return render_template('test.html',title='Record Test', form=form) + diff --git a/application/static/cms_logo.png b/application/static/cms_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..96ac7984793fdb197e6c15c8547e955c0d9f9144 Binary files /dev/null and b/application/static/cms_logo.png differ diff --git a/application/templates/admin/register.html b/application/templates/admin/register.html index c03923d293dcdecfd4188cbb769b25cbfadac5e9..cbaded7c1db385100585be39e7537456157abf16 100644 --- a/application/templates/admin/register.html +++ b/application/templates/admin/register.html @@ -1,45 +1,46 @@ {% extends 'admin/master.html' %} {% block body %} - <h1>Register Users</h1> + <h1>Register User</h1> <form action="", method="post"> {{ form.hidden_tag() }} - <p> - {{ form.username.label }}<br> - {{ form.username(size=32) }}<br> + <div class="form-group"> + {{ form.username.label }} + {{ form.username(size=32) }} {% for error in form.username.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} - </p> - <p> - {{ form.password.label }}<br> - {{ form.password(size=32) }}<br> + </div> + <div class="form-group"> + {{ form.password.label }} + {{ form.password(size=32) }} {% for error in form.password.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} - </p> - <p> - {{ form.password2.label }}<br> - {{ form.password2(size=32) }}<br> + </div> + <div class="form-group"> + {{ form.password2.label }} + {{ form.password2(size=32) }} {% for error in form.password2.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} - </p> - <p> - {{ form.email.label }}<br> - {{ form.email(size=64) }}<br> + </div> + <div class="form-group"> + {{ form.email.label }} + {{ form.email(size=32) }} {% for error in form.email.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} - </p> - <p> - {{ form.affiliation.label }}<br> - {{ form.affiliation(size=32) }}<br> + </div> + <div class="form-group"> + {{ form.affiliation.label }} + {{ form.affiliation(size=32) }} {% for error in form.affiliation.errors %} <span style="color: red;">[{{ error }}]</span> {% endfor %} - </p> - <p>{{ form.is_admin() }} {{ form.is_admin.label }}</p> - <p>{{ form.submit() }}</p> + </div> + <div class="form-group form-check">{{ form.is_admin() }} {{ form.is_admin.label }}</div> + <div style="margin-right: 10px;">{{ form.submit(class="w-10 btn btn-m btn-primary") }}</div> + </form> -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/application/templates/base.html b/application/templates/base.html index eef0c65c80dd7d0d02735db8b8533f36bd3aad10..8fbbf9986055950d7f359cfe1a677bbac9b21678 100644 --- a/application/templates/base.html +++ b/application/templates/base.html @@ -1,46 +1,66 @@ -<!DOCTYPE html> +<!doctype html> <html lang="en"> - <head> - <meta charset="UTF-8"> - {% if title %} - <title>{{ title }} - ETL Module Construction</title> - {% else %} - <title>ETL Module Construction</title> - {% endif %} - {{ moment.include_moment() }} - </head> - <body> - <div>ETL Module Construction: - <!-- url for the route defined in routes --> - <a href="{{ url_for('home') }}">Home</a> - {% if current_user.is_anonymous %} - <!-- above will only be True if user is not logged in --> - <a href="{{ url_for('login') }}">Login</a> - {% else %} - <a href="{{ url_for('logout') }}">Logout</a> - {% endif %} + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + {% if title %} + <title>{{ title }} - ETL Module Construction</title> + {% else %} + <title>ETL Module Construction</title> + {% endif %} + <!-- for date and times --> + {{ moment.include_moment() }} + + {{ bootstrap.load_css() }} - {% if current_user.is_admin %} - <a href="{{ url_for('admin.index') }}">Admin</a> - {% endif %} + </head> + <body> + <!-- Navigation Bar --> + <nav class="navbar navbar-expand-sm -bs-primary-bg-subtle" style="margin-bottom: 20px; background-color: #92e569;"> + <div class="container-fluid"> + <a class="navbar-brand" href="{{ url_for('home') }}">ETL Module Construction</a> + {% if not current_user.is_anonymous %} + <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> + <span class="navbar-toggler-icon"></span> + </button> + {% endif %} + <div class="collapse navbar-collapse" id="navbarSupportedContent"> + <ul class="navbar-nav me-auto mb-2 mb-lg-0"> + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('record_test')}}">Record Test</a> + </li> + {% if not current_user.is_anonymous %} + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('logout')}}">Logout</a> + </li> + {% endif %} + {% if current_user.is_admin %} + <li class="nav-item"> + <a class="nav-link" href="{{ url_for('admin.index') }}">Admin</a> + </li> + {% endif %} + </ul> </div> - <hr> - <!-- derived templates insert themselves here --> - <!-- base.html takes care of general page structure! --> - {% with messages = get_flashed_messages() %} - <!-- looks for flashed messages --> - {% if messages %} + </div> + </nav> + + <!-- turn this into an alert --> + {% with messages = get_flashed_messages() %} + <!-- looks for flashed messages --> + {% if messages %} <ul> {% for message in messages %} - <li>{{ message }}</li> + <div class="alert alert-warning" role="alert" style="margin-right: 20px;">{{ message }}</div> {% endfor %} </ul> - {% endif %} - {% endwith %} - - {% block content %}{% endblock %} - + {% endif %} + {% endwith %} + <!-- derived templates insert themselves here --> + <!-- base.html takes care of general page structure! --> + {% block content %}{% endblock %} + + {{ bootstrap.load_js() }} - </body> + </body> </html> \ No newline at end of file diff --git a/application/templates/home.html b/application/templates/home.html index 48bf3b7ec5dd5c6cd89cf3f69f3799f221440cbf..c8bd78509a4c31fc8092d07b020eeae5a7b4bbbf 100644 --- a/application/templates/home.html +++ b/application/templates/home.html @@ -1,14 +1,16 @@ {% extends "base.html" %} {% block content %} +<div style="margin-left: 20px;"> <h1> Welcome, {{ current_user.username }}!</h1> - <h2> Here are a list of current users:</h2> - {% for user in users %} - <div> - <li>{{ user.username }} - {% if user.created_at %} - <p>Created at: {{ moment(user.created_at).format('LLL') }}</p> - {% endif %} - </li> + <h4> Here are a list of current users:</h2> + <div style="margin-left: 20px;"> + {% for user in users %} + <li>{{ user.username }} + {% if user.created_at %} + <p>Created at: {{ moment(user.created_at).format('LLL') }}</p> + {% endif %} + </li> + {% endfor %} </div> - {% endfor %} +</div> {% endblock %} \ No newline at end of file diff --git a/application/templates/login.html b/application/templates/login.html index 2e57a06abfb9c2a56f77f650dd8f22de9411a8b4..2e8bae22c2d892899318f99afaf5ad00b35e385c 100644 --- a/application/templates/login.html +++ b/application/templates/login.html @@ -1,30 +1,32 @@ {% extends "base.html" %} {% block content %} - - <h1> Sign In </h1> - <form action="" method="post" novalidate> - <!-- protects against CSRF attacks, just need this and SECRET_KEY defined in Flask configuration --> - {{ form.hidden_tag() }} - <p> - <!-- form comes from LoginForm class --> - <!-- they know how to render the html --> - {{ form.username.label }} <br> - {{ form.username(size=32) }} - {% for error in form.username.errors %} - <span style="color: red;">[{{ error }}]</span> - {% endfor %} - </p> - <p> - {{ form.password.label }} <br> - {{ form.password(size=32) }} - {% for error in form.password.errors %} - <span style="color: red;">[{{ error }}]</span> - {% endfor %} - </p> - <p>{{ form.remember_me() }} {{ form.remember_me.label }}</p> - <p>{{ form.submit() }}</p> - - </form> - +<div class="text-center" data-new-gr-c-s-check-loaded="14.1125.0" data-gr-ext-installed=""> + <main class="form-signin"> + <div style="box-sizing: border-box; border: 1px solid rgb(236, 234, 234); width: 300px; margin: auto; margin-top: 40px; background-color: #85d2fbbc; box-shadow: 0 0 2px rgb(232, 238, 255); border-radius: 8px;"> + <h1 class="h3 mt-3 fw-normal" style="font-size: 40px; width: 250px; margin: auto; color: black;"> Sign In </h1> + <form action="" method="post" data-bitwarden-watching="1" novalidate style="width: 250px; margin: auto; padding-bottom: 10px;" > + <img class="mb-4" src="{{ url_for('static', filename='cms_logo.png') }}"> + <!-- protects against CSRF attacks, just need this and SECRET_KEY defined in Flask configuration --> + {{ form.hidden_tag() }} + <p> + <!-- form comes from LoginForm class --> + <!-- they know how to render the html --> + {{ form.username(size=14) }} + {% for error in form.username.errors %} + <div style="color: red;">[{{ error }}]</div> + {% endfor %} + </p> + <p> + {{ form.password(size=14) }} + {% for error in form.password.errors %} + <div style="color: red;">[{{ error }}]</div> + {% endfor %} + </p> + <span style="margin-right: 10px;">{{ form.submit(class="w-10 btn btn-m btn-primary") }}</span> + <span>{{ form.remember_me }} {{ form.remember_me.label }}</span> + </form> + </div> + </main> +</div> {% endblock %} \ No newline at end of file diff --git a/application/templates/test.html b/application/templates/test.html new file mode 100644 index 0000000000000000000000000000000000000000..824d371d49466dc4a702f16429c87b5ad3e13989 --- /dev/null +++ b/application/templates/test.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} + +{% block content %} +<h1>Record Test</h1> +<form action="", method="post"> + {{ form.hidden_tag() }} + <div class="form-group"> + {{ form.module.label }} + {{ form.module(size=32) }} + {% for error in form.module.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </div> + <div class="form-group"> + {{ form.component.label }} + {{ form.component(size=32) }} + {% for error in form.component.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </div> + <div class="form-group"> + {{ form.test_type.label }} + {{ form.test_type(size=32) }} + {% for error in form.test_type.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </div> + <div class="form-group"> + {{ form.location.label }} + {{ form.location(size=32) }} + {% for error in form.location.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </div> + <div class="form-group"> + {{ form.test_data.label }} + {{ form.test_data(size=32) }} + {% for error in form.test_data.errors %} + <span style="color: red;">[{{ error }}]</span> + {% endfor %} + </div> + <div style="margin-right: 10px;">{{ form.submit(class="w-10 btn btn-m btn-primary") }}</div> + +</form> + +{% endblock %} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 5d3c21f0feddd5afe6bdc296f5af62fe29c078c3..7a02bad11776af920faa6bb453435e7c0cb2d79d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,4 +7,5 @@ flask-login==0.6.2 flask-admin==1.6.1 email-validator==2.0.0.post2 flask-moment==1.0.5 -psycopg2==2.9.7 \ No newline at end of file +psycopg2==2.9.7 +bootstrap-flask==2.3.0 \ No newline at end of file