feat(platform): ship AI fallback stack and entity-scoped course planning
Unifies the new LangGraph-driven course-plan/media flow with robust provider fallbacks, admin AI provider settings, editable book-style materials, and strict entity isolation across LMS/course-plan APIs. Adds admin-only entity membership management in the Entities UI so users can switch linked entities directly from the platform. Made-with: Cursor
This commit is contained in:
@@ -304,8 +304,23 @@ class ApprovalWorkflowController(http.Controller):
|
||||
if not req_rec.exists():
|
||||
return _error_response('Request not found', 404)
|
||||
|
||||
# Authorization — only the user assigned to the current stage
|
||||
# (or a system admin) may approve. Without this check any
|
||||
# authenticated user could ride a valid JWT and approve any
|
||||
# request, bypassing the entire approval workflow.
|
||||
current_user = request.env.user
|
||||
stage = req_rec.current_stage_id
|
||||
assigned = stage.approver_id if stage else None
|
||||
is_admin = (
|
||||
current_user.has_group('base.group_system')
|
||||
or getattr(current_user, 'user_type', None) == 'admin'
|
||||
)
|
||||
if assigned and assigned.id != current_user.id and not is_admin:
|
||||
return _error_response(
|
||||
'You are not the assigned approver for this stage', 403,
|
||||
)
|
||||
|
||||
with request.env.cr.savepoint():
|
||||
stage = req_rec.current_stage_id
|
||||
if stage:
|
||||
stage.write({
|
||||
'status': 'approved',
|
||||
@@ -362,8 +377,20 @@ class ApprovalWorkflowController(http.Controller):
|
||||
req_rec = request.env['encoach.approval.request'].sudo().browse(req_id)
|
||||
if not req_rec.exists():
|
||||
return _error_response('Request not found', 404)
|
||||
# Same authorization gate as approve — the rejection action
|
||||
# is just as sensitive as the approval action.
|
||||
current_user = request.env.user
|
||||
stage = req_rec.current_stage_id
|
||||
assigned = stage.approver_id if stage else None
|
||||
is_admin = (
|
||||
current_user.has_group('base.group_system')
|
||||
or getattr(current_user, 'user_type', None) == 'admin'
|
||||
)
|
||||
if assigned and assigned.id != current_user.id and not is_admin:
|
||||
return _error_response(
|
||||
'You are not the assigned approver for this stage', 403,
|
||||
)
|
||||
with request.env.cr.savepoint():
|
||||
stage = req_rec.current_stage_id
|
||||
if stage:
|
||||
stage.write({
|
||||
'status': 'rejected',
|
||||
|
||||
@@ -11,6 +11,18 @@ _logger = logging.getLogger(__name__)
|
||||
ENTITY_MODEL = 'encoach.entity'
|
||||
|
||||
|
||||
def _ensure_admin_user():
|
||||
user = request.env.user.sudo()
|
||||
is_admin = bool(
|
||||
user.has_group('base.group_system')
|
||||
or user.has_group('base.group_erp_manager')
|
||||
or getattr(user, 'user_type', '') == 'admin'
|
||||
)
|
||||
if not is_admin:
|
||||
raise PermissionError('Admin access required')
|
||||
return user
|
||||
|
||||
|
||||
def _entity_to_dict(entity):
|
||||
d = entity.to_api_dict() if hasattr(entity, 'to_api_dict') else {
|
||||
'id': entity.id,
|
||||
@@ -35,6 +47,16 @@ def _role_to_dict(role):
|
||||
}
|
||||
|
||||
|
||||
def _user_to_dict(user):
|
||||
return {
|
||||
'id': user.id,
|
||||
'name': user.name or '',
|
||||
'login': user.login or '',
|
||||
'email': user.email or '',
|
||||
'active': bool(user.active),
|
||||
}
|
||||
|
||||
|
||||
class EntityController(http.Controller):
|
||||
|
||||
@http.route('/api/entities', type='http', auth='public',
|
||||
@@ -59,6 +81,62 @@ class EntityController(http.Controller):
|
||||
_logger.exception('list entities failed')
|
||||
return _error_response(str(e), 500)
|
||||
|
||||
@http.route('/api/entities/<int:entity_id>/users', type='http', auth='public',
|
||||
methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
def list_entity_users(self, entity_id, **kw):
|
||||
try:
|
||||
_ensure_admin_user()
|
||||
entity = request.env[ENTITY_MODEL].sudo().browse(entity_id)
|
||||
if not entity.exists():
|
||||
return _error_response('Entity not found', 404)
|
||||
items = [_user_to_dict(u) for u in entity.user_ids.sorted('name')]
|
||||
return _json_response({
|
||||
'items': items,
|
||||
'data': items,
|
||||
'total': len(items),
|
||||
'entity_id': entity.id,
|
||||
})
|
||||
except Exception as e:
|
||||
code = 403 if isinstance(e, PermissionError) else 500
|
||||
return _error_response(str(e), code)
|
||||
|
||||
@http.route('/api/entities/<int:entity_id>/users', type='http', auth='public',
|
||||
methods=['PATCH', 'PUT'], csrf=False)
|
||||
@jwt_required
|
||||
def update_entity_users(self, entity_id, **kw):
|
||||
try:
|
||||
_ensure_admin_user()
|
||||
body = _get_json_body() or {}
|
||||
raw_ids = body.get('user_ids') or []
|
||||
if not isinstance(raw_ids, list):
|
||||
return _error_response('user_ids must be a list', 400)
|
||||
try:
|
||||
user_ids = [int(uid) for uid in raw_ids if uid is not None]
|
||||
except (TypeError, ValueError):
|
||||
return _error_response('user_ids must contain integers', 400)
|
||||
|
||||
entity = request.env[ENTITY_MODEL].sudo().browse(entity_id)
|
||||
if not entity.exists():
|
||||
return _error_response('Entity not found', 404)
|
||||
|
||||
users = request.env['res.users'].sudo().browse(user_ids).exists()
|
||||
if len(users) != len(user_ids):
|
||||
return _error_response('Some users do not exist', 400)
|
||||
|
||||
entity.write({'user_ids': [(6, 0, users.ids)]})
|
||||
updated = [_user_to_dict(u) for u in entity.user_ids.sorted('name')]
|
||||
return _json_response({
|
||||
'success': True,
|
||||
'entity_id': entity.id,
|
||||
'user_ids': entity.user_ids.ids,
|
||||
'items': updated,
|
||||
'total': len(updated),
|
||||
})
|
||||
except Exception as e:
|
||||
code = 403 if isinstance(e, PermissionError) else 500
|
||||
return _error_response(str(e), code)
|
||||
|
||||
@http.route('/api/entities/<int:entity_id>', type='http', auth='public',
|
||||
methods=['GET'], csrf=False)
|
||||
@jwt_required
|
||||
|
||||
Reference in New Issue
Block a user