making-with-code-cli 3.1.0__tar.gz → 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/PKG-INFO +2 -3
- making_with_code_cli-4.0.0/making_with_code_cli/decorators.py +19 -0
- making_with_code_cli-4.0.0/making_with_code_cli/mwc_accounts_api.py +89 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/setup/__init__.py +34 -15
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/setup/tasks.py +3 -4
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/__init__.py +4 -0
- making_with_code_cli-4.0.0/making_with_code_cli/teach/section/__init__.py +45 -0
- making_with_code_cli-4.0.0/making_with_code_cli/teach/section/create.py +88 -0
- making_with_code_cli-4.0.0/making_with_code_cli/teach/section/edit.py +41 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/setup.py +30 -14
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/status.py +12 -12
- making_with_code_cli-4.0.0/making_with_code_cli/teach/student/__init__.py +10 -0
- making_with_code_cli-4.0.0/making_with_code_cli/teach/student/create.py +33 -0
- making_with_code_cli-4.0.0/making_with_code_cli/teach/student/update.py +29 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/student_repo_functions.py +9 -9
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/student_repos.py +11 -11
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/update.py +3 -3
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/update/__init__.py +2 -2
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/pyproject.toml +5 -5
- making_with_code_cli-3.1.0/making_with_code_cli/mwc_accounts_api.py +0 -61
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/README.md +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/cli.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/curriculum.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/errors.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/git_backend/__init__.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/git_backend/base_backend.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/git_backend/mwc_backend.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/git_wrapper.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/helpers.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/settings.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/styles.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/submit.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/assess.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/check/__init__.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/check/check_module.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/gitea_api/api.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/gitea_api/exceptions.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/log.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/patch.py +0 -0
- {making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/version.py +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: making-with-code-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Courseware for Making With Code
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Chris Proctor
|
|
7
7
|
Author-email: chris@chrisproctor.net
|
|
8
|
-
Requires-Python: >=3.
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from functools import update_wrapper
|
|
2
|
+
import click
|
|
3
|
+
import sys
|
|
4
|
+
from making_with_code_cli.styles import error
|
|
5
|
+
from making_with_code_cli.errors import MWCError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def handle_mwc_errors(f):
|
|
9
|
+
"""Decorator declaring a click command.
|
|
10
|
+
Wraps execution in a try/catch block, so that MWCErrors can be handled with
|
|
11
|
+
graceful output.
|
|
12
|
+
"""
|
|
13
|
+
def command(*args, **kwargs):
|
|
14
|
+
try:
|
|
15
|
+
return f(*args, **kwargs)
|
|
16
|
+
except MWCError as e:
|
|
17
|
+
click.echo(error(str(e), preformatted=True), err=True)
|
|
18
|
+
sys.exit(1)
|
|
19
|
+
return update_wrapper(command, f)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from urllib.parse import urljoin
|
|
6
|
+
from getpass import getpass
|
|
7
|
+
from making_with_code_cli.errors import MWCError
|
|
8
|
+
|
|
9
|
+
MWC_ACCOUNTS_SERVER = "https://accounts.makingwithcode.org"
|
|
10
|
+
|
|
11
|
+
class MWCAccountsAPI:
|
|
12
|
+
def __init__(self, mwc_accounts_server=None):
|
|
13
|
+
self.mwc_accounts_server = mwc_accounts_server or MWC_ACCOUNTS_SERVER
|
|
14
|
+
|
|
15
|
+
def login(self, username, password):
|
|
16
|
+
"Authenticates with a username and password, returning an auth token"
|
|
17
|
+
data = {"username": username, "password": password}
|
|
18
|
+
response = self.post("/login", data=data)
|
|
19
|
+
return self.handle_response(response)
|
|
20
|
+
|
|
21
|
+
def logout(self, token):
|
|
22
|
+
response = self.post("/logout", token=token)
|
|
23
|
+
return self.handle_response(response)
|
|
24
|
+
|
|
25
|
+
def get_status(self, token):
|
|
26
|
+
response = self.get("/status", token=token)
|
|
27
|
+
return self.handle_response(response)
|
|
28
|
+
|
|
29
|
+
def get_roster(self, token):
|
|
30
|
+
response = self.get("/roster", token=token)
|
|
31
|
+
return self.handle_response(response)
|
|
32
|
+
|
|
33
|
+
def create_student(self, token, params):
|
|
34
|
+
response = self.post("/students", data=params, token=token)
|
|
35
|
+
return self.handle_response(response)
|
|
36
|
+
|
|
37
|
+
def update_student(self, token, params):
|
|
38
|
+
response = self.put("/students", data=params, token=token)
|
|
39
|
+
return self.handle_response(response)
|
|
40
|
+
|
|
41
|
+
def create_section(self, token, params):
|
|
42
|
+
response = self.post("/sections", data=params, token=token)
|
|
43
|
+
return self.handle_response(response)
|
|
44
|
+
|
|
45
|
+
def update_section(self, token, params):
|
|
46
|
+
response = self.put("/sections", data=params, token=token)
|
|
47
|
+
return self.handle_response(response)
|
|
48
|
+
|
|
49
|
+
def get(self, url, data=None, token=None):
|
|
50
|
+
return self.http_request("get", url, data=data, token=token)
|
|
51
|
+
|
|
52
|
+
def post(self, url, data=None, token=None):
|
|
53
|
+
return self.http_request("post", url, data=data, token=token)
|
|
54
|
+
|
|
55
|
+
def put(self, url, data=None, token=None):
|
|
56
|
+
return self.http_request("put", url, data=data, token=token)
|
|
57
|
+
|
|
58
|
+
def http_request(self, method, url, data=None, token=None):
|
|
59
|
+
fn = getattr(requests, method)
|
|
60
|
+
headers = {"Authorization": f"Token {token}"} if token else None
|
|
61
|
+
try:
|
|
62
|
+
return fn(self.mwc_accounts_server + url, data=data, headers=headers)
|
|
63
|
+
except requests.exceptions.ConnectionError:
|
|
64
|
+
raise self.ServerError("Could not connect to server")
|
|
65
|
+
|
|
66
|
+
def handle_response(self, response):
|
|
67
|
+
if response.ok:
|
|
68
|
+
return response.json()
|
|
69
|
+
elif response.status_code == 500:
|
|
70
|
+
raise self.ServerError("Error 500")
|
|
71
|
+
else:
|
|
72
|
+
try:
|
|
73
|
+
rj = response.json()
|
|
74
|
+
raise self.RequestFailed(rj, data=rj)
|
|
75
|
+
except requests.exceptions.JSONDecodeError:
|
|
76
|
+
raise self.RequestFailed(response)
|
|
77
|
+
|
|
78
|
+
class RequestFailed(MWCError):
|
|
79
|
+
def __init__(self, *args, data=None, **kwargs):
|
|
80
|
+
super().__init__(*args, **kwargs)
|
|
81
|
+
self.data = data
|
|
82
|
+
|
|
83
|
+
class ServerError(MWCError):
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/setup/__init__.py
RENAMED
|
@@ -5,6 +5,7 @@ import yaml
|
|
|
5
5
|
from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
|
|
6
6
|
from making_with_code_cli.git_backend import get_backend
|
|
7
7
|
from making_with_code_cli.update import update
|
|
8
|
+
from making_with_code_cli.decorators import handle_mwc_errors
|
|
8
9
|
from making_with_code_cli.settings import (
|
|
9
10
|
get_settings_path,
|
|
10
11
|
read_settings,
|
|
@@ -42,39 +43,57 @@ from making_with_code_cli.setup.tasks import (
|
|
|
42
43
|
@click.command()
|
|
43
44
|
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
44
45
|
@click.option("--debug", is_flag=True, help="Show debug-level output")
|
|
46
|
+
@click.option("--git-name", help="Set git name")
|
|
47
|
+
@click.option("--git-email", help="Set git email address")
|
|
48
|
+
@click.option("--mwc-accounts-url", help="Set URL for MWC accounts server")
|
|
45
49
|
@click.pass_context
|
|
46
|
-
|
|
50
|
+
@handle_mwc_errors
|
|
51
|
+
def setup(ctx, config, debug, git_name, git_email, mwc_accounts_url):
|
|
47
52
|
"""Set up the MWC command line interface"""
|
|
48
53
|
settings = read_settings(config)
|
|
54
|
+
sp = get_settings_path(config)
|
|
49
55
|
if debug:
|
|
50
|
-
sp = get_settings_path(config)
|
|
51
56
|
click.echo(debug_fmt(f"Reading settings from {sp}"))
|
|
57
|
+
if not sp.parent.exists():
|
|
58
|
+
if click.confirm(confirm(f"Directory {sp.parent} doesn't exist. Create it?")):
|
|
59
|
+
sp.parent.mkdir(parents=True)
|
|
60
|
+
else:
|
|
61
|
+
click.error(f"Could not save config file at {sp}.")
|
|
62
|
+
return
|
|
52
63
|
rc_tasks = []
|
|
53
64
|
click.echo(address(INTRO_MESSAGE))
|
|
54
65
|
for note in INTRO_NOTES:
|
|
55
66
|
click.echo(address(note, list_format=True))
|
|
56
67
|
click.echo()
|
|
68
|
+
if git_name:
|
|
69
|
+
settings['git_name'] = git_name
|
|
70
|
+
if git_email:
|
|
71
|
+
settings['git_email'] = git_email
|
|
72
|
+
if mwc_accounts_url:
|
|
73
|
+
settings['mwc_accounts_url'] = mwc_accounts_url
|
|
57
74
|
settings['mwc_username'] = choose_mwc_username(settings.get("mwc_username"))
|
|
58
|
-
api = MWCAccountsAPI()
|
|
59
|
-
if
|
|
75
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
76
|
+
if not 'mwc_accounts_token' in settings:
|
|
77
|
+
token = prompt_mwc_password(settings['mwc_username'], api)
|
|
78
|
+
settings['mwc_accounts_token'] = token
|
|
79
|
+
while True:
|
|
60
80
|
try:
|
|
61
81
|
status = api.get_status(settings['mwc_accounts_token'])
|
|
82
|
+
break
|
|
62
83
|
except api.RequestFailed as bad_token:
|
|
63
|
-
|
|
84
|
+
click.echo(error("Sorry, there was an error logging in."))
|
|
85
|
+
if debug:
|
|
86
|
+
click.echo(debug_fmt(bad_token))
|
|
87
|
+
token = prompt_mwc_password(settings['mwc_username'], api)
|
|
64
88
|
settings['mwc_accounts_token'] = token
|
|
65
|
-
status = api.get_status(token)
|
|
66
|
-
else:
|
|
67
|
-
token = prompt_mwc_password(settings['mwc_username'])
|
|
68
|
-
settings['mwc_accounts_token'] = token
|
|
69
|
-
status = api.get_status(token)
|
|
70
|
-
if debug:
|
|
71
|
-
click.echo(debug_fmt("MWC Accounts Server status:"))
|
|
72
|
-
click.echo(debug_fmt(str(status)))
|
|
73
89
|
settings['mwc_git_token'] = status['git_token']
|
|
74
90
|
settings['work_dir'] = str(choose_work_dir(settings.get("work_dir")).resolve())
|
|
75
|
-
|
|
76
|
-
settings['editor'] = choose_editor(settings.get('editor', 'code'))
|
|
91
|
+
settings['editor'] = choose_editor(settings.get('editor', 'code'))
|
|
77
92
|
if debug:
|
|
93
|
+
if settings.get('mwc_accounts_url'):
|
|
94
|
+
click.echo(debug_fmt(f"Using custom MWC accounts server: {settings['mwc_accounts_url']}"))
|
|
95
|
+
click.echo(debug_fmt("MWC Accounts Server status:"))
|
|
96
|
+
click.echo(debug_fmt(str(status)))
|
|
78
97
|
click.echo(info("MWC settings:"))
|
|
79
98
|
click.echo(info(yaml.dump(settings), preformatted=True))
|
|
80
99
|
write_settings(settings, config)
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/setup/tasks.py
RENAMED
|
@@ -94,9 +94,8 @@ def choose_mwc_username(default=None):
|
|
|
94
94
|
default=default
|
|
95
95
|
)
|
|
96
96
|
|
|
97
|
-
def prompt_mwc_password(username):
|
|
97
|
+
def prompt_mwc_password(username, api):
|
|
98
98
|
"Asks for the password. Returns a token when successful."
|
|
99
|
-
api = MWCAccountsAPI()
|
|
100
99
|
while True:
|
|
101
100
|
password = click.prompt(question("What is your MWC password?"), hide_input=True)
|
|
102
101
|
try:
|
|
@@ -464,8 +463,8 @@ class GitConfiguration(SetupTask):
|
|
|
464
463
|
git_config["core.editor"] = self.editorcmds[self.settings.get('editor')]
|
|
465
464
|
if self.settings.get('git_name'):
|
|
466
465
|
git_config["user.name"] = self.settings['git_name']
|
|
467
|
-
if self.settings.get('
|
|
468
|
-
git_config["user.email"] = self.settings['
|
|
466
|
+
if self.settings.get('git_email'):
|
|
467
|
+
git_config["user.email"] = self.settings['git_email']
|
|
469
468
|
return git_config
|
|
470
469
|
|
|
471
470
|
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/__init__.py
RENAMED
|
@@ -5,6 +5,8 @@ from making_with_code_cli.teach.status import status
|
|
|
5
5
|
from making_with_code_cli.teach.log import log
|
|
6
6
|
from making_with_code_cli.teach.patch import patch
|
|
7
7
|
from making_with_code_cli.teach.check import check
|
|
8
|
+
from making_with_code_cli.teach.section import section
|
|
9
|
+
from making_with_code_cli.teach.student import student
|
|
8
10
|
|
|
9
11
|
@click.group()
|
|
10
12
|
def teach():
|
|
@@ -16,3 +18,5 @@ teach.add_command(status)
|
|
|
16
18
|
teach.add_command(log)
|
|
17
19
|
teach.add_command(patch)
|
|
18
20
|
teach.add_command(check)
|
|
21
|
+
teach.add_command(section)
|
|
22
|
+
teach.add_command(student)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from csv import DictWriter
|
|
3
|
+
from tabulate import tabulate
|
|
4
|
+
from making_with_code_cli.settings import read_settings
|
|
5
|
+
from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
|
|
6
|
+
from making_with_code_cli.styles import (
|
|
7
|
+
info,
|
|
8
|
+
)
|
|
9
|
+
from making_with_code_cli.teach.setup import check_required_teacher_settings
|
|
10
|
+
from making_with_code_cli.teach.section.create import create_section
|
|
11
|
+
from making_with_code_cli.teach.section.edit import edit_section
|
|
12
|
+
|
|
13
|
+
@click.group(invoke_without_command=True)
|
|
14
|
+
@click.pass_context
|
|
15
|
+
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
16
|
+
@click.option("-o", "--outfile", help="Save results as csv")
|
|
17
|
+
def section(ctx, config, outfile):
|
|
18
|
+
"Manage sections of students"
|
|
19
|
+
if ctx.invoked_subcommand is None:
|
|
20
|
+
settings = read_settings(config)
|
|
21
|
+
if not check_required_teacher_settings(settings):
|
|
22
|
+
return
|
|
23
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
24
|
+
roster = api.get_roster(settings['mwc_accounts_token'])
|
|
25
|
+
if roster['teacher_sections']:
|
|
26
|
+
data = summarize_sections(roster['teacher_sections'])
|
|
27
|
+
if outfile:
|
|
28
|
+
with open(outfile, "w") as fh:
|
|
29
|
+
writer = DictWriter(fh, data[0].keys())
|
|
30
|
+
writer.writeheader()
|
|
31
|
+
writer.writerows(data)
|
|
32
|
+
else:
|
|
33
|
+
click.echo(info(tabulate(data, headers="keys"), preformatted=True))
|
|
34
|
+
else:
|
|
35
|
+
click.echo(info("You have no sections."))
|
|
36
|
+
|
|
37
|
+
def summarize_sections(section_data):
|
|
38
|
+
for section in section_data:
|
|
39
|
+
section["students"] = len(section["student_tokens"])
|
|
40
|
+
del section["student_tokens"]
|
|
41
|
+
return section_data
|
|
42
|
+
|
|
43
|
+
section.add_command(create_section)
|
|
44
|
+
section.add_command(edit_section)
|
|
45
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from tabulate import tabulate
|
|
3
|
+
from making_with_code_cli.settings import read_settings
|
|
4
|
+
from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
|
|
5
|
+
from making_with_code_cli.curriculum import get_curriculum
|
|
6
|
+
from making_with_code_cli.errors import CurriculumSiteNotAvailable, MWCError
|
|
7
|
+
from making_with_code_cli.styles import (
|
|
8
|
+
address,
|
|
9
|
+
error,
|
|
10
|
+
info,
|
|
11
|
+
question,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
MWC_CURRICULUM_URL = "https://makingwithcode.org"
|
|
15
|
+
|
|
16
|
+
@click.command('create')
|
|
17
|
+
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
18
|
+
@click.option("--slug", help="Unique short identifier for the section")
|
|
19
|
+
@click.option("--name", help="Section name")
|
|
20
|
+
@click.option("--curriculum-site-url", help="URL for curriculum website. e.g. https://makingwithcode.org")
|
|
21
|
+
@click.option("--course-name", help="MWC course name")
|
|
22
|
+
@click.option("--code", help="Code students can use to join the section")
|
|
23
|
+
def create_section(config, slug, name, curriculum_site_url, course_name, code):
|
|
24
|
+
"Create a new student section"
|
|
25
|
+
settings = read_settings(config)
|
|
26
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
27
|
+
params = {
|
|
28
|
+
"name": name,
|
|
29
|
+
"slug": slug,
|
|
30
|
+
"curriculum_site_url": curriculum_site_url,
|
|
31
|
+
"course_name": course_name,
|
|
32
|
+
"code": code,
|
|
33
|
+
}
|
|
34
|
+
if not params_complete(params):
|
|
35
|
+
params = prompt_for_params(params)
|
|
36
|
+
try:
|
|
37
|
+
response = api.create_section(settings.get('mwc_accounts_token'), params)
|
|
38
|
+
click.echo(info(tabulate([response], headers="keys"), preformatted=True))
|
|
39
|
+
except api.RequestFailed as err:
|
|
40
|
+
click.echo(error("Could not create new section:"))
|
|
41
|
+
for field, problems in err.data.items():
|
|
42
|
+
click.echo(error(f" - {field}: {'; '.join(problems)}"))
|
|
43
|
+
|
|
44
|
+
def params_complete(params):
|
|
45
|
+
"""Check whether all params are present.
|
|
46
|
+
"""
|
|
47
|
+
required_params = ["name", "slug", "curriculum_site_url", "course_name", "code"]
|
|
48
|
+
if all(params.get(p) for p in required_params):
|
|
49
|
+
curriculum = get_curriculum(params["curriculum_site_url"])
|
|
50
|
+
courses = [c["name"] for c in curriculum.get("courses", [])]
|
|
51
|
+
if params["course_name"] in courses:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def prompt_for_params(params):
|
|
56
|
+
"""Interactively prompt user for missing params.
|
|
57
|
+
"""
|
|
58
|
+
params['name'] = click.prompt(question("Section name"), default=params.get('name'))
|
|
59
|
+
params['slug'] = click.prompt(question("Section slug"), default=params.get('slug'))
|
|
60
|
+
while True:
|
|
61
|
+
url = click.prompt(
|
|
62
|
+
question("Curriculum site URL"),
|
|
63
|
+
default=params.get('curriculum_site_url') or MWC_CURRICULUM_URL
|
|
64
|
+
)
|
|
65
|
+
try:
|
|
66
|
+
curriculum = get_curriculum(url)
|
|
67
|
+
params['curriculum_site_url'] = url
|
|
68
|
+
if not curriculum.get('courses'):
|
|
69
|
+
raise MWCError("Found the curriculum site, but no courses are published.")
|
|
70
|
+
break
|
|
71
|
+
except CurriculumSiteNotAvailable as err:
|
|
72
|
+
click.echo(error(err))
|
|
73
|
+
course_names = [c['name'] for c in curriculum['courses']]
|
|
74
|
+
if params.get('course_name') and params.get('course_name') not in course_names:
|
|
75
|
+
click.echo(info(f"Course name {params['course_name']} is invalid."))
|
|
76
|
+
del params['course_name']
|
|
77
|
+
if not params.get('course_name'):
|
|
78
|
+
click.echo(info(f"The following courses are published at {params['curriculum_site_url']}:"))
|
|
79
|
+
for name in course_names:
|
|
80
|
+
click.echo(info(f" - {name}"))
|
|
81
|
+
params['course_name'] = click.prompt(
|
|
82
|
+
question("Course name"),
|
|
83
|
+
type=click.Choice(course_names),
|
|
84
|
+
default=params.get('course_name')
|
|
85
|
+
)
|
|
86
|
+
params['code'] = click.prompt(question("Join code"), default=params.get("code"))
|
|
87
|
+
return params
|
|
88
|
+
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from tabulate import tabulate
|
|
3
|
+
from making_with_code_cli.settings import read_settings
|
|
4
|
+
from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
|
|
5
|
+
from making_with_code_cli.curriculum import get_curriculum
|
|
6
|
+
from making_with_code_cli.errors import CurriculumSiteNotAvailable, MWCError
|
|
7
|
+
from making_with_code_cli.styles import (
|
|
8
|
+
address,
|
|
9
|
+
error,
|
|
10
|
+
info,
|
|
11
|
+
question,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
@click.command('edit')
|
|
15
|
+
@click.argument("slug")
|
|
16
|
+
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
17
|
+
@click.option("--name", help="Section name")
|
|
18
|
+
@click.option("--curriculum-site-url", help="URL for curriculum website. e.g. https://makingwithcode.org")
|
|
19
|
+
@click.option("--course-name", help="MWC course name")
|
|
20
|
+
@click.option("--code", help="Code students can use to join the section")
|
|
21
|
+
@click.option("--roster", help="csv file containing student information")
|
|
22
|
+
def edit_section(config, slug, name, curriculum_site_url, course_name, code, roster):
|
|
23
|
+
"Edit an existing section"
|
|
24
|
+
settings = read_settings(config)
|
|
25
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
26
|
+
params = {"slug": slug}
|
|
27
|
+
if name:
|
|
28
|
+
params["name"] = name
|
|
29
|
+
if curriculum_site_url:
|
|
30
|
+
params["curriculum_site_url"] = curriculum_site_url
|
|
31
|
+
if course_name:
|
|
32
|
+
params["course_name"] = course_name
|
|
33
|
+
if code:
|
|
34
|
+
params["code"] = code
|
|
35
|
+
try:
|
|
36
|
+
response = api.update_section(settings.get('mwc_accounts_token'), params)
|
|
37
|
+
click.echo(info(tabulate([response], headers="keys"), preformatted=True))
|
|
38
|
+
except api.RequestFailed as err:
|
|
39
|
+
click.echo(error(f"Could not edit section {params['slug']}:"))
|
|
40
|
+
for field, problems in err.data.items():
|
|
41
|
+
click.echo(error(f" - {field}: {'; '.join(problems)}"))
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/setup.py
RENAMED
|
@@ -8,6 +8,7 @@ from making_with_code_cli.setup.tasks import (
|
|
|
8
8
|
choose_mwc_username,
|
|
9
9
|
prompt_mwc_password,
|
|
10
10
|
choose_work_dir,
|
|
11
|
+
choose_editor,
|
|
11
12
|
)
|
|
12
13
|
from making_with_code_cli.settings import (
|
|
13
14
|
get_settings_path,
|
|
@@ -45,33 +46,48 @@ def check_required_teacher_settings(settings):
|
|
|
45
46
|
@click.command
|
|
46
47
|
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
47
48
|
@click.option("--debug", is_flag=True, help="Show debug-level output")
|
|
48
|
-
|
|
49
|
+
@click.option("--git-name", help="Set git name")
|
|
50
|
+
@click.option("--git-email", help="Set git email address")
|
|
51
|
+
@click.option("--mwc-accounts-url", help="Set URL for MWC accounts server")
|
|
52
|
+
def setup(config, debug, git_name, git_email, mwc_accounts_url):
|
|
49
53
|
"""Configure teacher settings"""
|
|
50
|
-
click.echo(address(INTRO_MESSAGE))
|
|
51
|
-
click.echo()
|
|
52
54
|
settings = read_settings(config)
|
|
53
55
|
if debug:
|
|
54
56
|
sp = get_settings_path(config)
|
|
55
57
|
click.echo(debug_fmt(f"Reading settings from {sp}"))
|
|
58
|
+
click.echo(address(INTRO_MESSAGE))
|
|
59
|
+
click.echo()
|
|
60
|
+
if git_name:
|
|
61
|
+
settings['git_name'] = git_name
|
|
62
|
+
if git_email:
|
|
63
|
+
settings['git_email'] = git_email
|
|
64
|
+
if mwc_accounts_url:
|
|
65
|
+
settings['mwc_accounts_url'] = mwc_accounts_url
|
|
56
66
|
settings['mwc_username'] = choose_mwc_username(settings.get("mwc_username"))
|
|
57
|
-
api = MWCAccountsAPI()
|
|
58
|
-
if
|
|
67
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
68
|
+
if not 'mwc_accounts_token' in settings:
|
|
69
|
+
token = prompt_mwc_password(settings['mwc_username'], api)
|
|
70
|
+
settings['mwc_accounts_token'] = token
|
|
71
|
+
while True:
|
|
59
72
|
try:
|
|
60
73
|
status = api.get_status(settings['mwc_accounts_token'])
|
|
74
|
+
break
|
|
61
75
|
except api.RequestFailed as bad_token:
|
|
62
|
-
|
|
76
|
+
if debug:
|
|
77
|
+
click.echo(debug_fmt(bad_token))
|
|
78
|
+
token = prompt_mwc_password(settings['mwc_username'], api)
|
|
63
79
|
settings['mwc_accounts_token'] = token
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
settings['mwc_accounts_token'] = token
|
|
68
|
-
status = api.get_status(token)
|
|
69
|
-
if debug:
|
|
70
|
-
click.echo(debug_fmt("MWC Accounts Server status:"))
|
|
71
|
-
click.echo(debug_fmt(str(status)))
|
|
80
|
+
settings['mwc_git_token'] = status['git_token']
|
|
81
|
+
settings['work_dir'] = str(choose_work_dir(settings.get("work_dir")).resolve())
|
|
82
|
+
settings['editor'] = choose_editor(settings.get('editor', 'code'))
|
|
72
83
|
settings['teacher_work_dir'] = str(choose_work_dir(
|
|
73
84
|
settings.get("teacher_work_dir"),
|
|
74
85
|
teacher=True
|
|
75
86
|
))
|
|
87
|
+
if debug:
|
|
88
|
+
if settings.get('mwc_accounts_url'):
|
|
89
|
+
click.echo(debug_fmt(f"Using custom MWC accounts server: {settings['mwc_accounts_url']}"))
|
|
90
|
+
click.echo(debug_fmt("MWC Accounts Server status:"))
|
|
91
|
+
click.echo(debug_fmt(str(status)))
|
|
76
92
|
write_settings(settings, config)
|
|
77
93
|
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/status.py
RENAMED
|
@@ -29,7 +29,7 @@ measures = {
|
|
|
29
29
|
|
|
30
30
|
@click.command()
|
|
31
31
|
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
32
|
-
@click.option("-
|
|
32
|
+
@click.option("-s", "--section", help="Filter by section name/slug")
|
|
33
33
|
@click.option("-c", "--course", help="Filter by course name")
|
|
34
34
|
@click.option("-u", "--user", help="Filter by username")
|
|
35
35
|
@click.option("-n", "--unit", help="Filter by unit name/slug")
|
|
@@ -37,37 +37,37 @@ measures = {
|
|
|
37
37
|
@click.option("-v", "--measure", default="commits", type=click.Choice(measures.keys()),
|
|
38
38
|
help="Measure to show")
|
|
39
39
|
@click.option("-a", "--anonymous", is_flag=True, help="Hide usernames")
|
|
40
|
-
@click.option("-
|
|
40
|
+
@click.option("-x", "--short", is_flag=True, help="Show short module names")
|
|
41
41
|
@click.option("-o", "--outfile", help="Save results as csv")
|
|
42
42
|
@click.option('-U', "--update", is_flag=True, help="Update repos first")
|
|
43
43
|
@click.option('-t', "--threads", type=int, default=8, help="Maximum simultaneous threads")
|
|
44
|
-
def status(config,
|
|
44
|
+
def status(config, section, course, user, unit, module, measure, anonymous, short,
|
|
45
45
|
outfile, update, threads):
|
|
46
46
|
"Show status of student repos by module"
|
|
47
47
|
if update:
|
|
48
48
|
from making_with_code_cli.teach.update import update as update_task
|
|
49
|
-
update_task.callback(config,
|
|
49
|
+
update_task.callback(config, section, course, user, unit, module, threads)
|
|
50
50
|
settings = read_settings(config)
|
|
51
51
|
if not check_required_teacher_settings(settings):
|
|
52
52
|
return
|
|
53
53
|
repos = StudentRepos(settings, threads)
|
|
54
54
|
measure_status_message, measure_fn = measures[measure]
|
|
55
|
-
results = repos.apply(measure_fn,
|
|
55
|
+
results = repos.apply(measure_fn, section=section, course=course, user=user,
|
|
56
56
|
unit=unit, module=module, status_message=measure_status_message)
|
|
57
57
|
recursive_dict = lambda: defaultdict(recursive_dict)
|
|
58
58
|
results_dict = recursive_dict()
|
|
59
59
|
for r in results:
|
|
60
|
-
|
|
61
|
-
results_dict[
|
|
60
|
+
s = r['section']['name'] + ' | ' + r['section']['course_name']
|
|
61
|
+
results_dict[s][r['username']][r['module']] = r['score']
|
|
62
62
|
all_scores = []
|
|
63
|
-
for
|
|
63
|
+
for section, users in results_dict.items():
|
|
64
64
|
if users:
|
|
65
65
|
headers = ['username'] + sorted(next(iter(users.values())).keys())
|
|
66
|
-
|
|
67
|
-
all_scores +=
|
|
66
|
+
section_scores = format_user_scores(users, anonymous, short)
|
|
67
|
+
all_scores += section_scores
|
|
68
68
|
if not outfile:
|
|
69
|
-
click.echo(address(
|
|
70
|
-
click.echo(address(tabulate(
|
|
69
|
+
click.echo(address(section))
|
|
70
|
+
click.echo(address(tabulate(section_scores, headers='keys'),
|
|
71
71
|
preformatted=True))
|
|
72
72
|
if outfile and all_scores:
|
|
73
73
|
with open(outfile, 'w') as fh:
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from making_with_code_cli.teach.student.create import create_student
|
|
3
|
+
from making_with_code_cli.teach.student.update import update_student
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
def student():
|
|
7
|
+
"Manage students"
|
|
8
|
+
|
|
9
|
+
student.add_command(create_student)
|
|
10
|
+
student.add_command(update_student)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from tabulate import tabulate
|
|
3
|
+
from making_with_code_cli.settings import read_settings
|
|
4
|
+
from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
|
|
5
|
+
from making_with_code_cli.styles import (
|
|
6
|
+
info,
|
|
7
|
+
error,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
@click.command("create")
|
|
11
|
+
@click.argument("username")
|
|
12
|
+
@click.argument("password")
|
|
13
|
+
@click.argument("section")
|
|
14
|
+
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
15
|
+
def create_student(username, password, section, config):
|
|
16
|
+
"Create a student user"
|
|
17
|
+
settings = read_settings(config)
|
|
18
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
19
|
+
params = {
|
|
20
|
+
"username": username,
|
|
21
|
+
"password": password,
|
|
22
|
+
"section": section,
|
|
23
|
+
}
|
|
24
|
+
try:
|
|
25
|
+
response = api.create_student(settings.get('mwc_accounts_token'), params)
|
|
26
|
+
click.echo(info(f"Created student {response['username']} in {section}."))
|
|
27
|
+
except api.RequestFailed as err:
|
|
28
|
+
click.echo(error(f"Could not create {params['username']}:"))
|
|
29
|
+
print(err)
|
|
30
|
+
for field, problems in err.data.items():
|
|
31
|
+
click.echo(error(f" - {field}: {'; '.join(problems)}"))
|
|
32
|
+
|
|
33
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from tabulate import tabulate
|
|
3
|
+
from making_with_code_cli.settings import read_settings
|
|
4
|
+
from making_with_code_cli.mwc_accounts_api import MWCAccountsAPI
|
|
5
|
+
from making_with_code_cli.styles import (
|
|
6
|
+
info,
|
|
7
|
+
error,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
@click.command("update")
|
|
11
|
+
@click.argument("username")
|
|
12
|
+
@click.argument("password")
|
|
13
|
+
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
14
|
+
def update_student(username, password, config):
|
|
15
|
+
"Update a student's password"
|
|
16
|
+
settings = read_settings(config)
|
|
17
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
18
|
+
params = {
|
|
19
|
+
"username": username,
|
|
20
|
+
"password": password,
|
|
21
|
+
}
|
|
22
|
+
try:
|
|
23
|
+
response = api.update_student(settings.get('mwc_accounts_token'), params)
|
|
24
|
+
click.echo(info(f"Updated student {response['username']}."))
|
|
25
|
+
except api.RequestFailed as err:
|
|
26
|
+
click.echo(error(f"Could not update {params['username']}:"))
|
|
27
|
+
for field, problems in err.data.items():
|
|
28
|
+
click.echo(error(f" - {field}: {'; '.join(problems)}"))
|
|
29
|
+
|
|
@@ -9,7 +9,7 @@ MWC_GIT_SERVER = "git.makingwithcode.org"
|
|
|
9
9
|
def run_in_repo(command, cwd):
|
|
10
10
|
return run(command, cwd=cwd, shell=True, capture_output=True, text=True)
|
|
11
11
|
|
|
12
|
-
def update_repo(semaphore, results,
|
|
12
|
+
def update_repo(semaphore, results, section, username, path, token):
|
|
13
13
|
semaphore.acquire()
|
|
14
14
|
git_api = GiteaTeacherApi()
|
|
15
15
|
if path.exists():
|
|
@@ -25,20 +25,20 @@ def update_repo(semaphore, results, group, username, path, token):
|
|
|
25
25
|
results.append({"action": "clone", "path": path, "process": process})
|
|
26
26
|
semaphore.release()
|
|
27
27
|
|
|
28
|
-
def count_commits(semaphore, results,
|
|
28
|
+
def count_commits(semaphore, results, section, username, path, token):
|
|
29
29
|
"Counts commits in repo"
|
|
30
30
|
semaphore.acquire()
|
|
31
31
|
if path.exists():
|
|
32
32
|
repo = Repo(path)
|
|
33
33
|
results.append({
|
|
34
|
-
"
|
|
34
|
+
"section": section,
|
|
35
35
|
"username": username,
|
|
36
36
|
"module": path.name,
|
|
37
37
|
"score": len(list(repo.iter_commits())) - 1
|
|
38
38
|
})
|
|
39
39
|
semaphore.release()
|
|
40
40
|
|
|
41
|
-
def count_changed_py_lines(semaphore, results,
|
|
41
|
+
def count_changed_py_lines(semaphore, results, section, username, path, token):
|
|
42
42
|
"Counts lines changed in Python files across all commits in repo"
|
|
43
43
|
semaphore.acquire()
|
|
44
44
|
if path.exists():
|
|
@@ -51,14 +51,14 @@ def count_changed_py_lines(semaphore, results, group, username, path, token):
|
|
|
51
51
|
if f.endswith(".py"):
|
|
52
52
|
changed_lines += stats['lines']
|
|
53
53
|
results.append({
|
|
54
|
-
"
|
|
54
|
+
"section": section,
|
|
55
55
|
"username": username,
|
|
56
56
|
"module": path.name,
|
|
57
57
|
"score": changed_lines
|
|
58
58
|
})
|
|
59
59
|
semaphore.release()
|
|
60
60
|
|
|
61
|
-
def count_changed_md_lines(semaphore, results,
|
|
61
|
+
def count_changed_md_lines(semaphore, results, section, username, path, token):
|
|
62
62
|
"Counts lines changed in Python files across all commits in repo"
|
|
63
63
|
semaphore.acquire()
|
|
64
64
|
if path.exists():
|
|
@@ -71,19 +71,19 @@ def count_changed_md_lines(semaphore, results, group, username, path, token):
|
|
|
71
71
|
if f.endswith(".md"):
|
|
72
72
|
changed_lines += stats['lines']
|
|
73
73
|
results.append({
|
|
74
|
-
"
|
|
74
|
+
"section": section,
|
|
75
75
|
"username": username,
|
|
76
76
|
"module": path.name,
|
|
77
77
|
"score": changed_lines
|
|
78
78
|
})
|
|
79
79
|
semaphore.release()
|
|
80
80
|
|
|
81
|
-
def module_completion(semaphore, results,
|
|
81
|
+
def module_completion(semaphore, results, section, username, path, token):
|
|
82
82
|
"Returns a [0..1] ratio of module completion, based on tests."
|
|
83
83
|
semaphore.acquire()
|
|
84
84
|
if path.exists():
|
|
85
85
|
results.append({
|
|
86
|
-
"
|
|
86
|
+
"section": section,
|
|
87
87
|
"username": username,
|
|
88
88
|
"module": path.name,
|
|
89
89
|
"score": 0
|
|
@@ -13,7 +13,7 @@ class StudentRepos:
|
|
|
13
13
|
self.settings = settings
|
|
14
14
|
self.max_threads = max_threads
|
|
15
15
|
|
|
16
|
-
def apply(self, function,
|
|
16
|
+
def apply(self, function, section=None, course=None, user=None,
|
|
17
17
|
unit=None, module=None, status_message=""):
|
|
18
18
|
"""Applies function to repo paths matching args, and returns the result.
|
|
19
19
|
The function should take (sem, result, g, user, path, token).
|
|
@@ -22,7 +22,7 @@ class StudentRepos:
|
|
|
22
22
|
sem = Semaphore(self.max_threads)
|
|
23
23
|
results = []
|
|
24
24
|
threads = []
|
|
25
|
-
for g, user, path, token in self.iter_repos(
|
|
25
|
+
for g, user, path, token in self.iter_repos(section, course, user, unit, module):
|
|
26
26
|
thread = Thread(target=function, args=(sem, results, g, user, path, token))
|
|
27
27
|
thread.start()
|
|
28
28
|
threads.append(thread)
|
|
@@ -32,17 +32,17 @@ class StudentRepos:
|
|
|
32
32
|
print()
|
|
33
33
|
return results
|
|
34
34
|
|
|
35
|
-
def iter_repos(self,
|
|
35
|
+
def iter_repos(self, section=None, course=None, user=None, unit=None, module=None):
|
|
36
36
|
root = Path(self.settings['teacher_work_dir']).resolve()
|
|
37
|
-
api = MWCAccountsAPI()
|
|
37
|
+
api = MWCAccountsAPI(self.settings.get('mwc_accounts_url'))
|
|
38
38
|
roster = api.get_roster(self.settings['mwc_accounts_token'])
|
|
39
|
-
for
|
|
40
|
-
if
|
|
39
|
+
for s in roster['teacher_sections']:
|
|
40
|
+
if section and (section not in s['name']) and (section not in s['slug']):
|
|
41
41
|
continue
|
|
42
|
-
if course and course not in
|
|
42
|
+
if course and course not in s['course_name']:
|
|
43
43
|
continue
|
|
44
|
-
curriculum = get_curriculum(
|
|
45
|
-
for username, token in
|
|
44
|
+
curriculum = get_curriculum(s['curriculum_site_url'], s['course_name'])
|
|
45
|
+
for username, token in s['student_tokens'].items():
|
|
46
46
|
if user and user not in username:
|
|
47
47
|
continue
|
|
48
48
|
for u in curriculum['units']:
|
|
@@ -51,6 +51,6 @@ class StudentRepos:
|
|
|
51
51
|
for m in u['modules']:
|
|
52
52
|
if module and (module not in m['name']) and (module not in m['slug']):
|
|
53
53
|
continue
|
|
54
|
-
path = root /
|
|
55
|
-
yield
|
|
54
|
+
path = root / s['slug'] / username / u['slug'] / m['slug']
|
|
55
|
+
yield s, username, path, token
|
|
56
56
|
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/update.py
RENAMED
|
@@ -26,19 +26,19 @@ from making_with_code_cli.styles import (
|
|
|
26
26
|
|
|
27
27
|
@click.command()
|
|
28
28
|
@click.option("--config", help="Path to config file (default: ~/.mwc)")
|
|
29
|
-
@click.option("--
|
|
29
|
+
@click.option("--section", help="Filter by section slug")
|
|
30
30
|
@click.option("--course", help="Filter by course name")
|
|
31
31
|
@click.option("--user", help="Filter by username")
|
|
32
32
|
@click.option("--unit", help="Filter by unit name/slug")
|
|
33
33
|
@click.option("--module", help="Filter by module name/slug")
|
|
34
34
|
@click.option("--threads", type=int, default=8, help="Maximum simultaneous threads")
|
|
35
|
-
def update(config,
|
|
35
|
+
def update(config, section, course, user, unit, module, threads):
|
|
36
36
|
"Update student repos"
|
|
37
37
|
settings = read_settings(config)
|
|
38
38
|
if not check_required_teacher_settings(settings):
|
|
39
39
|
return
|
|
40
40
|
repos = StudentRepos(settings, threads)
|
|
41
|
-
results = repos.apply(update_repo,
|
|
41
|
+
results = repos.apply(update_repo, section=section, course=course, user=user,
|
|
42
42
|
unit=unit, module=module, status_message="Updating repos")
|
|
43
43
|
for result in results:
|
|
44
44
|
if result['process'].returncode != 0:
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/update/__init__.py
RENAMED
|
@@ -26,13 +26,13 @@ def update(config):
|
|
|
26
26
|
mwc_home = Path(settings["work_dir"])
|
|
27
27
|
if not mwc_home.exists():
|
|
28
28
|
mwc_home.mkdir(mode=WORK_DIR_PERMISSIONS, parents=True)
|
|
29
|
-
api = MWCAccountsAPI()
|
|
29
|
+
api = MWCAccountsAPI(settings.get('mwc_accounts_url'))
|
|
30
30
|
try:
|
|
31
31
|
status = api.get_status(settings.get('mwc_accounts_token'))
|
|
32
32
|
except api.RequestFailed as bad_token:
|
|
33
33
|
click.echo(error(f"Error logging into MWC accounts server. Please run mwc setup."))
|
|
34
34
|
return
|
|
35
|
-
for course in status['
|
|
35
|
+
for course in status['student_section_memberships']:
|
|
36
36
|
curr = get_curriculum(course['curriculum_site_url'], course['course_name'])
|
|
37
37
|
git_backend = get_backend(curr['git_backend'])(settings)
|
|
38
38
|
course_dir = mwc_home / curr['slug']
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "making-with-code-cli"
|
|
3
|
-
version = "
|
|
3
|
+
version = "4.0.0"
|
|
4
4
|
description = "Courseware for Making With Code"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Chris Proctor",email = "chris@chrisproctor.net"}
|
|
@@ -8,7 +8,7 @@ authors = [
|
|
|
8
8
|
license = {text = "MIT"}
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
homepage = "https://github.com/cproctor/making-with-code-courseware"
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.11,<4.0"
|
|
12
12
|
dependencies = [
|
|
13
13
|
"pyyaml (>=6.0.2,<7.0.0)",
|
|
14
14
|
"click (>=8.1.8,<9.0.0)",
|
|
@@ -17,7 +17,7 @@ dependencies = [
|
|
|
17
17
|
"tabulate (>=0.9.0,<0.10.0)",
|
|
18
18
|
"gitpython (>=3.1.44,<4.0.0)",
|
|
19
19
|
"dateparser (>=1.2.0,<2.0.0)",
|
|
20
|
-
"tqdm (>=4.67.1,<5.0.0)"
|
|
20
|
+
"tqdm (>=4.67.1,<5.0.0)",
|
|
21
21
|
]
|
|
22
22
|
|
|
23
23
|
[project.urls]
|
|
@@ -34,5 +34,5 @@ build-backend = "poetry.core.masonry.api"
|
|
|
34
34
|
optional = true
|
|
35
35
|
|
|
36
36
|
[tool.poetry.group.docs.dependencies]
|
|
37
|
-
sphinx = "^
|
|
38
|
-
sphinx-rtd-theme = "^
|
|
37
|
+
sphinx = "^8.2.3"
|
|
38
|
+
sphinx-rtd-theme = "^3.0.2"
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
from urllib.parse import urljoin
|
|
6
|
-
from getpass import getpass
|
|
7
|
-
|
|
8
|
-
LOCALHOST = "http://localhost:8000"
|
|
9
|
-
MWC_ACCOUNTS_SERVER = "https://accounts.makingwithcode.org"
|
|
10
|
-
|
|
11
|
-
class MWCAccountsAPI:
|
|
12
|
-
def __init__(self, mwc_accounts_server=MWC_ACCOUNTS_SERVER):
|
|
13
|
-
self.mwc_accounts_server = mwc_accounts_server
|
|
14
|
-
|
|
15
|
-
def login(self, username, password):
|
|
16
|
-
"Authenticates with a username and password, returning an auth token"
|
|
17
|
-
url = self.mwc_accounts_server + "/login"
|
|
18
|
-
data = {"username": username, "password": password}
|
|
19
|
-
response = requests.post(url, data=data)
|
|
20
|
-
return self.handle_response(response)
|
|
21
|
-
|
|
22
|
-
def logout(self, token):
|
|
23
|
-
url = self.mwc_accounts_server + "/logout"
|
|
24
|
-
headers = {"Authorization": f"Token {token}"}
|
|
25
|
-
response = requests.post(url, headers=headers)
|
|
26
|
-
return self.handle_response(response)
|
|
27
|
-
|
|
28
|
-
def get_status(self, token):
|
|
29
|
-
url = self.mwc_accounts_server + "/status"
|
|
30
|
-
headers = {"Authorization": f"Token {token}"}
|
|
31
|
-
response = requests.get(url, headers=headers)
|
|
32
|
-
return self.handle_response(response)
|
|
33
|
-
|
|
34
|
-
def get_roster(self, token):
|
|
35
|
-
url = self.mwc_accounts_server + "/roster"
|
|
36
|
-
headers = {"Authorization": f"Token {token}"}
|
|
37
|
-
response = requests.get(url, headers=headers)
|
|
38
|
-
return self.handle_response(response)
|
|
39
|
-
|
|
40
|
-
def handle_response(self, response):
|
|
41
|
-
if response.ok:
|
|
42
|
-
return response.json()
|
|
43
|
-
elif response.status_code == 500:
|
|
44
|
-
raise self.ServerError("Error 500")
|
|
45
|
-
else:
|
|
46
|
-
try:
|
|
47
|
-
rj = response.json()
|
|
48
|
-
raise self.RequestFailed(rj)
|
|
49
|
-
except requests.exceptions.JSONDecodeError:
|
|
50
|
-
raise self.RequestFailed(response)
|
|
51
|
-
|
|
52
|
-
class RequestFailed(Exception):
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
class ServerError(Exception):
|
|
56
|
-
pass
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
File without changes
|
|
File without changes
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/curriculum.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/git_wrapper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/assess.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{making_with_code_cli-3.1.0 → making_with_code_cli-4.0.0}/making_with_code_cli/teach/patch.py
RENAMED
|
File without changes
|
|
File without changes
|