From 1d91d783ed9b82c54bffb7d0cd11ea36829a084c Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Thu, 14 Sep 2023 16:28:08 -0400
Subject: [PATCH 1/8] renamed secure views for the inherited admin classes for
 better readability

---
 application/routes.py | 14 +++++++-------
 1 file changed, 7 insertions(+), 7 deletions(-)

diff --git a/application/routes.py b/application/routes.py
index 94e9bf0..47928ae 100644
--- a/application/routes.py
+++ b/application/routes.py
@@ -37,16 +37,16 @@ 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): pass
 
 @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 +62,16 @@ 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())
 
 admin.add_view(AdminRegisterUser(name='Register User', endpoint='register'))
-admin.add_view(ModelView(User, db.session))
+admin.add_view(SecureModelView(User, db.session))
 admin.add_view(MainPage(name='Return to Main Page'))
 
 #-----------user interface-----------------#
-- 
GitLab


From 3bd36bb200e75947f0c4a0f56e81eb7799531238 Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Thu, 14 Sep 2023 20:35:44 -0400
Subject: [PATCH 2/8] upgrading styling for sign in page, the current
 implementation is very hacky should add seperate css styles but itll do for
 now...

---
 application/__init__.py          |   5 ++
 application/forms.py             |   4 +-
 application/static/cms_logo.png  | Bin 0 -> 8695 bytes
 application/templates/base.html  |  86 ++++++++++++++++++-------------
 application/templates/login.html |  80 +++++++++++++++++++---------
 requirements.txt                 |   3 +-
 6 files changed, 114 insertions(+), 64 deletions(-)
 create mode 100644 application/static/cms_logo.png

diff --git a/application/__init__.py b/application/__init__.py
index 3be7f7d..817bdcd 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 41fff02..2076eee 100644
--- a/application/forms.py
+++ b/application/forms.py
@@ -6,8 +6,8 @@ from application.models import User
 
 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')
 
diff --git a/application/static/cms_logo.png b/application/static/cms_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..96ac7984793fdb197e6c15c8547e955c0d9f9144
GIT binary patch
literal 8695
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
V#JrsndOQFC002ovPDHLkV1lSN>Ye}q

literal 0
HcmV?d00001

diff --git a/application/templates/base.html b/application/templates/base.html
index eef0c65..4501416 100644
--- a/application/templates/base.html
+++ b/application/templates/base.html
@@ -1,46 +1,60 @@
-<!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() }}
 
-          {% if current_user.is_admin %}
-          <a href="{{ url_for('admin.index') }}">Admin</a>
-          {% endif %}
+    {{ bootstrap.load_css() }}
 
+  </head>
+  <body>
+    <!-- Navigation Bar -->
+    <nav class="navbar navbar-expand-lg bg-body-tertiary">
+      <div class="container-fluid">
+        <a class="navbar-brand" href="{{ url_for('home') }}">ETL Module Construction</a>
+        <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>
+        <div class="collapse navbar-collapse" id="navbarSupportedContent">
+          <ul class="navbar-nav me-auto mb-2 mb-lg-0">
+            {% 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>
+            <li>{{ message }}</li>
           {% 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/login.html b/application/templates/login.html
index 2e57a06..0a24316 100644
--- a/application/templates/login.html
+++ b/application/templates/login.html
@@ -1,30 +1,60 @@
 {% extends "base.html" %}
 
 {% block content %}
+<!-- 
+<body class="text-center" data-new-gr-c-s-check-loaded="14.1125.0" data-gr-ext-installed="">
+    
+    <main class="form-signin">
+      <form data-bitwarden-watching="1">
+        <img class="mb-4" src="/static/cms_logo.png" alt="" width="72" height="57">
+        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
+    
+        <div class="form-floating">
+          <input type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
+          <label for="floatingInput">Username</label>
+        </div>
+        <div class="form-floating">
+          <input type="password" class="form-control" id="floatingPassword" placeholder="Password">
+          <label for="floatingPassword">Password</label>
+        </div>
+    
+        <div class="checkbox mb-3">
+          <label>
+            <input type="checkbox" value="remember-me"> Remember me
+          </label>
+        </div>
+        <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
+        <p class="mt-5 mb-3 text-muted">© 2017–2021</p>
+      </form>
+    </main>
+</body> -->
 
-    <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: rgb(232, 238, 255); 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"> 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(class="checkbox mb-3") }} {{ form.remember_me.label }}</span>
+        </form>
+        </div>
+    </main>
+</div>
 {% endblock %}
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 5d3c21f..7a02bad 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
-- 
GitLab


From 2c0c18f89bd2ba4386f2b90e83603a0832b570a5 Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Fri, 15 Sep 2023 12:19:01 -0400
Subject: [PATCH 3/8] even more styling, I now know that styling can turn into
 a massive time sink

---
 application/forms.py                      | 12 +++---
 application/routes.py                     | 11 ++++-
 application/templates/admin/register.html | 49 ++++++++++++-----------
 application/templates/base.html           |  7 +++-
 application/templates/home.html           | 20 ++++-----
 application/templates/login.html          | 34 ++--------------
 6 files changed, 59 insertions(+), 74 deletions(-)

diff --git a/application/forms.py b/application/forms.py
index 2076eee..94effc9 100644
--- a/application/forms.py
+++ b/application/forms.py
@@ -14,12 +14,12 @@ class LoginForm(FlaskForm):
 
 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):
diff --git a/application/routes.py b/application/routes.py
index 47928ae..daebd34 100644
--- a/application/routes.py
+++ b/application/routes.py
@@ -37,7 +37,14 @@ def admin_required(cls):
 
 #new classes with same name or very different names? sign to maybe use flask-security
 @admin_required
-class SecureModelView(ModelView): pass
+class SecureModelView(ModelView): 
+    #column_exclude_list = ('password_hash')
+     def _password_hash_formatter(view, context, model, name):
+        # 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 SecureAdminIndexView(AdminIndexView): pass
@@ -68,7 +75,7 @@ class MainPage(SecureBaseView):
         return redirect(url_for('home'))
 
 #initialize extension admin
-admin = Admin(app, name='Admin', index_view=SecureAdminIndexView())
+admin = Admin(app, name='Admin', index_view=SecureAdminIndexView(), template_mode='bootstrap4')
 
 admin.add_view(AdminRegisterUser(name='Register User', endpoint='register'))
 admin.add_view(SecureModelView(User, db.session))
diff --git a/application/templates/admin/register.html b/application/templates/admin/register.html
index c03923d..cbaded7 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 4501416..23e6740 100644
--- a/application/templates/base.html
+++ b/application/templates/base.html
@@ -16,12 +16,15 @@
   </head>
   <body>
     <!-- Navigation Bar -->
-    <nav class="navbar navbar-expand-lg bg-body-tertiary">
+
+    <nav class="navbar navbar-expand-sm -bs-primary-bg-subtle" style="margin-bottom: 10px; 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">
             {% if not current_user.is_anonymous %}
@@ -45,7 +48,7 @@
       {% 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 %}
diff --git a/application/templates/home.html b/application/templates/home.html
index 48bf3b7..c8bd785 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 0a24316..2e8bae2 100644
--- a/application/templates/login.html
+++ b/application/templates/login.html
@@ -1,38 +1,10 @@
 {% extends "base.html" %}
 
 {% block content %}
-<!-- 
-<body class="text-center" data-new-gr-c-s-check-loaded="14.1125.0" data-gr-ext-installed="">
-    
-    <main class="form-signin">
-      <form data-bitwarden-watching="1">
-        <img class="mb-4" src="/static/cms_logo.png" alt="" width="72" height="57">
-        <h1 class="h3 mb-3 fw-normal">Please sign in</h1>
-    
-        <div class="form-floating">
-          <input type="email" class="form-control" id="floatingInput" placeholder="name@example.com">
-          <label for="floatingInput">Username</label>
-        </div>
-        <div class="form-floating">
-          <input type="password" class="form-control" id="floatingPassword" placeholder="Password">
-          <label for="floatingPassword">Password</label>
-        </div>
-    
-        <div class="checkbox mb-3">
-          <label>
-            <input type="checkbox" value="remember-me"> Remember me
-          </label>
-        </div>
-        <button class="w-100 btn btn-lg btn-primary" type="submit">Sign in</button>
-        <p class="mt-5 mb-3 text-muted">© 2017–2021</p>
-      </form>
-    </main>
-</body> -->
-
 <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: rgb(232, 238, 255); 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"> Sign In </h1>
+        <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 -->
@@ -52,7 +24,7 @@
                 {% endfor %}           
             </p>
             <span style="margin-right: 10px;">{{ form.submit(class="w-10 btn btn-m btn-primary") }}</span>
-            <span>{{ form.remember_me(class="checkbox mb-3") }} {{ form.remember_me.label }}</span>
+            <span>{{ form.remember_me }} {{ form.remember_me.label }}</span>
         </form>
         </div>
     </main>
-- 
GitLab


From 7e3781ec84d1fe055fb1dcaae11c191515a90613 Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Fri, 15 Sep 2023 12:43:55 -0400
Subject: [PATCH 4/8] small style additions

---
 application/templates/base.html | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/application/templates/base.html b/application/templates/base.html
index 23e6740..727e738 100644
--- a/application/templates/base.html
+++ b/application/templates/base.html
@@ -17,7 +17,7 @@
   <body>
     <!-- Navigation Bar -->
 
-    <nav class="navbar navbar-expand-sm -bs-primary-bg-subtle" style="margin-bottom: 10px; background-color: #92e569;">
+    <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 %}
-- 
GitLab


From a26a1663ad6d984709a956414a5eeb542d995881 Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Fri, 15 Sep 2023 16:18:17 -0400
Subject: [PATCH 5/8] added base tables for our database schema, no
 relationships at the moment

---
 .gitignore            |   1 +
 application/models.py | 124 ++++++++++++++++++++++++++++++++++++++----
 application/routes.py |   5 +-
 3 files changed, 117 insertions(+), 13 deletions(-)

diff --git a/.gitignore b/.gitignore
index f824511..b335a5e 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/models.py b/application/models.py
index 5589e03..226b9a2 100644
--- a/application/models.py
+++ b/application/models.py
@@ -40,11 +40,11 @@ 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)
 
     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,20 +56,122 @@ class User(UserMixin, db.Model):
         return f'<User {self.username}>'
     
 
-# class module(db.Model):
-#     pass
+class assembly(db.Model):
+    __tablename__ = "assembly"
 
-# class assembly(db.Model):
-#     pass
+    id: Mapped[int] = mapped_column(db.Integer, autoincrement=True, primary_key=True)    
+    user_id: Mapped[int] = mapped_column(db.Integer, 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, nullable=True)
+    component_id: Mapped[int] = mapped_column(db.Integer, nullable=True)
+    assembly_type_id: Mapped[int] = mapped_column(db.Integer, nullable=False)
 
-# class assembly_step(db.Model):
-#     pass
+    location: Mapped[str] = mapped_column(db.String(50), unique=False, nullable=True)
+    completed_at: Mapped[datetime] = mapped_column(default=now_utc, nullable=False)
 
-# class component(db.Model):
-#     pass
+    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, 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, nullable=True)
+    component_id: Mapped[int] = mapped_column(db.Integer, nullable=True)
+    test_type_id: Mapped[int] = mapped_column(db.Integer, 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, 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)
+
+    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, 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, 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)
+
+    def __repr__(self) -> str:
+        return f'<Component {self.component_serial_number}>'
+#-------------------------------------------------#
+
+
+#----------Lookup Tables-----------#
+class module_type(db.Model):
+    __tablename__ = "module_type"
+
+    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)
+    
+    def __repr__(self) -> str:
+        return f'<Module Name {self.module_name}>'
+
+
+class component_type(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)
+    
+    def __repr__(self) -> str:
+        return f'<Component Name {self.component_name}>'
+
+
+class test_type(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)
+
+    def __repr__(self) -> str:
+        return f'<Test Name {self.test_name}>'
+
+
+class assembly_type(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)
+
+    def __repr__(self) -> str:
+        return f'<Assembly Name {self.assembly_name}>'
+#---------------------------------#
 
-# class component_type(db.Model):
-#     pass
 
 
 
diff --git a/application/routes.py b/application/routes.py
index daebd34..957867c 100644
--- a/application/routes.py
+++ b/application/routes.py
@@ -106,8 +106,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
-- 
GitLab


From 9cd2a2178c87f4db82ac786df3c860e6c4f7c73e Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Mon, 18 Sep 2023 18:45:35 -0400
Subject: [PATCH 6/8] added relationships to database, getting ready to fill
 with data and make forms!

---
 application/models.py | 70 +++++++++++++++++++++++++++++--------------
 application/routes.py | 20 +++++++++----
 2 files changed, 62 insertions(+), 28 deletions(-)

diff --git a/application/models.py b/application/models.py
index 226b9a2..b351868 100644
--- a/application/models.py
+++ b/application/models.py
@@ -43,6 +43,13 @@ class User(UserMixin, db.Model):
     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
         self.password_hash = generate_password_hash(password)
@@ -56,123 +63,142 @@ class User(UserMixin, db.Model):
         return f'<User {self.username}>'
     
 
-class assembly(db.Model):
+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, nullable=False)
+    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, nullable=True)
-    component_id: Mapped[int] = mapped_column(db.Integer, nullable=True)
-    assembly_type_id: Mapped[int] = mapped_column(db.Integer, nullable=False)
+    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):
+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, nullable=False)
+    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, nullable=True)
-    component_id: Mapped[int] = mapped_column(db.Integer, nullable=True)
-    test_type_id: Mapped[int] = mapped_column(db.Integer, nullable=False)
+    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):
+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, nullable=False)
+    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):
+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, nullable=True) #for production or throughput demo nullable should be False, every component should have an associated module pcb
+    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, nullable=False)
+    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')
+
     def __repr__(self) -> str:
         return f'<Component {self.component_serial_number}>'
 #-------------------------------------------------#
 
 
 #----------Lookup Tables-----------#
-class module_type(db.Model):
+class ModuleType(db.Model):
     __tablename__ = "module_type"
 
     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 component_type(db.Model):
+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 test_type(db.Model):
+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 assembly_type(db.Model):
+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 957867c..2918761 100644
--- a/application/routes.py
+++ b/application/routes.py
@@ -10,7 +10,7 @@ from application.forms import LoginForm, RegistrationForm
 
 #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,14 +37,13 @@ def admin_required(cls):
 
 #new classes with same name or very different names? sign to maybe use flask-security
 @admin_required
-class SecureModelView(ModelView): 
-    #column_exclude_list = ('password_hash')
-     def _password_hash_formatter(view, context, model, name):
+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}
+    column_formatters = {'password_hash': _password_hash_formatter}
 
 @admin_required
 class SecureAdminIndexView(AdminIndexView): pass
@@ -78,7 +77,16 @@ class MainPage(SecureBaseView):
 admin = Admin(app, name='Admin', index_view=SecureAdminIndexView(), template_mode='bootstrap4')
 
 admin.add_view(AdminRegisterUser(name='Register User', endpoint='register'))
-admin.add_view(SecureModelView(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-----------------#
-- 
GitLab


From 6f04c9b52fde3495cef800d48e2db80ed185c0b1 Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Tue, 19 Sep 2023 12:54:10 -0400
Subject: [PATCH 7/8] Basic form for recording tests

---
 application/forms.py            | 56 +++++++++++++++++++++++++++++++--
 application/routes.py           | 53 ++++++++++++++++++++++++++++++-
 application/templates/base.html |  3 ++
 application/templates/test.html | 46 +++++++++++++++++++++++++++
 4 files changed, 155 insertions(+), 3 deletions(-)
 create mode 100644 application/templates/test.html

diff --git a/application/forms.py b/application/forms.py
index 94effc9..c3b67ae 100644
--- a/application/forms.py
+++ b/application/forms.py
@@ -1,8 +1,8 @@
 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):
 
@@ -34,3 +34,55 @@ 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')
+
+    # def validate_mod_comp_not_empty(self, module, component):
+    #     if module.data == '' and component.data == '':
+    #         raise ValidationError("Module and Component Serial numbers both can't be empty")
+    
+    #TO DO:
+    #query is done twice, here and in routes, is this bad?
+    #probably bad to have module and component string fields right? One or the other?
+    #conisder stripping white space
+    #also add a Flask-WTF FileField that takes precident over data
+    #ideally a searchable table and you select the 
+    #test, module, or component because it could get long! Maybe locations too
+    #think about smart way to do this registration of test, module and component with the lookup tables
+    #idea being so you dont have to also manually fill something seperate out for the lookup tables
+
diff --git a/application/routes.py b/application/routes.py
index 2918761..505af4a 100644
--- a/application/routes.py
+++ b/application/routes.py
@@ -6,7 +6,7 @@ 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
@@ -144,4 +144,55 @@ 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():
+        print(f'Module data: [{form.module.data}], {type(form.module.data)}')
+        print(f'Component data: [{form.component.data}], {type(form.component.data)}')
+        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)
+
+        print(f'Module data: [{module_input}], {type(module_input)}')
+        print(f'Component data: [{component_input}], {type(component_input)}')
+
+        test_type = TestType.query.filter_by(test_name=form.test_type.data).first()
+
+        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()
+        redirect(url_for('record_test'))
+
+    return render_template('test.html',title='Record Test', form=form)
+
     
diff --git a/application/templates/base.html b/application/templates/base.html
index 727e738..8fbbf99 100644
--- a/application/templates/base.html
+++ b/application/templates/base.html
@@ -27,6 +27,9 @@
         {% 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>
diff --git a/application/templates/test.html b/application/templates/test.html
new file mode 100644
index 0000000..824d371
--- /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
-- 
GitLab


From 9dab91309bf8e684da43f6c7764a1b50012c7a8d Mon Sep 17 00:00:00 2001
From: Hayden Swanson <hayden_swanson22@yahoo.com>
Date: Tue, 19 Sep 2023 17:38:06 -0400
Subject: [PATCH 8/8] further improvements on the register test form

---
 application/forms.py  | 15 +--------------
 application/routes.py | 28 ++++++----------------------
 2 files changed, 7 insertions(+), 36 deletions(-)

diff --git a/application/forms.py b/application/forms.py
index c3b67ae..00228b0 100644
--- a/application/forms.py
+++ b/application/forms.py
@@ -56,7 +56,6 @@ class RecordTest(FlaskForm):
             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:
@@ -72,17 +71,5 @@ class RecordTest(FlaskForm):
         if test_type_query is None:
             raise ValidationError('Test type does not exist, please register it')
 
-    # def validate_mod_comp_not_empty(self, module, component):
-    #     if module.data == '' and component.data == '':
-    #         raise ValidationError("Module and Component Serial numbers both can't be empty")
-    
-    #TO DO:
-    #query is done twice, here and in routes, is this bad?
-    #probably bad to have module and component string fields right? One or the other?
-    #conisder stripping white space
-    #also add a Flask-WTF FileField that takes precident over data
-    #ideally a searchable table and you select the 
-    #test, module, or component because it could get long! Maybe locations too
-    #think about smart way to do this registration of test, module and component with the lookup tables
-    #idea being so you dont have to also manually fill something seperate out for the lookup tables
+ 
 
diff --git a/application/routes.py b/application/routes.py
index 505af4a..804c20a 100644
--- a/application/routes.py
+++ b/application/routes.py
@@ -150,8 +150,6 @@ def record_test():
     form = RecordTest()
     print('hi first')
     if form.validate_on_submit():
-        print(f'Module data: [{form.module.data}], {type(form.module.data)}')
-        print(f'Component data: [{form.component.data}], {type(form.component.data)}')
         def check_empty(form_input):
             #if empty string set to None so it is NULL in database
             if form_input == '':
@@ -162,36 +160,22 @@ def record_test():
         module_input = check_empty(form.module.data)
         component_input = check_empty(form.component.data)
 
-        print(f'Module data: [{module_input}], {type(module_input)}')
-        print(f'Component data: [{component_input}], {type(component_input)}')
-
         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)
+            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)
+            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)
+            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()
-        redirect(url_for('record_test'))
+        
 
     return render_template('test.html',title='Record Test', form=form)
 
-- 
GitLab