Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 179 additions & 1 deletion apps/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from apps import db, cache
from apps.api import blueprint
from apps.controllers import k8s
from apps.home.models import Labs, LabInstances, LabAnswers, LabAnswerSheet, UserLikes, UserFeedbacks, lab_groups
from apps.home.models import Labs, LabInstances, LabAnswers, LabAnswerSheet, UserLikes, UserFeedbacks, lab_groups, Namespaces
from apps.authentication.models import Users, Groups, DeletedGroupUsers, group_members, group_owners
from flask import request, current_app
from flask_login import login_required, current_user
Expand Down Expand Up @@ -505,3 +505,181 @@ def extend_lab(lab_id):
)

return {"status": "ok", "result": new_expiration}, 200

@blueprint.route("/namespaces", methods=["POST"])
@login_required
def create_namespace():
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of one function to insert and another one to update, where the actual difference between them is minimal (object loading, which can abstracted into a if statement), we could have one function called "upset_namespace()" (this naming convention is used across other projects) that does both tasks. Can you please refactor?

if current_user.category not in ["admin", "teacher"]:
return {"status": "fail", "result": "Unauthorized access."}, 401

content = request.get_json(silent=True)
if not content:
return {"status": "fail", "result": "Invalid content."}, 400

if current_user.category == "admin":
content["is_approved"] = True
content["is_enabled"] = True

owners = [int(x) for x in json.loads(content.get("owners", "[]"))]
members = [int(x) for x in json.loads(content.get("members", "[]"))]

if current_user.id not in owners:
owners = json.dumps([current_user.id] + owners)

content["owners"] = json.dumps(list(set(owners)))
content["members"] = json.dumps(list(set(members)))
content["user_id"] = current_user.id

namespace = Namespaces(**content)
db.session.add(namespace)
db.session.commit()

return {"status": "ok", "result": "Namespace saved successfully"}, 200


@blueprint.route("/namespaces/<namespace_id>", methods=["GET"])
@login_required
def get_namespace_by_id(namespace_id):
namespace = Namespaces.query.get(namespace_id)
if not namespace:
return {"status": "fail", "result": "Namespace not found"}, 404

users_in_namespace = json.loads(namespace.owners or "[]") + json.loads(namespace.members or "[]")
if current_user.id not in users_in_namespace:
return {"status": "fail", "result": "Unauthorized access to this namespace."}, 401

return {
"status": "ok",
"result": namespace.as_dict()
}, 200


@blueprint.route("/namespaces/<namespace_id>", methods=["PUT"])
@login_required
def update_namespace(namespace_id):
if current_user.category not in ["admin", "teacher"]:
return {"status": "fail", "result": "Unauthorized access"}, 401
Comment on lines +560 to +561
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we have a decorator called "check_user_category()" that can be used to reduce code duplicated like this validation.


content = request.get_json(silent=True)
if not content:
return {"status": "fail", "result": "Invalid content."}, 400

print(content)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left over?


namespace = Namespaces.query.get(namespace_id)
if not namespace:
return {"status": "fail", "result": "Namespace not found"}, 404

owners = [int(x) for x in json.loads(content.get("owners", namespace.owners))]
members = [int(x) for x in json.loads(content.get("members", namespace.members))]

if current_user.id not in owners:
return {"status": "fail", "result": "Unauthorized access to modify this namespace."}, 401

data = request.get_json(silent=True)
if not data:
return {"status": "fail", "result": "Invalid content"}, 400

namespace.description = data.get("description", namespace.description)
namespace.namespace = data.get("namespace", namespace.namespace)
namespace.organization = data.get("organization", namespace.organization)
namespace.website = data.get("website", namespace.website)
namespace.members = json.dumps(members)
namespace.owners = json.dumps(owners)

if (current_user.category == "admin"):
namespace.is_approved = data.get("is_approved", namespace.is_approved)
namespace.is_enabled = data.get("is_enabled", namespace.is_enabled)

try:
db.session.commit()
except Exception as exc:
current_app.logger.error(f"Failed to update lab: {exc}")
return {"status": "fail", "result": "Failed to update lab"}, 400

return {"status": "ok", "result": "Lab updated successfully"}, 200


@blueprint.route('/namespaces/bulk-approve', methods=["POST"])
@login_required
def bulk_approve_namespaces():
if current_user.category not in ["admin", "teacher"]:
return {}, 401

content = request.get_json(silent=True)
if not content:
return {"status": "fail", "result": "invalid content"}, 400

namespaces = []
errors = []
for namespace_id in content:
if not namespace_id.isdigit():
errors.append(f"Invalid namespace provided {namespace_id=}")
continue
namespace = Namespaces.query.get(int(namespace_id))
owners = [int(x) for x in json.loads(namespace.owners)]
if current_user.id not in owners or current_user.category != "admin":
errors.append(f"Unauthorized access to modify the namespace with ID {namespace_id=}")
continue
if not namespace or namespace.is_deleted:
errors.append(f"Invalid namespace provided {namespace_id=}")
continue
namespaces.append(namespace)

if errors:
return {"status": "fail", "result": "Invalid namespaces to approve: " + "<br/>".join(errors)}, 400

for namespace in namespaces:
namespace.is_approved = True
namespace.is_enabled = True

try:
db.session.commit()
except Exception as exc:
current_app.logger.error(f"Failed to approve namespaces: {exc}")
return {"status": "fail", "result": "Failed to save updated data"}, 400

return {"status": "ok", "result": "all namespaces approved"}, 200


@blueprint.route('/namespaces/bulk-delete', methods=["POST"])
@login_required
def bulk_delete_namespaces():
if current_user.category not in ["admin", "teacher"]:
return {}, 401

content = request.get_json(silent=True)
if not content:
return {"status": "fail", "result": "invalid content"}, 400

namespaces = []
errors = []
for namespace_id in content:
if not str(namespace_id).isdigit():
errors.append(f"Invalid namespace provided {namespace_id=}")
continue
namespace = Namespaces.query.get(int(namespace_id))
if not namespace or namespace.is_deleted:
errors.append(f"Invalid namespace provided {namespace_id=}")
continue
owners = [int(x) for x in json.loads(namespace.owners)]
if current_user.id not in owners and current_user.category != "admin":
errors.append(f"Unauthorized access to delete the namespace with ID {namespace_id=}")
continue
namespaces.append(namespace)

if errors:
return {"status": "fail", "result": "Invalid namespaces to delete: " + "<br/>".join(errors)}, 400

for namespace in namespaces:
who = "owner" if namespace.user_id == current_user.id else "admin"
namespace.is_deleted = True
namespace.finish_reason = "Finished by the " + who

try:
db.session.commit()
except Exception as exc:
current_app.logger.error(f"Failed to delete namespaces: {exc}")
return {"status": "fail", "result": "Failed to save updated data"}, 400

return {"status": "ok", "result": "all namespaces deleted"}, 200
44 changes: 44 additions & 0 deletions apps/home/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,50 @@ def as_dict(self):
def __repr__(self):
return f'<UserFeedbacks User {self.user_id}, Stars {self.stars}>'

class Namespaces (db.Model, AuditMixin):
__tablename__ = 'namespaces'
id = db.Column(db.Integer, primary_key=True)
description = db.Column(db.String)
namespace = db.Column(db.String)
organization = db.Column(db.String)
website = db.Column(db.String)
members = db.Column(db.String)
owners = db.Column(db.String)
Comment on lines +241 to +242
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

members and owners should be association tables, like what we have for group membership. Can you please check this?

is_approved = db.Column(db.Boolean, default=False)
is_enabled = db.Column(db.Boolean, default=False)
is_deleted = db.Column(db.Boolean, default=False)
user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
user = db.relationship('Users', backref='namespaces', foreign_keys=[user_id])
Comment on lines +246 to +247
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please clarify why we need those two fields (user being a relationship)?


@property
def members_dict(self):
try:
return json.loads(self.members)
except:
return {}

@property
def owners_dict(self):
try:
return json.loads(self.owners)
except:
return {}

def as_dict(self):
return {
"id": self.id,
"description": self.description,
"namespace": self.namespace,
"organization": self.organization,
"website": self.website,
"members": self.members_dict,
"owners": self.owners_dict,
"is_approved": self.is_approved,
"is_enabled": self.is_enabled,
"is_deleted": self.is_deleted,
"user_id": self.user_id
}


@event.listens_for(Labs, 'after_insert')
@event.listens_for(LabInstances, 'after_insert')
Expand Down
16 changes: 15 additions & 1 deletion apps/home/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"""
Copyright (c) 2019 - present AppSeed.us
"""
import json
import traceback
import uuid
import re
Expand All @@ -10,7 +11,7 @@
from apps import db, cache
from apps.home import blueprint
from apps.controllers import k8s
from apps.home.models import Labs, LabInstances, LabCategories, LabAnswers, LabAnswerSheet, HomeLogging, UserLikes, UserFeedbacks
from apps.home.models import Labs, LabInstances, LabCategories, LabAnswers, LabAnswerSheet, HomeLogging, UserLikes, UserFeedbacks, Namespaces
from apps.authentication.models import Users, Groups
from flask import render_template, request, current_app, redirect, url_for, session
from flask_login import login_required, current_user
Expand Down Expand Up @@ -949,3 +950,16 @@ def view_contact():
@login_required
def view_finished_lab_infos(lab_id):
return render_template("pages/finished_lab_infos.html", lab_id=lab_id)

@blueprint.route('/namespaces', methods=["GET"])
@login_required
@check_user_category(["admin", "teacher"])
def view_namespaces():
namespaces = Namespaces.query.all()
authorized_namespaces = []
for ns in namespaces:
users_in_namespace = json.loads(ns.owners or "[]") + json.loads(ns.members or "[]")
if (current_user.id in users_in_namespace) and ns.is_deleted == False:
authorized_namespaces.append(ns)

return render_template("pages/namespaces.html", namespaces=authorized_namespaces)
9 changes: 9 additions & 0 deletions apps/templates/includes/sidebar.html
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,15 @@
</p>
</a>
</li>
<li class="nav-item">
<a href="{{url_for('home_blueprint.view_namespaces')}}" class="nav-link {% if request.url_rule.endpoint == 'home_blueprint.namespaces' %} active {% endif %}">
<i class="nav-icon far fa-calendar-alt"></i>
<p>
Namespaces
<span class="badge badge-info right">{{g.namespaces}}</span>
</p>
</a>
</li>
<li class="nav-header">MANAGEMENT</li>
{% if current_user.category == "admin" or current_user.category == "teacher" %}
<li class="nav-item">
Expand Down
Loading