Skip to content

Commit 4a9ae01

Browse files
first commit
0 parents  commit 4a9ae01

32 files changed

+2044
-0
lines changed

.gitignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/.idea/
2+
/app/prod.db
3+
/app/static/temp/
4+
/app/static/images/
5+
/start.sh

app/__init__.py

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from config import Config
2+
from flask import Flask
3+
from flask_sqlalchemy import SQLAlchemy
4+
from flask_login import LoginManager
5+
6+
7+
db = SQLAlchemy()
8+
login_manager = LoginManager()
9+
login_manager.login_view = 'login'
10+
11+
12+
def create_app(config_class=Config):
13+
app = Flask(__name__)
14+
app.config.from_object(config_class)
15+
16+
db.init_app(app)
17+
login_manager.init_app(app)
18+
19+
from app.default import bp as default_bp
20+
from app.login import bp as login_bp
21+
from app.part_creation import bp as part_creation_bp
22+
from app.prod_recording import bp as prod_recording_bp
23+
24+
app.register_blueprint(default_bp)
25+
app.register_blueprint(login_bp)
26+
app.register_blueprint(part_creation_bp)
27+
app.register_blueprint(prod_recording_bp)
28+
29+
return app

app/default/__init__.py

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from flask import Blueprint
2+
3+
bp = Blueprint('default', __name__, template_folder="templates")
4+
5+
from app.default import routes

app/default/models.py

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
from app import db, login_manager
2+
from flask_login import UserMixin
3+
from werkzeug.security import generate_password_hash, check_password_hash
4+
5+
"""Association table, storing which users are working on which groups of products"""
6+
association_table = db.Table('users_batches',
7+
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
8+
db.Column('batch_id', db.Integer, db.ForeignKey('batch.id')))
9+
10+
11+
class User(db.Model, UserMixin):
12+
id = db.Column(db.Integer, primary_key=True)
13+
username = db.Column(db.String(64), index=True, unique=True)
14+
email = db.Column(db.String(120), index=True, unique=True)
15+
password_hash = db.Column(db.String(128))
16+
admin = db.Column(db.Boolean)
17+
18+
recorded_data = db.relation('ProdData')
19+
20+
def set_password(self, password):
21+
self.password_hash = generate_password_hash(password)
22+
23+
def check_password(self, password):
24+
return check_password_hash(self.password_hash, password)
25+
26+
def __repr__(self):
27+
return '<User {}>'.format(self.username)
28+
29+
30+
class PartType(db.Model):
31+
"""Used to store different types of products"""
32+
id = db.Column(db.Integer, primary_key=True)
33+
part_name = db.Column(db.String)
34+
35+
steps = db.relationship('Step')
36+
37+
def clone_part_type(self):
38+
# Get next ID
39+
new_part_name = self.part_name + "(copy)"
40+
new_part_type = PartType(part_name=new_part_name)
41+
db.session.add(new_part_type)
42+
db.session.commit()
43+
for step in self.steps:
44+
step.clone_step(part_type_id=new_part_type.id)
45+
return new_part_type
46+
47+
48+
class Batch(db.Model):
49+
"""Used to store batches of parts"""
50+
# Set sqlite autoincrement to true for sqlite to stop it reusing IDs after deletion
51+
__table_args__ = {'sqlite_autoincrement': True}
52+
id = db.Column(db.Integer, primary_key=True)
53+
batch_number = db.Column(db.String, nullable=False)
54+
part_type_id = db.Column(db.Integer, db.ForeignKey('part_type.id'), nullable=False)
55+
completed = db.Column(db.Boolean, default=False)
56+
created_timestamp = db.Column(db.Integer)
57+
last_edit_timestamp = db.Column(db.Integer)
58+
completed_timestamp = db.Column(db.Integer)
59+
60+
users = db.relationship('User', secondary=association_table, lazy='dynamic', backref=db.backref('batches'))
61+
parts = db.relationship('Part')
62+
csvs = db.relationship('Csv')
63+
part_type = db.relationship('PartType', backref=db.backref('batches'))
64+
65+
66+
class ProdData(db.Model):
67+
""" Holds the data recorded for each part/step during production"""
68+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
69+
part_id = db.Column(db.Integer, db.ForeignKey('part.id'))
70+
field_id = db.Column(db.Integer, db.ForeignKey('field.id'))
71+
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
72+
73+
field_data = db.Column(db.String)
74+
75+
76+
class Part(db.Model):
77+
"""Used to store basic information about a single product"""
78+
id = db.Column(db.Integer, primary_key=True)
79+
relative_id = db.Column(db.Integer) # The id to quickly identify the part within a batch, usually starting at 1
80+
batch_id = db.Column(db.Integer, db.ForeignKey('batch.id'))
81+
last_edit_timestamp = db.Column(db.Integer)
82+
83+
data = db.relationship('ProdData')
84+
85+
86+
class Field(db.Model):
87+
""" Specifies which fields of data should appear for each step"""
88+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
89+
step_id = db.Column(db.Integer, db.ForeignKey('step.id'))
90+
field_name = db.Column(db.String)
91+
batch_wide = db.Column(db.Boolean) # True if the data field counts for all the parts in the batch
92+
93+
data = db.relationship('ProdData')
94+
95+
def clone_field(self, new_step_id):
96+
new_field = Field(step_id=new_step_id,
97+
field_name=self.field_name,
98+
batch_wide=self.batch_wide)
99+
db.session.add(new_field)
100+
db.session.commit()
101+
102+
103+
class Csv(db.Model):
104+
"""Data saved in csv format"""
105+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
106+
batch_id = db.Column(db.Integer, db.ForeignKey('batch.id'))
107+
step_id = db.Column(db.Integer, db.ForeignKey('step.id'))
108+
body = db.Column(db.String)
109+
uploaded_timestamp = db.Column(db.Integer)
110+
111+
112+
class Step(db.Model):
113+
"""Holds the information for each step for each type of part"""
114+
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
115+
part_type_id = db.Column(db.Integer, db.ForeignKey('part_type.id'))
116+
step_number = db.Column(db.Integer)
117+
step_title = db.Column(db.String)
118+
step_instructions = db.Column(db.String)
119+
step_image = db.Column(db.String)
120+
csv_upload = db.Column(db.Boolean)
121+
csv_id = db.Column(db.Integer, db.ForeignKey('csv.id'))
122+
123+
fields = db.relationship('Field')
124+
125+
def clone_step(self, part_type_id):
126+
new_step = Step(part_type_id=part_type_id,
127+
step_number=self.step_number,
128+
step_title=self.step_title,
129+
step_instructions=self.step_instructions,
130+
step_image=self.step_image)
131+
db.session.add(new_step)
132+
db.session.commit()
133+
for field in self.fields:
134+
field.clone_field(new_step.id)
135+
136+
137+
@login_manager.user_loader
138+
def load_user(id):
139+
return User.query.get(int(id))
140+
141+
142+
def copy_row(model, row, ignored_columns=[]):
143+
copy = model()
144+
145+
for col in row.__table__.columns:
146+
if col.name not in ignored_columns:
147+
try:
148+
copy.__setattr__(col.name, getattr(row, col.name))
149+
except Exception as e:
150+
print(e)
151+
continue
152+
return copy

app/default/routes.py

+153
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
from app import db
2+
from app.prod_recording.export import recreate_csv_log, create_csv_from_data
3+
from app.default import bp
4+
from app.default.models import User, Batch, PartType
5+
from datetime import datetime
6+
from time import time
7+
from flask import render_template, request, redirect, url_for, abort, send_file
8+
from flask_login import current_user, login_required
9+
10+
11+
12+
@bp.route('/')
13+
def default():
14+
db.create_all()
15+
if len(User.query.all()) == 0:
16+
base_admin = User(username="admin", admin=True)
17+
base_admin.set_password("password")
18+
db.session.add(base_admin)
19+
db.session.commit()
20+
return redirect(url_for('login.login'))
21+
22+
23+
@bp.route('/index')
24+
@login_required
25+
def index():
26+
""" The default page """
27+
if current_user.is_authenticated:
28+
user = {'username': current_user.username, 'id': current_user.id}
29+
else:
30+
user = {'username': "nobody"}
31+
return render_template('index.html', title='Home', user=user)
32+
33+
34+
35+
@bp.route('/home', methods=['GET', 'POST'])
36+
@login_required
37+
def home():
38+
""" The default page for a logged-in user
39+
40+
This will show the user the batches they are currently working on, as well as the option to start a new batch
41+
"""
42+
43+
if request.method == "POST":
44+
try:
45+
action = request.form["action"]
46+
except Exception as e:
47+
print(e)
48+
abort(400)
49+
return
50+
51+
if action == "completeBatch":
52+
# Mark the batch as completed
53+
completed_batch = Batch.query.get_or_404(request.form["batchId"])
54+
completed_batch.completed = True
55+
completed_batch.completed_timestamp = time()
56+
db.session.commit()
57+
58+
batches = current_user.batches
59+
batches_in_progress = []
60+
for b in batches:
61+
if not b.completed:
62+
batches_in_progress.append(b)
63+
admin = current_user.admin
64+
nav_bar_title = "Home - " + current_user.username
65+
return render_template('home.html',
66+
nav_bar_title=nav_bar_title,
67+
batches=batches_in_progress,
68+
admin=admin)
69+
70+
71+
@bp.route('/adminhome', methods=['GET', 'POST'])
72+
@login_required
73+
def admin_home():
74+
"""The home page for a logged-in admin"""
75+
if not current_user.admin:
76+
return redirect(url_for('default.home'))
77+
78+
# By default, get batches from the current month
79+
# This is overwritten if the user provides dates in a post request
80+
now = datetime.now()
81+
requested_month = now.month
82+
requested_year = now.year
83+
84+
if request.method == "POST":
85+
try:
86+
action = request.form["action"]
87+
except Exception as e:
88+
print(e)
89+
abort(400)
90+
return
91+
92+
if action == "alterDates":
93+
# Changes the month for which batches to show
94+
requested_month = int(request.form["month"])
95+
requested_year = int(request.form["year"])
96+
97+
if action == "newPartType":
98+
# Create a new part type
99+
part_type = PartType()
100+
db.session.add(part_type)
101+
db.session.commit()
102+
return redirect(url_for("part_creation.create_part_type", part_type_id=part_type.id))
103+
if action == "copyPartType":
104+
# Create a new part type using an older part type as template
105+
part_type = PartType.query.get_or_404(request.form["partTypeId"])
106+
part_type_copy = part_type.clone_part_type()
107+
return redirect(url_for("part_creation.create_part_type", part_type_id=part_type_copy.id))
108+
109+
if action == "deletePartType":
110+
# Delete a part type
111+
delete_part_type = PartType.query.get_or_404(request.form["partTypeId"])
112+
db.session.delete(delete_part_type)
113+
db.session.commit()
114+
if action == "deleteBatch":
115+
# Delete a batch
116+
delete_batch = Batch.query.get_or_404(request.form["batchId"])
117+
db.session.delete(delete_batch)
118+
db.session.commit()
119+
120+
if action == "downloadData":
121+
# Download a batch's data in the form of a CSV file
122+
batch_id = request.form["batchId"]
123+
filepath = create_csv_from_data(batch_id)
124+
new_file_name = "batch_{batch}_data.csv".format(batch=batch_id)
125+
return send_file(filename_or_fp=filepath, cache_timeout=-1, as_attachment=True,
126+
attachment_filename=new_file_name)
127+
128+
if action == "downloadCsv":
129+
# Download any CSV data that has been uploaded for a batch
130+
batch_id = request.form["batchId"]
131+
filepath = recreate_csv_log(batch_id)
132+
new_file_name = "batch_{batch}_log.csv".format(batch=batch_id)
133+
return send_file(filename_or_fp=filepath, cache_timeout=-1, as_attachment=True,
134+
attachment_filename=new_file_name)
135+
136+
requested_batches_start = datetime(year=requested_year, month=requested_month, day=1, hour=0).timestamp()
137+
requested_batches_end = datetime(year=requested_year, month=requested_month + 1, day=1, hour=0).timestamp()
138+
batches = Batch.query.filter(Batch.created_timestamp.between(requested_batches_start, requested_batches_end))
139+
140+
# Send all of the parts to the page
141+
part_types = PartType.query.all()
142+
users = User.query.all()
143+
144+
nav_bar_title = "Admin Home"
145+
146+
return render_template('adminhome.html',
147+
requested_year=requested_year,
148+
requested_month=requested_month,
149+
current_year=now.year,
150+
nav_bar_title=nav_bar_title,
151+
part_types=part_types,
152+
users=users,
153+
batches=batches)

0 commit comments

Comments
 (0)