07. Manager Access
The user with manager role will have the same set of features as the admin, but every action of manager will ask for admin approval. Whenever the manager will do some action it will be stored as a request in the database and send to the admin, now admin can approve or reject the request. It's up to him/her.
So, let's start with request for CRUD operations on category.
Request to Add Category
Since every action of a manager will be taken as a request, so we will create a new table to store those requests. This table will contain the details of the request like the type of the request, message which will contain the actual data of the request, sender and reciever of the request, status of the request and the timestamp when the request was created.
Database Update
The table will have the following structure:
python filename=database.py
...
class RequestResponse(db.Model): # [tl! add:start]
__tablename__ = 'request_response'
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
status = db.Column(db.String, nullable=False) # approved or rejected or pending
type = db.Column(db.String, nullable=False)
message = db.Column(db.String, nullable=False)
image = db.Column(db.BLOB)
sender = db.Column(db.Integer, db.ForeignKey('user.id'))
receiver = db.Column(db.Integer, db.ForeignKey('user.id'))
timestamp = db.Column(db.DateTime, nullable=False) # [tl! add:end]
Note Even a new manager signup on the app will ask for the approval from the admin.
All the columns in the table can be easily interpreted by their names, but the two columns which I need to explain are type
and message
:
-
type
is the type of the request. It can bemanager
,update
,delete
orview
. type | Description manager | New manager signup request category | Adding New category request category update | Updating an existing category request category delete | Deleting an existing category request product | Adding New product request product update | Updating an existing product request product delete | Deleting an existing product request
-
message
will treat specially. This message will contain the data of the request. For example, if a new user wants to signup as a manager the message will store the comma separated stirng with all the detailsmessage = f"{data['email']},{data['name']},{data['role']},{data['password']}"
.
Updating Dashboard
Since we have separate Dashboard js file for manager so we will have to implement independent componets there.
Let's update the manager dashboard.
js filename=static/views/ManagerApp.js
const ManagerApp = {
name: "ManagerApp",
template: `
...
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto">
...
<li class="nav-item"> <!-- [tl! add:start] -->
<a class="nav-link pointer-on-hover" @click="createCat">Add category</a>
</li> <!-- [tl! add:end] -->
</ul>
</div>
...
`,
...
methods: {
createCat() { // [tl! add:start]
if (this.$route.path != "/app/manager/cat/create") {
this.$router.push("/app/manager/cat/create");
}
} // [tl! add:end]
},
mounted() {
this.$store.dispatch("fetchCategories");// [tl! add]
},
};
export default ManagerApp;
Our manager dashboard will look same as of the admin dashboard.
Backend Implementation
python filename=app.py
...
from role_auth import role_required
...
@main.route('/add/cat', methods=['POST'])
@jwt_required()
@role_required(['admin', 'manager'])
def create():
data = request.get_json()
if data:
if not Category.query.filter_by(name=data['name']).first():
if current_user.role == 'manager': # [tl! add:start]
admin = User.query.filter_by(role='admin').first()
message = data['name']
requested = RequestResponse(status='pending',
type='category',
message=message,
sender=current_user.email,
receiver=admin.email,
timestamp=datetime.now())
db.session.add(requested)
db.session.commit()
noti_data = {
'id': requested.id,
'state': requested.status,
'message': requested.message,
'sender': requested.sender,
'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
}
return jsonify({'message': 'Created request',
'resource': noti_data}), 201
else: # [tl! add:end]
category = Category(name=data['name'])
db.session.add(category)
db.session.commit()
return jsonify({
'message': f"Category {data['name']} created successfully",
'resource': {
'id': category.id,
'name': category.name
}
}), 201
abort(409, message="Resource already exists")
else:
abort(404, message="Not found")
...
Request to Update Category
The way we have created add request similarly we will create update request.
Edit Category Component
What we want to do is that whenever we click on edit it should show a form to edit and all the fields should show the current data.
js filename=static/components/EditCatCompo.js
const EditCatCompo = {
name: "EditCatCompo",
...
methods: {
closeCard() { // [tl! remove:start]
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
} // [tl! remove:end]
closeCard() {// [tl! add:start]
if (this.$store.state.authenticatedUser.role === "admin") {
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
} else {
if (this.$route.path != "/app/manager") {
this.$router.push("/app/manager");
}
}
} // [tl! add:end]
},
mounted() {
this.fetchcategory();
},
};
export default EditCatCompo;
Request to Delete Category
Already we have implemented the button for deleting the category, we just need the backend implementation for manager.
Backend Implementation
python filename=app.py
...
@main.route('/update/<int:cat_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update(cat_id):
if isinstance(cat_id, int):
...
if request.method == 'DELETE':
category = Category.query.filter_by(id=cat_id).first()
if category:
products = Product.query.filter_by( # [tl! remove:start]
category_id=int(cat_id)).all()
for product in products:
carts = Cart.query.filter_by(
product_id=product.id).all()
for cart in carts:
db.session.delete(cart)
db.session.commit()
db.session.delete(product)
db.session.commit()
db.session.delete(category)
db.session.commit()
return jsonify(
{
'message': f"Category { category.name } Deleted\
successfully from database",
'resource': {
'id': category.id,
'name': category.name
}
}), 201 # [tl! remove:end]
if current_user.role == 'manager': # [tl! add:start]
admin = User.query.filter_by(role='admin').first()
message = f"{cat_id},{category.name}"
requested = RequestResponse(status='pending',
type='category delete',
message=message,
sender=current_user.email,
receiver=admin.email,
timestamp=datetime.now())
db.session.add(requested)
db.session.commit()
noti_data = {
'id': requested.id,
'state': requested.status,
'message': requested.message,
'sender': requested.sender,
'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
}
return jsonify(
{
'message': 'Created request',
'resource': noti_data
}
), 201
else:
products = Product.query.filter_by(
category_id=int(cat_id)).all()
for product in products:
carts = Cart.query.filter_by(
product_id=product.id).all()
for cart in carts:
db.session.delete(cart)
db.session.commit()
db.session.delete(product)
db.session.commit()
db.session.delete(category)
db.session.commit()
return jsonify(
{
'message': f"Category { category.name } Deleted\
successfully from database",
'resource': {
'id': category.id,
'name': category.name
}
}), 201 # [tl! add:end]
return jsonify({'message': "Not found"}), 404
else:
return '', 400
...
Now we will move to the product.
Request to Create Product
Updating the UI
js filename=static/components/EditProCompo.js
const EditProCompo = {
...
methods: {
closeCard() {
if (this.$store.state.authenticatedUser.role === "admin") {
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
} else {
if (this.$route.path != "/app/manager") {
this.$router.push("/app/manager");
}
}
}
}
...
};
export default EditProCompo;
Backend Implementation
python filename=app.py
@main.route('/update/product/<int:prod_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update_product(prod_id):
if isinstance(prod_id, int):
if request.method == 'GET':
...
elif request.method == 'PUT':
product = Product.query.filter_by(id=prod_id).first()
if product: # [tl! remove:start]
product.name = request.form['name']
product.quantity = int(request.form['quantity'])
product.manufacture = datetime.strptime(
request.form['manufacture'], '%Y-%m-%d')
product.expiry = datetime.strptime(
request.form['expiry'], '%Y-%m-%d')
product.rpu = float(request.form['rpu'])
product.category_id = float(request.form['category_id'])
product.unit = request.form['unit']
product.description = request.form['description']
# Handle file upload
product.image = request.files['image'].read()
db.session.commit()
prod_data = {
'id': product.id,
'quantity': product.quantity,
'name': product.name,
'manufacture': product.manufacture,
'expiry': product.expiry,
'description': product.description,
'rpu': product.rpu,
'unit': product.unit,
# Assuming image is stored as a base64-encoded string
'image': base64.b64encode(
product.image).decode('utf-8')
}
return jsonify({
'message': f"Product {request.form['name']} updated\
successfully in the database",
'resource': prod_data}), 201
else:
return jsonify({'message': "Not found"}), 404 # [tl! remove:end]
if product: # [tl! add:start]
if current_user.role == 'manager':
admin = User.query.filter_by(role='admin').first()
message = f"{prod_id},{request.form['name']},\
{request.form['quantity']},\
{request.form['manufacture']},\
{request.form['expiry']},{request.form['rpu']},\
{request.form['category_id']},{request.form['unit']},\
{request.form['description']}"
requested = RequestResponse(
status='pending',
type='product update',
message=message,
sender=current_user.email,
image=request.files['image'].read(),
receiver=admin.email,
timestamp=datetime.now())
db.session.add(requested)
db.session.commit()
noti_data = {
'id': requested.id,
'state': requested.status,
'message': requested.message,
'sender': requested.sender,
'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
}
return jsonify(
{'message': 'Created request',
'resource': noti_data}), 201
else:
product.name = request.form['name']
product.quantity = int(request.form['quantity'])
product.manufacture = datetime.strptime(
request.form['manufacture'], '%Y-%m-%d')
product.expiry = datetime.strptime(
request.form['expiry'], '%Y-%m-%d')
product.rpu = float(request.form['rpu'])
product.category_id = float(request.form['category_id'])
product.unit = request.form['unit']
product.description = request.form['description']
# Handle file upload
product.image = request.files['image'].read()
db.session.commit()
prod_data = {
'id': product.id,
'quantity': product.quantity,
'name': product.name,
'manufacture': product.manufacture,
'expiry': product.expiry,
'description': product.description,
'rpu': product.rpu,
'unit': product.unit,
# Assuming image is stored as a base64-encoded string
'image': base64.b64encode(
product.image).decode('utf-8')
}
return jsonify({
'message': f"Product {request.form['name']} updated\
successfully in the database",
'resource': prod_data}), 201
else:
return jsonify({'message': "Not found"}), 404 # [tl! add:end]
else:
return '', 400
Request to Delete Product
Update the Component
js filename=static/components/EditProCompo.js
const EditProCompo = {
...
methods: {
closeCard() {
if (this.$route.path != "/app/admin") { // [tl! remove: start]
this.$router.push("/app/admin");
} // [tl! remove:end]
if (this.$store.state.authenticatedUser.role === "admin") { // [tl! add:start]
if (this.$route.path != "/app/admin") {
this.$router.push("/app/admin");
}
} else {
if (this.$route.path != "/app/manager") {
this.$router.push("/app/manager");
}
}// [tl! add:end]
},
...
},
mounted() {
this.fetchproduct();
},
};
export default EditProCompo;
Here we just need to implement the backend.
Backend Implementation
python filename=app.py
@main.route('/update/product/<int:prod_id>', methods=['GET', 'PUT', 'DELETE'])
@jwt_required()
@role_required(['admin'])
def update_product(prod_id):
if isinstance(prod_id, int):
...
elif request.method == 'DELETE':
product = Product.query.filter_by(id=prod_id).first()
if product:
if current_user.role == 'manager':
admin = User.query.filter_by(role='admin').first()
message = f"{prod_id},{product.name}"
requested = RequestResponse(status='pending',
type='product delete',
message=message,
sender=current_user.email,
receiver=admin.email,
timestamp=datetime.now())
db.session.add(requested)
db.session.commit()
noti_data = {
'id': requested.id,
'state': requested.status,
'message': requested.message,
'sender': requested.sender,
'timestamp': requested.timestamp.strftime('%Y-%m-%d'),
}
return jsonify(
{'message': 'Created request',
'resource': noti_data}), 201
else:
carts = Cart.query.filter_by(product_id=product.id).all()
for cart in carts:
db.session.delete(cart)
db.session.commit()
db.session.delete(product)
db.session.commit()
return jsonify(
{'message': f"Product {product.name} deleted\
successfully from the database",
'resource': prod_id}), 201
return jsonify({'message': "Not found"}), 404
else:
return '', 400
Now manager is able to create delete request for products.
Logout
We have already implemented the backend code just need to add the logout method in the frontend.
js filename=static/views/ManagerApp.js
...
<nav class="navbar navbar-expand-lg navbar-dark bg-success">
...
<ul class="navbar-nav ms-auto">
...
<li class="nav-item"> <!-- [tl! add:start] -->
<a class="nav-link pointer-on-hover" @click="logout">logout</a>
</li> <!-- [tl! add:end] -->
</ul>
...
</nav>
...
methods: {
...
async logout() {
try {
const response = await fetch("http://127.0.0.1:5000/logout", {
method: "DELETE",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${localStorage.getItem("token")}`,
},
});
if (response.status === 200) {
const data = await response.json();
this.$store.commit("setAuthenticatedUser", "");
localStorage.removeItem("token");
if (this.$route.path != "/app") {
this.$router.push("/app");
}
} else {
const data = await response.json();
alert(data.message);
}
} catch (error) {
console.error(error);
}
}
}
Testing it out
Now we can start the app and test all the feature implemented on this page.
Run the app.
python3 app.py
Commit the changes to the local git.
git add .
git commit -m "added admin features"