import click
import csv
import json
import os
import re
import shutil
import signal
import subprocess
from sqlalchemy import create_engine
from sqlalchemy.exc import IntegrityError
from psycopg2.errors import UniqueViolation
import sys
class Config(object):
def __init__(self):
self.verbose = False
pass_config = click.make_pass_decorator(Config, ensure=True)
configuration
REQ_ENV = ["WEB2PY_CONFIG", "DBURL"]
OPT_ENV = ["WEB2PY_MIGRATE"]
APP = "runestone"
APP_PATH = "applications/{}".format(APP)
DBSDIR = "{}/databases".format(APP_PATH)
BUILDDIR = "{}/build".format(APP_PATH)
PRIVATEDIR = "{}/private".format(APP_PATH)
CUSTOMDIR = "{}/custom_courses".format(APP_PATH)
@click.group(chain=True)
@click.option("--verbose", is_flag=True, help="More verbose output")
@click.option("--if_clean", is_flag=True, help="only run if database is uninitialized")
@pass_config
def cli(config, verbose, if_clean):
Type subcommand –help for help on any subcommand
checkEnvironment()
conf = os.environ.get("WEB2PY_CONFIG", "production")
if conf == "production":
config.dburl = os.environ.get("DBURL")
elif conf == "development":
config.dburl = os.environ.get("DEV_DBURL")
elif conf == "test":
config.dburl = os.environ.get("TEST_DBURL")
else:
click.echo("Incorrect WEB2PY_CONFIG")
sys.exit(1)
DAL uses “postgres:”, while SQLAlchemy (and the PostgreSQL spec) uses “postgresql:”. Fix.
remove_prefix = "postgres://"
if config.dburl.startswith(remove_prefix):
config.dburl = "postgresql://" + config.dburl[len(remove_prefix) :]
config.conf = conf
config.dbname = re.match(r"postgres.*//.*?@.*?/(.*)", config.dburl).group(1)
config.dbhost = re.match(r"postgres.*//.*?@(.*?)/(.*)", config.dburl).group(1)
if conf != "production":
config.dbuser = re.match(r"postgres.*//(.*?)(:.*?)?@(.*?)/(.*)", config.dburl).group(1)
else:
config.dbuser = re.match(r"postgres.*//(.*?):(.*?)@(.*?)/(.*)", config.dburl).group(1)
if verbose:
echoEnviron(config)
if if_clean:
count = check_db_for_useinfo(config)
if count != 0:
click.echo("The database is already inititlized Exiting")
sys.exit()
config.verbose = verbose
initdb
@cli.command()
@click.option(
"--list_tables", is_flag=True, help="List all of the defined tables when done"
)
@click.option(
"--reset", is_flag=True, help="drop database and delete all migration information"
)
@click.option("--fake", is_flag=True, help="perform a fake migration")
@click.option("--force", is_flag=True, help="answer Yes to confirm questions")
@pass_config
def initdb(config, list_tables, reset, fake, force):
Initialize and optionally reset the database
os.chdir(findProjectRoot())
if not os.path.exists(DBSDIR):
click.echo("Making databases folder")
os.mkdir(DBSDIR)
if not os.path.exists(PRIVATEDIR):
click.echo("Making private directory for auth")
os.mkdir(PRIVATEDIR)
if reset:
if not force:
click.confirm(
"Resetting the database will delete the database and the contents of the databases folder. Are you sure?",
default=False,
abort=True,
prompt_suffix=": ",
show_default=True,
err=False,
)
res = subprocess.call(
"dropdb --if-exists --host={} --username={} {}".format(
config.dbhost, config.dbuser, config.dbname
),
shell=True,
)
if res == 0:
res = subprocess.call(
"createdb --echo --host={} --username={} {}".format(
config.dbhost, config.dbuser, config.dbname
),
shell=True,
)
else:
click.echo("Failed to drop the database do you have permission?")
sys.exit(1)
click.echo("Removing all files in databases/")
table_migrate_prefix = "runestone_"
if config.conf == "test":
table_migrate_prefix = "test_runestone_"
for the_file in os.listdir(DBSDIR):
file_path = os.path.join(DBSDIR, the_file)
try:
if os.path.isfile(file_path) and file_path.startswith(
os.path.join(DBSDIR, table_migrate_prefix)
):
print("removing ", file_path)
os.unlink(file_path)
except Exception as e:
print(e)
if len(os.listdir("{}/databases".format(APP_PATH))) > 1 and not fake and not force:
click.confirm(
"It appears you already have database migration information do you want to proceed?",
default=False,
abort=True,
prompt_suffix=": ",
show_default=True,
err=False,
)
click.echo(
message="Initializing the database", file=None, nl=True, err=False, color=None
)
if fake:
os.environ["WEB2PY_MIGRATE"] = "fake"
list_tables = "-A --list_tables" if config.verbose or list_tables else ""
cmd = "python web2py.py --no-banner -S {} -M -R {}/rsmanage/initialize_tables.py {}".format(
APP, APP_PATH, list_tables
)
click.echo("Running: {}".format(cmd))
res = subprocess.call(cmd, shell=True)
if res != 0:
click.echo(message="Database Initialization Failed")
@cli.command()
@click.option("--fake", is_flag=True, help="perform a fake migration")
@pass_config
def migrate(config, fake):
"Startup web2py and load the models with Migrate set to Yes"
os.chdir(findProjectRoot())
if fake:
os.environ["WEB2PY_MIGRATE"] = "fake"
else:
os.environ["WEB2PY_MIGRATE"] = "Yes"
subprocess.call(
"python web2py.py -S runestone -M -R applications/runestone/rsmanage/migrate.py",
shell=True,
)
run
@cli.command()
@click.option(
"--with-scheduler", is_flag=True, help="Star the background task scheduler too"
)
@pass_config
def run(config, with_scheduler):
Starts up the runestone server and optionally scheduler
os.chdir(findProjectRoot())
_ = subprocess.Popen(
"python -u web2py.py --ip=0.0.0.0 --port=8000 --password='<recycle>' -d rs.pid -K runestone --nogui -X",
shell=True,
)
shutdown
@cli.command()
@pass_config
def shutdown(config):
Shutdown the server and any schedulers
os.chdir(findProjectRoot())
with open("rs.pid", "r") as pfile:
pid = int(pfile.read())
click.echo("killing process {}".format(pid))
os.kill(pid, signal.SIGINT)
select worker_name from scheduler_worker; iterate over results to kill all schedulers
eng = create_engine(config.dburl)
res = eng.execute("select worker_name from scheduler_worker")
for row in res:
result will be form of hostname#pid
os.kill(int(row[0].split("#")[1]), signal.SIGINT)
addcourse
@cli.command()
@click.option("--course-name", help="The name of a course to create")
@click.option("--basecourse", help="The name of the basecourse")
@click.option(
"--start-date", default="2001-01-01", help="Start Date for the course in YYYY-MM-DD"
)
@click.option("--python3", is_flag=True, default=True, help="Use python3 style syntax")
@click.option(
"--login-required",
is_flag=True,
help="Only registered users can access this course?",
)
@click.option("--institution", help="Your institution")
@click.option("--language", default="python", help="Default Language for your course")
@click.option("--host", default="runestone.academy", help="runestone server host name")
@click.option(
"--allow_pairs",
is_flag=True,
default=False,
help="enable experimental pair programming support",
)
@pass_config
def addcourse(
config,
course_name,
basecourse,
start_date,
python3,
login_required,
institution,
language,
host,
allow_pairs,
):
Create a course in the database
os.chdir(findProjectRoot()) # change to a known location
eng = create_engine(config.dburl)
done = False
if course_name:
use_defaults = True
else:
use_defaults = False
while not done:
if not course_name:
course_name = click.prompt("Course Name")
if not python3 and not use_defaults:
python3 = (
"T" if click.confirm("Use Python3 style syntax?", default="T") else "F"
)
else:
python3 = "T" if python3 else "F"
if not basecourse and not use_defaults:
basecourse = click.prompt("Base Course")
if not start_date and not use_defaults:
start_date = click.prompt("Start Date YYYY-MM-DD")
if not institution and not use_defaults:
institution = click.prompt("Your institution")
if not login_required and not use_defaults:
login_required = (
"T" if click.confirm("Require users to log in", default="T") else "F"
)
else:
login_required = "T" if login_required else "F"
if not allow_pairs and not use_defaults:
allow_pairs = (
"T"
if click.confirm("Enable pair programming support", default=False)
else "F"
)
else:
allow_pairs = "T" if allow_pairs else "F"
res = eng.execute(
"select id from courses where course_name = '{}'".format(course_name)
).first()
if not res:
done = True
else:
click.confirm(
"Course {} already exists continue with a different name?".format(
course_name
),
default=True,
abort=True,
)
eng.execute(
"""insert into courses (course_name, base_course, python3, term_start_date, login_required, institution, allow_pairs)
values ('{}', '{}', '{}', '{}', '{}', '{}', '{}')
""".format(
course_name,
basecourse,
python3,
start_date,
login_required,
institution,
allow_pairs,
)
)
click.echo("Course added to DB successfully")
build
@cli.command()
@click.option(
"--course", help="The name of a course that should already exist in the DB"
)
@click.option("--repo", help="URL to a git repository with the book to build")
@click.option(
"--skipclone", is_flag=True, help="avoid recloning when directory is already there"
)
@pass_config
def build(config, course, repo, skipclone):
Build the book for an existing course
os.chdir(findProjectRoot()) # change to a known location
eng = create_engine(config.dburl)
res = eng.execute(
"select id from courses where course_name = '{}'".format(course)
).first()
if not res:
click.echo(
"Error: The course {} must already exist in the database -- use rsmanage addcourse".format(
course
),
color="red",
)
exit(1)
os.chdir(BUILDDIR)
if not skipclone:
res = subprocess.call("git clone {}".format(repo), shell=True)
if res != 0:
click.echo(
"Cloning the repository failed, please check the URL and try again"
)
exit(1)
proj_dir = os.path.basename(repo).replace(".git", "")
click.echo("Switching to project dir {}".format(proj_dir))
os.chdir(proj_dir)
paver_file = os.path.join("..", "..", "custom_courses", course, "pavement.py")
click.echo("Checking for pavement {}".format(paver_file))
if os.path.exists(paver_file):
shutil.copy(paver_file, "pavement.py")
else:
cont = click.confirm("WARNING -- NOT USING CUSTOM PAVEMENT FILE - continue")
if not cont:
sys.exit()
try:
if os.path.exists("pavement.py"):
sys.path.insert(0, os.getcwd())
from pavement import options, dest
else:
click.echo(
"I can't find a pavement.py file in {} you need that to build".format(
os.getcwd()
)
)
exit(1)
except ImportError as e:
click.echo("You do not appear to have a good pavement.py file.")
print(e)
exit(1)
if options.project_name != course:
click.echo(
"Error: {} and {} do not match. Your course name needs to match the project_name in pavement.py".format(
course, project_name
)
)
exit(1)
res = subprocess.call("runestone build --all", shell=True)
if res != 0:
click.echo("building the book failed, check the log for errors and try again")
exit(1)
click.echo("Build succeedeed... Now deploying to static")
if dest != "../../static":
click.echo(
"Incorrect deployment directory. dest should be ../../static in pavement.py"
)
exit(1)
res = subprocess.call("runestone deploy", shell=True)
if res == 0:
click.echo("Success! Book deployed")
else:
click.echo("Deploy failed, check the log to see what went wrong.")
click.echo("Cleaning up")
os.chdir("..")
subprocess.call("rm -rf {}".format(proj_dir), shell=True)
inituser
@cli.command()
@click.option("--instructor", is_flag=True, help="Make this user an instructor")
@click.option(
"--fromfile",
default=None,
type=click.File(mode="r"),
help="read a csv file of users of the form username, email, first_name, last_name, password, course",
)
@click.option("--username", help="Username, must be unique")
@click.option("--password", help="password - plaintext -- sorry")
@click.option("--first_name", help="Real first name")
@click.option("--last_name", help="Real last name")
@click.option("--email", help="email address for password resets")
@click.option("--course", help="course to register for")
@click.option(
"--ignore_dupes",
is_flag=True,
help="ignore duplicate student errors and keep processing",
)
@pass_config
def inituser(
config,
instructor,
fromfile,
username,
password,
first_name,
last_name,
email,
course,
ignore_dupes,
):
Add a user (or users from a csv file)
os.chdir(findProjectRoot())
mess = [
"Success",
"Value Error -- check the format of your CSV file",
"Duplicate User -- Check your data or use --ignore_dupes if you are adding students to an existing CSV",
"Unknown Error -- check the format of your CSV file",
]
if fromfile:
if fromfile then be sure to get the full path name NOW. csv file should be username, email first_name, last_name, password, course users from a csv cannot be instructors
for line in csv.reader(fromfile):
if len(line) != 6:
click.echo("Not enough data to create a user. Lines must be")
click.echo("username, email first_name, last_name, password, course")
exit(1)
if "@" not in line[1]:
click.echo("emails should have an @ in them in column 2")
exit(1)
userinfo = {}
userinfo["username"] = line[0]
userinfo["password"] = line[4]
userinfo["first_name"] = line[2]
userinfo["last_name"] = line[3]
userinfo["email"] = line[1]
userinfo["course"] = line[5]
userinfo["instructor"] = False
os.environ["RSM_USERINFO"] = json.dumps(userinfo)
res = subprocess.call(
"python web2py.py --no-banner -S runestone -M -R applications/runestone/rsmanage/makeuser.py",
shell=True,
)
if res != 0:
click.echo(
"Failed to create user {} error {}".format(line[0], mess[res])
)
if res == 2 and ignore_dupes:
click.echo(f"ignoring duplicate user {userinfo['username']}")
continue
else:
exit(res)
else:
userinfo = {}
userinfo["username"] = username or click.prompt("Username")
userinfo["password"] = password or click.prompt("Password", hide_input=True)
userinfo["first_name"] = first_name or click.prompt("First Name")
userinfo["last_name"] = last_name or click.prompt("Last Name")
userinfo["email"] = email or click.prompt("email address")
userinfo["course"] = course or click.prompt("course name")
if not instructor:
if (
username and course
): # user has supplied other info via CL parameter safe to assume False
userinfo["instructor"] = False
else:
userinfo["instructor"] = click.confirm(
"Make this user an instructor", default=False
)
os.environ["RSM_USERINFO"] = json.dumps(userinfo)
res = subprocess.call(
"python web2py.py --no-banner -S runestone -M -R applications/runestone/rsmanage/makeuser.py",
shell=True,
)
if res != 0:
click.echo(
"Failed to create user {} error {} fix your data and try again. Use --verbose for more detail".format(
userinfo["username"], res
)
)
exit(1)
else:
click.echo("Success")
@cli.command()
@click.option("--username", help="Username, must be unique")
@click.option("--password", help="password - plaintext -- sorry")
@pass_config
def resetpw(config, username, password):
Utility to change a users password. Useful If they can’t do it through the normal mechanism
os.chdir(findProjectRoot())
userinfo = {}
userinfo["username"] = username or click.prompt("Username")
userinfo["password"] = password or click.prompt("Password", hide_input=True)
eng = create_engine(config.dburl)
res = eng.execute(
"select * from auth_user where username = %s", userinfo["username"]
).first()
if not res:
click.echo("ERROR - User: {} does not exist.".format(userinfo["username"]))
exit(1)
os.environ["RSM_USERINFO"] = json.dumps(userinfo)
res = subprocess.call(
"python web2py.py --no-banner -S runestone -M -R applications/runestone/rsmanage/makeuser.py -A --resetpw",
shell=True,
)
if res != 0:
click.echo(
"Failed to create user {} error {} fix your data and try again. Use --verbose for more detail".format(
userinfo["username"], res
)
)
exit(1)
else:
click.echo("Success")
@cli.command()
@click.option("--username", help="Username, must be unique")
@pass_config
def rmuser(config, username):
Utility to remove a user from the system completely.
os.chdir(findProjectRoot())
sid = username or click.prompt("Username")
eng = create_engine(config.dburl)
eng.execute("delete from auth_user where username = %s", sid)
eng.execute("delete from useinfo where sid = %s", sid)
eng.execute("delete from code where sid = %s", sid)
for t in [
"clickablearea",
"codelens",
"dragndrop",
"fitb",
"lp",
"mchoice",
"parsons",
"shortanswer",
]:
eng.execute("delete from {}_answers where sid = '{}'".format(t, sid))
@cli.command()
@click.option("--checkdb", is_flag=True, help="check state of db and databases folder")
@pass_config
def env(config, checkdb):
Print out your configured environment If –checkdb is used then env will exit with one of the following exit codes
0: no database, no database folder 1: no database but databases folder 2: database exists but no databases folder 3: both database and databases folder exist
os.chdir(findProjectRoot())
dbinit = 0
dbdir = 0
if checkdb:
count = check_db_for_useinfo(config)
if count == 0:
dbinit = 0
print("Database not initialized")
else:
dbinit = 2
print("Database is initialized")
if os.path.exists(DBSDIR):
dbdir = 1
print("Database migration folder exists")
else:
dbdir = 0
print("No Database Migration Folder")
if not checkdb or config.verbose:
echoEnviron(config)
print("Exiting with result of {}".format(dbinit | dbdir))
sys.exit(dbinit | dbdir)
@cli.command()
@click.option("--username", default=None, help="user to promote to instructor")
@click.option("--course", default=None, help="name of course")
@pass_config
def addinstructor(config, username, course):
Add an existing user as an instructor for a course
eng = create_engine(config.dburl)
username = username or click.prompt("Username")
course = course or click.prompt("Course name")
res = eng.execute("select id from auth_user where username=%s", username).first()
if res:
userid = res[0]
else:
print("Sorry, that user does not exist")
sys.exit(-1)
res = eng.execute("select id from courses where course_name=%s", course).first()
if res:
courseid = res[0]
else:
print("Sorry, that course does not exist")
sys.exit(-1)
if needed insert a row into auth_membership
res = eng.execute("select id from auth_group where role='instructor'").first()
if res:
role = res[0]
else:
print(
"Sorry, your system does not have the instructor role setup -- this is bad"
)
sys.exit(-1)
res = eng.execute(
"select * from auth_membership where user_id=%s and group_id=%s", userid, role
).first()
if not res:
eng.execute(
"insert into auth_membership (user_id, group_id) values (%s, %s)",
userid,
role,
)
print("made {} an instructor".format(username))
else:
print("{} is already an instructor".format(username))
if needed insert a row into user_courses
res = eng.execute(
"select * from user_courses where user_id=%s and course_id=%s ",
userid,
courseid,
).first()
if not res:
eng.execute(
"insert into user_courses (user_id, course_id) values (%s, %s)",
userid,
courseid,
)
print("enrolled {} in {}".format(username, course))
else:
print("{} is already enrolled in {}".format(username, course))
if needed insert a row into course_instructor
res = eng.execute(
"select * from course_instructor where instructor=%s and course=%s ",
userid,
courseid,
).first()
if not res:
eng.execute(
"insert into course_instructor (instructor, course) values (%s, %s)",
userid,
courseid,
)
print("made {} and instructor for {}".format(username, course))
else:
print("{} is already an instructor for {}".format(username, course))
@cli.command()
@click.option("--username", help="user to promote to instructor")
@click.option("--basecourse", help="name of base course")
@pass_config
def addeditor(config, username, basecourse):
Add an existing user as an instructor for a course
eng = create_engine(config.dburl)
res = eng.execute("select id from auth_user where username=%s", username).first()
if res:
userid = res[0]
else:
click.echo("Sorry, that user does not exist", color="red")
sys.exit(-1)
res = eng.execute(
"select id from courses where course_name=%s and base_course=%s",
basecourse,
basecourse,
).first()
if not res:
click.echo("Sorry, that base course does not exist", color="red")
sys.exit(-1)
if needed insert a row into auth_membership
res = eng.execute("select id from auth_group where role='editor'").first()
if res:
role = res[0]
else:
click.echo(
"Sorry, your system does not have the editor role setup -- this is bad",
color="red",
)
sys.exit(-1)
res = eng.execute(
"select * from auth_membership where user_id=%s and group_id=%s", userid, role
).first()
if not res:
eng.execute(
"insert into auth_membership (user_id, group_id) values (%s, %s)",
userid,
role,
)
click.echo("made {} an editor".format(username), color="green")
else:
click.echo("{} is already an editor".format(username), color="red")
if needed insert a row into user_courses
res = eng.execute(
"select * from editor_basecourse where editor=%s and base_course=%s ",
userid,
basecourse,
).first()
if not res:
eng.execute(
"insert into editor_basecourse (editor, base_course) values (%s, %s)",
userid,
basecourse,
)
click.echo(
"made {} an editor for {}".format(username, basecourse), color="green"
)
else:
click.echo(
"{} is already an editor for {}".format(username, basecourse), color="red"
)
@cli.command()
@click.option("--name", help="Name of the course")
@pass_config
def courseinfo(config, name):
List all information for a single course
eng = create_engine(config.dburl)
if not name:
name = click.prompt("What course do you want info about?")
course = eng.execute(
"""select id, term_start_date, institution, base_course from courses where course_name = %s""",
name,
).first()
cid = course[0]
start_date = course[1]
inst = course[2]
bc = course[3]
s_count = eng.execute(
"""select count(*) from user_courses where course_id=%s""", cid
).first()[0]
res = eng.execute(
"""select username, first_name, last_name, email, courses.course_name
from auth_user
join course_instructor ON course_instructor.instructor = auth_user.id
join courses ON courses.id = course_instructor.course
where course_instructor.course = %s
order by username;""",
cid,
)
print("Course Information for {} -- ({})".format(name, cid))
print(inst)
print("Base course: {}".format(bc))
print("Start date: {}".format(start_date))
print("Number of students: {}".format(s_count))
print("Instructors:")
for row in res:
print(" ".join(row[:-1]))
@cli.command()
@click.option("--course", default=None, help="Name of the course")
@click.option("--attr", default=None, help="Attribute to add")
@click.option("--value", default=None, help="Attribute Value")
@pass_config
def addattribute(config, course, attr, value):
Add an attribute to the course_attributes table
course = course or click.prompt("Name of the course ")
attr = attr or click.prompt("Attribute to set: ")
value = value or click.prompt(f"Value of {attr}: ")
eng = create_engine(config.dburl)
res = eng.execute("select id from courses where course_name=%s", course).first()
if res:
course_id = res[0]
else:
print("Sorry, that course does not exist")
sys.exit(-1)
try:
res = eng.execute(
f"""insert into course_attributes (course_id, attr, value)
values ({course_id}, '{attr}', '{value}')"""
)
except UniqueViolation:
click.echo(f"Can only have one attribute {attr} per course")
except IntegrityError:
click.echo(f"Can only have one attribute {attr} per course")
click.echo("Success")
@cli.command()
@click.option(
"--course", help="The name of a course that should already exist in the DB"
)
@pass_config
def instructors(config, course):
List instructor information for all courses or just for a single course
eng = create_engine(config.dburl)
where_clause = ""
if course:
where_clause = "where courses.course_name = '{}'".format(course)
res = eng.execute(
"""select username, first_name, last_name, email, courses.course_name
from auth_user
join course_instructor ON course_instructor.instructor = auth_user.id
join courses ON courses.id = course_instructor.course
{}
order by username;""".format(
where_clause
)
)
outline = ""
row = next(res)
current = row[0]
outline = "{:<10} {:<10} {:<10} {:<20} {}".format(*row)
for row in res:
if row[0] == current:
outline += " {}".format(row[-1])
else:
print(outline)
outline = "{:<10} {:<10} {:<10} {:<20} {}".format(*row)
current = row[0]
print(outline)
grade
@cli.command()
@click.option("--enforce", is_flag=True, help="Enforce deadline when grading")
@click.option(
"--course", help="The name of a course that should already exist in the DB"
)
@click.option("--pset", help="Database ID of the Problem Set")
@pass_config
def grade(config, course, pset, enforce):
Grade a problem set; hack for long-running grading processes
os.chdir(findProjectRoot())
userinfo = {}
userinfo["course"] = course if course else click.prompt("Name of course")
userinfo["pset"] = pset if pset else click.prompt("Problem Set ID")
userinfo["enforce_deadline"] = (
enforce if enforce else click.confirm("Enforce deadline?", default=True)
)
os.environ["RSM_USERINFO"] = json.dumps(userinfo)
subprocess.call(
"python web2py.py -S runestone -M -R applications/runestone/rsmanage/grade.py",
shell=True,
)
@cli.command()
@click.option("--course", help="name of course")
@pass_config
def findinstructor(config, course):
Print the PII of the instructor for a given course.
if not course:
course = click.prompt("enter the course name")
eng = create_engine(config.dburl)
query = """
select username, first_name, last_name, email
from auth_user join course_instructor on auth_user.id = instructor join courses on course = courses.id
where courses.course_name = %s order by last_name
"""
res = eng.execute(query, course)
if res:
for row in res:
print("{} {} {} {}").format(
row.first_name, row.last_name, row.email, row.username
)
else:
print("No instructors found for {}".format(course))
Utility Functions Below here
def checkEnvironment():
Check the list of required and optional environment variables to be sure they are defined.
stop = False
assert os.environ["WEB2PY_CONFIG"]
config = os.environ["WEB2PY_CONFIG"]
if config == "production":
for var in REQ_ENV:
if var not in os.environ:
stop = True
click.echo("Missing definition for {} environment variable".format(var))
elif config == "test":
if "TEST_DBURL" not in os.environ:
stop = True
click.echo("Missing definition for TEST_DBURL environment variable")
elif config == "development":
if "DEV_DBURL" not in os.environ:
stop = True
click.echo("Missing definition for DEV_DBURL environment variable")
for var in OPT_ENV:
if var not in os.environ:
click.echo("You may want to define the {} environment variable".format(var))
if stop:
sys.exit(1)
def echoEnviron(config):
click.echo("WEB2PY_CONFIG is {}".format(config.conf))
click.echo("The database URL is configured as {}".format(config.dburl))
click.echo("DBNAME is {}".format(config.dbname))
def findProjectRoot():
start = os.getcwd()
prevdir = ""
while start != prevdir:
if os.path.exists(os.path.join(start, "web2py.py")):
return start
prevdir = start
start = os.path.dirname(start)
raise IOError("You must be in a web2py application to run rsmanage")
fill_practice_log_missings
@cli.command()
@pass_config
def fill_practice_log_missings(config):
Only for one-time use to fill out the missing values of the columns that we added to user_topic_practice_log table during the semester.
os.chdir(findProjectRoot())
subprocess.call(
"python web2py.py -S runestone -M -R applications/runestone/rsmanage/fill_practice_log_missings.py",
shell=True,
)
def check_db_for_useinfo(config):
eng = create_engine(config.dburl)
res = eng.execute("select count(*) from pg_class where relname = 'useinfo'")
count = res.first()[0]
return count
if __name__ == "__main__":
cli()