Skip to content

Commit

Permalink
Add infrastructure for controlling experiments; add chats to experiment.
Browse files Browse the repository at this point in the history
  • Loading branch information
liffiton committed Jul 20, 2024
1 parent 889e1ee commit 4595be2
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/codehelp/templates/tutor_nav_item.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
SPDX-License-Identifier: AGPL-3.0-only
#}

{% if auth['user_id'] and auth['is_tester'] %}
{% if auth['user_id'] and "chats_experiment" in auth['class_experiments'] %}
<a class="navbar-item has-text-success" href="{{ url_for('tutor.tutor_form') }}">
<div class="icon-text">
<span class="icon">
Expand Down
7 changes: 4 additions & 3 deletions src/codehelp/tutor.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
)
from gened.admin import bp as bp_admin
from gened.admin import register_admin_link
from gened.auth import get_auth, login_required, tester_required
from gened.auth import get_auth, login_required
from gened.contexts import (
ContextNotFoundError,
get_available_contexts,
get_context_by_name,
)
from gened.db import get_db
from gened.experiments import experiment_required
from gened.openai import LLMDict, get_completion, with_llm
from gened.queries import get_query
from openai.types.chat import ChatCompletionMessageParam
Expand All @@ -45,11 +46,11 @@ class AccessDeniedError(Exception):


@bp.before_request
@tester_required
@experiment_required("chats_experiment")
@login_required
def before_request() -> None:
"""Apply decorators to protect all tutor blueprint endpoints.
Use @tester_required first so that non-logged-in users get a 404 as well.
Use @experiment_required first so that non-logged-in users get a 404 as well.
"""


Expand Down
21 changes: 13 additions & 8 deletions src/gened/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class AuthDict(TypedDict, total=False):
role: str | None # current role name (e.g., 'instructor')
class_id: int | None # current class ID
class_name: str | None # current class name
class_experiments: list[str] # any experiments the current class is registered in
other_classes: list[ClassDict] # for storing active classes that are not the user's current class


Expand Down Expand Up @@ -129,6 +130,7 @@ def _get_auth_from_session() -> AuthDict:
# to be filled
'class_id': None,
'class_name': None,
'class_experiments': [],
'role': None,
'other_classes': [],
}
Expand All @@ -152,18 +154,21 @@ def _get_auth_from_session() -> AuthDict:
found_role = False # track whether the current role from auth is actually found as an active role
if role_rows:
for row in role_rows:
class_dict: ClassDict = {
'class_id': row['class_id'],
'class_name': row['name'],
'role': row['role'],
}
if row['id'] == auth_dict['role_id']:
found_role = True
auth_dict['class_id'] = row['class_id']
auth_dict['class_name'] = row['name']
auth_dict['role'] = row['role']
# merge class info into auth_dict
for key in class_dict:
auth_dict[key] = class_dict[key]
# check for any registered experiments in the current class
experiment_class_rows = db.execute("SELECT experiments.name FROM experiments JOIN experiment_class ON experiment_class.experiment_id=experiments.id WHERE experiment_class.class_id=?", [class_dict['class_id']]).fetchall()
auth_dict['class_experiments'] = [row['name'] for row in experiment_class_rows]
elif row['enabled']:
# store a list of any other classes that are enabled (for switching UI)
class_dict: ClassDict = {
'class_id': row['class_id'],
'class_name': row['name'],
'role': row['role'],
}
auth_dict['other_classes'].append(class_dict)

if not found_role:
Expand Down
35 changes: 35 additions & 0 deletions src/gened/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
#
# SPDX-License-Identifier: AGPL-3.0-only

from collections.abc import Callable
from functools import wraps
from typing import ParamSpec, TypeVar

from flask import (
abort,
flash,
redirect,
render_template,
Expand All @@ -13,8 +18,38 @@

from .admin import bp as bp_admin
from .admin import register_admin_link
from .auth import get_auth
from .db import get_db


# Functions for controlling access to experiments based on the current class
def current_class_in_experiment(experiment_name: str) -> bool:
""" Return True if the current active class is registered in the specified experiment,
False otherwise.
"""
db = get_db()
experiment_class_rows = db.execute("SELECT experiment_class.class_id FROM experiments JOIN experiment_class ON experiment_class.experiment_id=experiments.id WHERE experiments.name=?", [experiment_name]).fetchall()
experiment_class_ids = [row['class_id'] for row in experiment_class_rows]
auth = get_auth()
return 'class_id' in auth and auth['class_id'] in experiment_class_ids

# Decorator for routes designated as part of an experiment
# For decorator type hints
P = ParamSpec('P')
R = TypeVar('R')
def experiment_required(experiment_name: str) -> Callable[[Callable[P, R]], Callable[P, Response | R]]:
'''404 if the current class is not registered in the specified experiment.'''
def decorator(f: Callable[P, R]) -> Callable[P, Response | R]:
@wraps(f)
def decorated_function(*args: P.args, **kwargs: P.kwargs) -> Response | R:
if not current_class_in_experiment(experiment_name):
return abort(404)
else:
return f(*args, **kwargs)
return decorated_function
return decorator


# ### Admin routes ###
# Auth requirements covered by admin.before_request()

Expand Down
2 changes: 1 addition & 1 deletion src/gened/templates/experiment_form.html
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ <h1 class="title">New Experiment</h1>
{% if experiment %}
<button class="button is-danger" type="submit" formaction="{{ url_for('admin.experiment_delete', exp_id=experiment.id) }}" onclick="return confirm('Are you sure you want to delete this experiment?');">
<span class="delete mr-2"></span>
Delete {{ experiment.name }}
Delete '{{ experiment.name }}'
</button>
{% endif %}
</div>
Expand Down

0 comments on commit 4595be2

Please sign in to comment.