with the appropriate children
- function Schematic(input) {
- // set up diagram viewing parameters
- this.show_grid = true;
- this.grid = 8;
- this.scale = 2;
- this.origin_x = input.getAttribute("origin_x");
- if (this.origin_x == undefined) this.origin_x = 0;
- this.origin_y = input.getAttribute("origin_y");
- if (this.origin_y == undefined) this.origin_y = 0;
- this.cursor_x = 0;
- this.cursor_y = 0;
- this.window_list = []; // list of pop-up windows in increasing z order
-
- // use user-supplied list of parts if supplied
- // else just populate parts bin with all the parts
- this.edits_allowed = true;
- var parts = input.getAttribute("parts");
- if (parts == undefined || parts == "None") {
- parts = [];
- for (var p in parts_map) parts.push(p);
- } else if (parts == "") {
- this.edits_allowed = false;
- parts = [];
- } else parts = parts.split(",");
-
- // now add the parts to the parts bin
- this.parts_bin = [];
- for (var i = 0; i < parts.length; i++) {
- var part = new Part(this);
- var pm = parts_map[parts[i]];
- part.set_component(new pm[0](0, 0, 0), pm[1]);
- this.parts_bin.push(part);
- }
-
- // use user-supplied list of analyses, otherwise provide them all
- // analyses="" means no analyses
- var analyses = input.getAttribute("analyses");
- if (analyses == undefined || analyses == "None") analyses = ["dc", "ac", "tran"];
- else if (analyses == "") analyses = [];
- else analyses = analyses.split(",");
-
- if (parts.length == 0 && analyses.length == 0) this.diagram_only = true;
- else this.diagram_only = false;
-
- // see what we need to submit. Expecting attribute of the form
- // submit_analyses="{'tran':[[node_name,t1,t2,t3],...],
- // 'ac':[[node_name,f1,f2,...],...]}"
- var submit = input.getAttribute("submit_analyses");
- if (submit && submit.indexOf("{") != -1) this.submit_analyses = JSON.parse(submit);
- else this.submit_analyses = undefined;
-
- // toolbar
- this.tools = [];
- this.toolbar = [];
-
- /* DISABLE HELP BUTTON (target URL not consistent with multicourse hierarchy) -- SJSU
- if (!this.diagram_only) {
- this.tools['help'] = this.add_tool(help_icon,'Help: display help page',this.help);
- this.enable_tool('help',true);
- this.toolbar.push(null); // spacer
- }
- END DISABLE HELP BUTTON -- SJSU */
-
- if (this.edits_allowed) {
- this.tools["grid"] = this.add_tool(grid_icon, "Grid: toggle grid display", this.toggle_grid);
- this.enable_tool("grid", true);
- this.tools["cut"] = this.add_tool(
- cut_icon,
- "Cut: move selected components from diagram to the clipboard",
- this.cut,
- );
- this.tools["copy"] = this.add_tool(copy_icon, "Copy: copy selected components into the clipboard", this.copy);
- this.tools["paste"] = this.add_tool(paste_icon, "Paste: copy clipboard into the diagram", this.paste);
- this.toolbar.push(null); // spacer
- }
-
- // simulation interface if cktsim.js is loaded
- if (typeof cktsim != "undefined") {
- if (analyses.indexOf("dc") != -1) {
- this.tools["dc"] = this.add_tool("DC", "DC Analysis", this.dc_analysis);
- this.enable_tool("dc", true);
- this.dc_max_iters = "1000"; // default values dc solution
- }
-
- if (analyses.indexOf("ac") != -1) {
- this.tools["ac"] = this.add_tool("AC", "AC Small-Signal Analysis", this.setup_ac_analysis);
- this.enable_tool("ac", true);
- this.ac_npts = "50"; // default values for AC Analysis
- this.ac_fstart = "10";
- this.ac_fstop = "1G";
- this.ac_source_name = undefined;
- }
-
- if (analyses.indexOf("tran") != -1) {
- this.tools["tran"] = this.add_tool("TRAN", "Transient Analysis", this.transient_analysis);
- this.enable_tool("tran", true);
- this.tran_npts = "100"; // default values for transient analysis
- this.tran_tstop = "1";
- }
- }
-
- // set up diagram canvas
- this.canvas = document.createElement("canvas");
- this.width = input.getAttribute("width");
- this.width = parseInt(this.width == undefined ? "400" : this.width);
- this.canvas.width = this.width;
- this.height = input.getAttribute("height");
- this.height = parseInt(this.height == undefined ? "300" : this.height);
- this.canvas.height = this.height;
-
- this.sctl_r = 16; // scrolling control parameters
- this.sctl_x = this.sctl_r + 8; // upper left
- this.sctl_y = this.sctl_r + 8;
- this.zctl_left = this.sctl_x - 8;
- this.zctl_top = this.sctl_y + this.sctl_r + 8;
-
- // repaint simply draws this buffer and then adds selected elements on top
- this.bg_image = document.createElement("canvas");
- this.bg_image.width = this.width;
- this.bg_image.height = this.height;
-
- if (!this.diagram_only) {
- this.canvas.tabIndex = 0; // so we get keystrokes
- this.canvas.style.borderStyle = "solid";
- this.canvas.style.borderWidth = "1px";
- this.canvas.style.borderColor = grid_style;
- this.canvas.style.outline = "none";
- }
-
- this.canvas.schematic = this;
- if (this.edits_allowed) {
- this.canvas.addEventListener("mousemove", schematic_mouse_move, false);
- this.canvas.addEventListener("mouseover", schematic_mouse_enter, false);
- this.canvas.addEventListener("mouseout", schematic_mouse_leave, false);
- this.canvas.addEventListener("mousedown", schematic_mouse_down, false);
- this.canvas.addEventListener("mouseup", schematic_mouse_up, false);
- this.canvas.addEventListener("mousewheel", schematic_mouse_wheel, false);
- this.canvas.addEventListener("DOMMouseScroll", schematic_mouse_wheel, false); // for FF
- this.canvas.addEventListener("dblclick", schematic_double_click, false);
- this.canvas.addEventListener("keydown", schematic_key_down, false);
- this.canvas.addEventListener("keyup", schematic_key_up, false);
- }
-
- // set up message area
- if (!this.diagram_only) {
- this.status_div = document.createElement("div");
- this.status = document.createTextNode("");
- this.status_div.appendChild(this.status);
- this.status_div.style.height = status_height + "px";
- } else this.status_div = undefined;
-
- this.connection_points = []; // location string => list of cp's
- this.components = [];
- this.dragging = false;
- this.select_rect = undefined;
- this.wire = undefined;
- this.operating_point = undefined; // result from DC analysis
- this.dc_results = undefined; // saved analysis results for submission
- this.ac_results = undefined; // saved analysis results for submission
- this.transient_results = undefined; // saved analysis results for submission
-
- // state of modifier keys
- this.ctrlKey = false;
- this.shiftKey = false;
- this.altKey = false;
- this.cmdKey = false;
-
- // make sure other code can find us!
- input.schematic = this;
- this.input = input;
-
- // set up DOM -- use nested tables to do the layout
- var table, tr, td;
- table = document.createElement("table");
- table.rules = "none";
- if (!this.diagram_only) {
- table.frame = "box";
- table.style.borderStyle = "solid";
- table.style.borderWidth = "2px";
- table.style.borderColor = normal_style;
- table.style.backgroundColor = background_style;
- }
-
- // add tools to DOM
- if (this.toolbar.length > 0) {
- tr = document.createElement("tr");
- table.appendChild(tr);
- td = document.createElement("td");
- td.style.verticalAlign = "top";
- td.colSpan = 2;
- tr.appendChild(td);
- for (var i = 0; i < this.toolbar.length; ++i) {
- var tool = this.toolbar[i];
- if (tool != null) td.appendChild(tool);
- }
- }
-
- // add canvas and parts bin to DOM
- tr = document.createElement("tr");
- table.appendChild(tr);
-
- td = document.createElement("td");
- tr.appendChild(td);
- var wrapper = document.createElement("div"); // for inserting pop-up windows
- td.appendChild(wrapper);
- wrapper.style.position = "relative"; // so we can position subwindows
- wrapper.appendChild(this.canvas);
-
- td = document.createElement("td");
- td.style.verticalAlign = "top";
- tr.appendChild(td);
- var parts_table = document.createElement("table");
- td.appendChild(parts_table);
- parts_table.rules = "none";
- parts_table.frame = "void";
- parts_table.cellPadding = "0";
- parts_table.cellSpacing = "0";
-
- // fill in parts_table
- var parts_per_column = Math.floor(this.height / (part_h + 5)); // mysterious extra padding
- for (var i = 0; i < parts_per_column; ++i) {
- tr = document.createElement("tr");
- parts_table.appendChild(tr);
- for (var j = i; j < this.parts_bin.length; j += parts_per_column) {
- td = document.createElement("td");
- tr.appendChild(td);
- td.appendChild(this.parts_bin[j].canvas);
- }
- }
-
- if (this.status_div != undefined) {
- tr = document.createElement("tr");
- table.appendChild(tr);
- td = document.createElement("td");
- tr.appendChild(td);
- td.colSpan = 2;
- td.appendChild(this.status_div);
- }
-
- // add to dom
- // avoid Chrome bug that changes to text cursor whenever
- // drag starts. Just do this in schematic tool...
- var toplevel = document.createElement("div");
- toplevel.onselectstart = function () {
- return false;
- };
- toplevel.appendChild(table);
- this.input.parentNode.insertBefore(toplevel, this.input.nextSibling);
-
- // process initial contents of diagram
- this.load_schematic(this.input.getAttribute("value"), this.input.getAttribute("initial_value"));
-
- // start by centering diagram on the screen
- this.zoomall();
- }
-
- var part_w = 42; // size of a parts bin compartment
- var part_h = 42;
- var status_height = 18;
-
- Schematic.prototype.add_component = function (new_c) {
- this.components.push(new_c);
- // create undoable edit record here
- };
-
- Schematic.prototype.remove_component = function (c) {
- var index = this.components.indexOf(c);
- if (index != -1) this.components.splice(index, 1);
- };
-
- Schematic.prototype.find_connections = function (cp) {
- return this.connection_points[cp.location];
- };
-
- Schematic.prototype.add_connection_point = function (cp) {
- var cplist = this.connection_points[cp.location];
- if (cplist) cplist.push(cp);
- else {
- cplist = [cp];
- this.connection_points[cp.location] = cplist;
- }
-
- return cplist;
- };
-
- Schematic.prototype.remove_connection_point = function (cp, old_location) {
- // remove cp from list at old location
- var cplist = this.connection_points[old_location];
- if (cplist) {
- var index = cplist.indexOf(cp);
- if (index != -1) {
- cplist.splice(index, 1);
- // if no more connections at this location, remove
- // entry from array to keep our search time short
- if (cplist.length == 0) delete this.connection_points[old_location];
- }
- }
- };
-
- Schematic.prototype.update_connection_point = function (cp, old_location) {
- this.remove_connection_point(cp, old_location);
- return this.add_connection_point(cp);
- };
-
- Schematic.prototype.add_wire = function (x1, y1, x2, y2) {
- var new_wire = new Wire(x1, y1, x2, y2);
- new_wire.add(this);
- new_wire.move_end();
- return new_wire;
- };
-
- Schematic.prototype.split_wire = function (w, cp) {
- // remove bisected wire
- w.remove();
-
- // add two new wires with connection point cp in the middle
- this.add_wire(w.x, w.y, cp.x, cp.y);
- this.add_wire(w.x + w.dx, w.y + w.dy, cp.x, cp.y);
- };
-
- // see if connection points of component c split any wires
- Schematic.prototype.check_wires = function (c) {
- for (var i = 0; i < this.components.length; i++) {
- var cc = this.components[i];
- if (cc != c) {
- // don't check a component against itself
- // only wires will return non-null from a bisect call
- var cp = cc.bisect(c);
- if (cp) {
- // cc is a wire bisected by connection point cp
- this.split_wire(cc, cp);
- this.redraw_background();
- }
- }
- }
- };
-
- // see if there are any existing connection points that bisect wire w
- Schematic.prototype.check_connection_points = function (w) {
- for (var locn in this.connection_points) {
- var cplist = this.connection_points[locn];
- if (cplist && w.bisect_cp(cplist[0])) {
- this.split_wire(w, cplist[0]);
- this.redraw_background();
-
- // stop here, new wires introduced by split will do their own checks
- return;
- }
- }
- };
-
- // merge collinear wires sharing an end point
- Schematic.prototype.clean_up_wires = function () {
- for (var locn in this.connection_points) {
- var cplist = this.connection_points[locn];
- if (cplist && cplist.length == 2) {
- // found a connection with just two connections, see if they're wires
- var c1 = cplist[0].parent;
- var c2 = cplist[1].parent;
- if (c1.type == "w" && c2.type == "w") {
- var e1 = c1.other_end(cplist[0]);
- var e2 = c2.other_end(cplist[1]);
- var e3 = cplist[0]; // point shared by the two wires
- if (collinear(e1, e2, e3)) {
- c1.remove();
- c2.remove();
- this.add_wire(e1.x, e1.y, e2.x, e2.y);
- }
- }
- }
- }
- };
-
- Schematic.prototype.unselect_all = function (which) {
- this.operating_point = undefined; // remove annotations
- for (var i = this.components.length - 1; i >= 0; --i) if (i != which) this.components[i].set_select(false);
- };
-
- Schematic.prototype.drag_begin = function () {
- // let components know they're about to move
- for (var i = this.components.length - 1; i >= 0; --i) {
- var component = this.components[i];
- if (component.selected) component.move_begin();
- }
-
- // remember where drag started
- this.drag_x = this.cursor_x;
- this.drag_y = this.cursor_y;
- this.dragging = true;
- };
-
- Schematic.prototype.drag_end = function () {
- // let components know they're done moving
- for (var i = this.components.length - 1; i >= 0; --i) {
- var component = this.components[i];
- if (component.selected) component.move_end();
- }
- this.dragging = false;
- this.clean_up_wires();
- this.redraw_background();
- };
-
- Schematic.prototype.help = function () {
- window.open("/static/handouts/schematic_tutorial.pdf");
- };
-
- // zoom diagram around given coords
- Schematic.prototype.rescale = function (nscale, cx, cy) {
- if (cx == undefined) {
- // use current center point if no point has been specified
- cx = this.origin_x + this.width / (2 * this.scale);
- cy = this.origin_y + this.height / (2 * this.scale);
- }
-
- this.origin_x += cx * (this.scale - nscale);
- this.origin_y += cy * (this.scale - nscale);
- this.scale = nscale;
- this.redraw_background();
- };
-
- Schematic.prototype.toggle_grid = function () {
- this.show_grid = !this.show_grid;
- this.redraw_background();
- };
-
- var zoom_factor = 1.25; // scaling is some power of zoom_factor
- var zoom_min = 0.5;
- var zoom_max = 4.0;
- var origin_min = -200; // in grids
- var origin_max = 200;
-
- Schematic.prototype.zoomin = function () {
- var nscale = this.scale * zoom_factor;
- if (nscale < zoom_max) {
- // keep center of view unchanged
- this.origin_x += (this.width / 2) * (1.0 / this.scale - 1.0 / nscale);
- this.origin_y += (this.height / 2) * (1.0 / this.scale - 1.0 / nscale);
- this.scale = nscale;
- this.redraw_background();
- }
- };
-
- Schematic.prototype.zoomout = function () {
- var nscale = this.scale / zoom_factor;
- if (nscale > zoom_min) {
- // keep center of view unchanged
- this.origin_x += (this.width / 2) * (1.0 / this.scale - 1.0 / nscale);
- this.origin_y += (this.height / 2) * (1.0 / this.scale - 1.0 / nscale);
- this.scale = nscale;
- this.redraw_background();
- }
- };
-
- Schematic.prototype.zoomall = function () {
- // w,h for schematic including a 25% margin on all sides
- var sch_w = 1.5 * (this.bbox[2] - this.bbox[0]);
- var sch_h = 1.5 * (this.bbox[3] - this.bbox[1]);
-
- if (sch_w == 0 && sch_h == 0) {
- this.origin_x = 0;
- this.origin_y = 0;
- this.scale = 2;
- } else {
- // compute scales that would make schematic fit, choose smallest
- var scale_x = this.width / sch_w;
- var scale_y = this.height / sch_h;
- this.scale = Math.pow(zoom_factor, Math.ceil(Math.log(Math.min(scale_x, scale_y)) / Math.log(zoom_factor)));
- if (this.scale < zoom_min) this.scale = zoom_min;
- else if (this.scale > zoom_max) this.scale = zoom_max;
-
- // center the schematic
- this.origin_x = (this.bbox[2] + this.bbox[0]) / 2 - this.width / (2 * this.scale);
- this.origin_y = (this.bbox[3] + this.bbox[1]) / 2 - this.height / (2 * this.scale);
- }
-
- this.redraw_background();
- };
-
- Schematic.prototype.cut = function () {
- // clear previous contents
- sch_clipboard = [];
-
- // look for selected components, move them to clipboard.
- for (var i = this.components.length - 1; i >= 0; --i) {
- var c = this.components[i];
- if (c.selected) {
- c.remove();
- sch_clipboard.push(c);
- }
- }
-
- // update diagram view
- this.redraw();
- };
-
- Schematic.prototype.copy = function () {
- // clear previous contents
- sch_clipboard = [];
-
- // look for selected components, copy them to clipboard.
- for (var i = this.components.length - 1; i >= 0; --i) {
- var c = this.components[i];
- if (c.selected) sch_clipboard.push(c.clone(c.x, c.y));
- }
- };
-
- Schematic.prototype.paste = function () {
- // compute left,top of bounding box for origins of
- // components in the clipboard
- var left = undefined;
- var top = undefined;
- for (var i = sch_clipboard.length - 1; i >= 0; --i) {
- var c = sch_clipboard[i];
- left = left ? Math.min(left, c.x) : c.x;
- top = top ? Math.min(top, c.y) : c.y;
- }
-
- this.message("cursor " + this.cursor_x + "," + this.cursor_y);
-
- // clear current selections
- this.unselect_all(-1);
- this.redraw_background(); // so we see any components that got unselected
-
- // make clones of components on the clipboard, positioning
- // them relative to the cursor
- for (var i = sch_clipboard.length - 1; i >= 0; --i) {
- var c = sch_clipboard[i];
- var new_c = c.clone(this.cursor_x + (c.x - left), this.cursor_y + (c.y - top));
- new_c.set_select(true);
- new_c.add(this);
- }
-
- this.redraw();
- };
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Netlist and Simulation interface
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- // load diagram from JSON representation
- Schematic.prototype.load_schematic = function (value, initial_value) {
- // use default value if no schematic info in value
- if (value == undefined || value.indexOf("[") == -1) value = initial_value;
- if (value && value.indexOf("[") != -1) {
- // convert string value into data structure
- var json = JSON.parse(value);
-
- // top level is a list of components
- for (var i = json.length - 1; i >= 0; --i) {
- var c = json[i];
- if (c[0] == "view") {
- this.ac_fstart = c[5];
- this.ac_fstop = c[6];
- this.ac_source_name = c[7];
- this.tran_npts = c[8];
- this.tran_tstop = c[9];
- this.dc_max_iters = c[10];
- } else if (c[0] == "w") {
- // wire
- this.add_wire(c[1][0], c[1][1], c[1][2], c[1][3]);
- } else if (c[0] == "dc") {
- this.dc_results = c[1];
- } else if (c[0] == "transient") {
- this.transient_results = c[1];
- } else if (c[0] == "ac") {
- this.ac_results = c[1];
- } else {
- // ordinary component
- // c := [type, coords, properties, connections]
- var type = c[0];
- var coords = c[1];
- var properties = c[2];
-
- var part = new parts_map[type][0](coords[0], coords[1], coords[2]);
- for (var name in properties) part.properties[name] = properties[name];
-
- part.add(this);
- }
- }
- }
-
- this.redraw_background();
- };
-
- // label all the nodes in the circuit
- Schematic.prototype.label_connection_points = function () {
- // start by clearing all the connection point labels
- for (var i = this.components.length - 1; i >= 0; --i) this.components[i].clear_labels();
-
- // components are in charge of labeling their unlabeled connections.
- // labels given to connection points will propagate to coincident connection
- // points and across Wires.
-
- // let special components like GND label their connection(s)
- for (var i = this.components.length - 1; i >= 0; --i) this.components[i].add_default_labels();
-
- // now have components generate labels for unlabeled connections
- this.next_label = 0;
- for (var i = this.components.length - 1; i >= 0; --i) this.components[i].label_connections();
- };
-
- Schematic.prototype.get_next_label = function () {
- // generate next label in sequence
- this.next_label += 1;
- return this.next_label.toString();
- };
-
- // propagate label to coincident connection points
- Schematic.prototype.propagate_label = function (label, location) {
- var cplist = this.connection_points[location];
- for (var i = cplist.length - 1; i >= 0; --i) cplist[i].propagate_label(label);
- };
-
- // update the value field of our corresponding input field with JSON
- // representation of schematic
- Schematic.prototype.update_value = function () {
- // label connection points
- this.label_connection_points();
-
- // build JSON data structure, convert to string value for
- // input field
- this.input.value = JSON.stringify(this.json_with_analyses());
- };
-
- Schematic.prototype.json = function () {
- var json = [];
-
- // output all the components/wires in the diagram
- var n = this.components.length;
- for (var i = 0; i < n; i++) json.push(this.components[i].json(i));
-
- // capture the current view parameters
- json.push([
- "view",
- this.origin_x,
- this.origin_y,
- this.scale,
- this.ac_npts,
- this.ac_fstart,
- this.ac_fstop,
- this.ac_source_name,
- this.tran_npts,
- this.tran_tstop,
- this.dc_max_iters,
- ]);
-
- return json;
- };
-
- Schematic.prototype.json_with_analyses = function () {
- var json = this.json();
-
- if (this.dc_results != undefined) json.push(["dc", this.dc_results]);
- if (this.ac_results != undefined) json.push(["ac", this.ac_results]);
- if (this.transient_results != undefined) json.push(["transient", this.transient_results]);
-
- return json;
- };
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Simulation interface
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- Schematic.prototype.extract_circuit = function () {
- // give all the circuit nodes a name, extract netlist
- this.label_connection_points();
- var netlist = this.json();
-
- // since we've done the heavy lifting, update input field value
- // so user can grab diagram if they want
- this.input.value = JSON.stringify(netlist);
-
- // create a circuit from the netlist
- var ckt = new cktsim.Circuit();
- if (ckt.load_netlist(netlist)) return ckt;
- else return null;
- };
-
- Schematic.prototype.dc_analysis = function () {
- // remove any previous annotations
- this.unselect_all(-1);
- this.redraw_background();
-
- var ckt = this.extract_circuit();
- if (ckt === null) return;
-
- // run the analysis
- this.operating_point = ckt.dc();
-
- if (this.operating_point != undefined) {
- // save a copy of the results for submission
- this.dc_results = {};
- for (var i in this.operating_point) this.dc_results[i] = this.operating_point[i];
-
- // display results on diagram
- this.redraw();
- }
- };
-
- // return a list of [color,node_label,offset,type] for each probe in the diagram
- // type == 'voltage' or 'current'
- Schematic.prototype.find_probes = function () {
- var result = [];
- var result = [];
- for (var i = this.components.length - 1; i >= 0; --i) {
- var c = this.components[i];
- var info = c.probe_info();
- if (info != undefined) result.push(c.probe_info());
- }
- return result;
- };
-
- // use a dialog to get AC analysis parameters
- Schematic.prototype.setup_ac_analysis = function () {
- this.unselect_all(-1);
- this.redraw_background();
-
- var npts_lbl = "Number of points/decade";
- var fstart_lbl = "Starting frequency (Hz)";
- var fstop_lbl = "Ending frequency (Hz)";
- var source_name_lbl = "Name of V or I source for ac";
-
- if (this.find_probes().length == 0) {
- alert("AC Analysis: there are no voltage probes in the diagram!");
- return;
- }
-
- var fields = [];
- fields[fstart_lbl] = build_input("text", 10, this.ac_fstart);
- fields[fstop_lbl] = build_input("text", 10, this.ac_fstop);
- fields[source_name_lbl] = build_input("text", 10, this.ac_source_name);
-
- var content = build_table(fields);
- content.fields = fields;
- content.sch = this;
-
- this.dialog("AC Analysis", content, function (content) {
- var sch = content.sch;
-
- // retrieve parameters, remember for next time
- sch.ac_fstart = content.fields[fstart_lbl].value;
- sch.ac_fstop = content.fields[fstop_lbl].value;
- sch.ac_source_name = content.fields[source_name_lbl].value;
-
- sch.ac_analysis(
- cktsim.parse_number(sch.ac_npts),
- cktsim.parse_number(sch.ac_fstart),
- cktsim.parse_number(sch.ac_fstop),
- sch.ac_source_name,
- );
- });
- };
-
- Schematic.prototype.ac_analysis = function (npts, fstart, fstop, ac_source_name) {
- var ckt = this.extract_circuit();
- if (ckt === null) return;
- var results = ckt.ac(npts, fstart, fstop, ac_source_name);
-
- if (typeof results == "string") this.message(results);
- else {
- var x_values = results["_frequencies_"];
-
- // x axis will be a log scale
- for (var i = x_values.length - 1; i >= 0; --i) x_values[i] = Math.log(x_values[i]) / Math.LN10;
-
- if (this.submit_analyses != undefined) {
- var submit = this.submit_analyses["ac"];
- if (submit != undefined) {
- // save a copy of the results for submission
- this.ac_results = {};
-
- // save requested values for each requested node
- for (var j = 0; j < submit.length; j++) {
- var flist = submit[j]; // [node_name,f1,f2,...]
- var node = flist[0];
- var values = results[node];
- var fvlist = [];
- // for each requested freq, interpolate response value
- for (var k = 1; k < flist.length; k++) {
- var f = flist[k];
- var v = interpolate(f, x_values, values);
- // convert to dB
- fvlist.push([f, v == undefined ? "undefined" : (20.0 * Math.log(v)) / Math.LN10]);
- }
- // save results as list of [f,response] paris
- this.ac_results[node] = fvlist;
- }
- }
- }
-
- // set up plot values for each node with a probe
- var y_values = []; // list of [color, result_array]
- var z_values = []; // list of [color, result_array]
- var probes = this.find_probes();
- var probe_maxv = [];
- var probe_color = [];
-
- // Check for probe with near zero transfer function and warn
- for (var i = probes.length - 1; i >= 0; --i) {
- if (probes[i][3] != "voltage") continue;
- probe_color[i] = probes[i][0];
- var label = probes[i][1];
- var v = results[label];
- probe_maxv[i] = array_max(v); // magnitudes always > 0
- }
-
- var all_max = array_max(probe_maxv);
- if (all_max < 1.0e-16) {
- alert("Zero ac response, -infinity on DB scale.");
- } else {
- for (var i = probes.length - 1; i >= 0; --i) {
- if (probes[i][3] != "voltage") continue;
- if (probe_maxv[i] / all_max < 1.0e-10) {
- alert("Near zero ac response, remove " + probe_color[i] + " probe");
- return;
- }
- }
- }
-
- for (var i = probes.length - 1; i >= 0; --i) {
- if (probes[i][3] != "voltage") continue;
- var color = probes[i][0];
- var label = probes[i][1];
- var offset = cktsim.parse_number(probes[i][2]);
- var v = results[label];
- // convert values into dB relative to source amplitude
- var v_max = 1;
- for (var j = v.length - 1; j >= 0; --j)
- // convert each value to dB relative to max
- v[j] = (20.0 * Math.log(v[j] / v_max)) / Math.LN10;
- y_values.push([color, offset, v]);
-
- var v = results[label + "_phase"];
- z_values.push([color, 0, v]);
- }
-
- // graph the result and display in a window
- var graph2 = this.graph(x_values, "log(Frequency in Hz)", z_values, "degrees");
- this.window("AC Analysis - Phase", graph2);
- var graph1 = this.graph(x_values, "log(Frequency in Hz)", y_values, "dB");
- this.window("AC Analysis - Magnitude", graph1, 50);
- }
- };
-
- Schematic.prototype.transient_analysis = function () {
- this.unselect_all(-1);
- this.redraw_background();
-
- var npts_lbl = "Minimum number of timepoints";
- var tstop_lbl = "Stop Time (seconds)";
- var probes = this.find_probes();
- if (probes.length == 0) {
- alert("Transient Analysis: there are no probes in the diagram!");
- return;
- }
-
- var fields = [];
- fields[tstop_lbl] = build_input("text", 10, this.tran_tstop);
-
- var content = build_table(fields);
- content.fields = fields;
- content.sch = this;
-
- this.dialog("Transient Analysis", content, function (content) {
- var sch = content.sch;
- var ckt = sch.extract_circuit();
- if (ckt === null) return;
-
- // retrieve parameters, remember for next time
- sch.tran_tstop = content.fields[tstop_lbl].value;
-
- // gather a list of nodes that are being probed. These
- // will be added to the list of nodes checked during the
- // LTE calculations in transient analysis
- var probe_list = sch.find_probes();
- var probe_names = new Array(probe_list.length);
- for (var i = probe_list.length - 1; i >= 0; --i) probe_names[i] = probe_list[i][1];
-
- // run the analysis
- var results = ckt.tran(ckt.parse_number(sch.tran_npts), 0, ckt.parse_number(sch.tran_tstop), probe_names, false);
-
- if (typeof results == "string") sch.message(results);
- else {
- if (sch.submit_analyses != undefined) {
- var submit = sch.submit_analyses["tran"];
- if (submit != undefined) {
- // save a copy of the results for submission
- sch.transient_results = {};
- var times = results["_time_"];
-
- // save requested values for each requested node
- for (var j = 0; j < submit.length; j++) {
- var tlist = submit[j]; // [node_name,t1,t2,...]
- var node = tlist[0];
- var values = results[node];
- var tvlist = [];
- // for each requested time, interpolate waveform value
- for (var k = 1; k < tlist.length; k++) {
- var t = tlist[k];
- var v = interpolate(t, times, values);
- tvlist.push([t, v == undefined ? "undefined" : v]);
- }
- // save results as list of [t,value] pairs
- sch.transient_results[node] = tvlist;
- }
- }
- }
-
- var x_values = results["_time_"];
- var x_legend = "Time";
-
- // set up plot values for each node with a probe
- var v_values = []; // voltage values: list of [color, result_array]
- var i_values = []; // current values: list of [color, result_array]
- var probes = sch.find_probes();
-
- for (var i = probes.length - 1; i >= 0; --i) {
- var color = probes[i][0];
- var label = probes[i][1];
- var offset = cktsim.parse_number(probes[i][2]);
- var v = results[label];
- if (v == undefined) {
- alert(
- "The " +
- color +
- " probe is connected to node " +
- '"' +
- label +
- '"' +
- " which is not an actual circuit node",
- );
- } else if (probes[i][3] == "voltage") {
- if (color == "x-axis") {
- x_values = v;
- x_legend = "Voltage";
- } else v_values.push([color, offset, v]);
- } else {
- if (color == "x-axis") {
- x_values = v;
- x_legend = "Current";
- } else i_values.push([color, offset, v]);
- }
- }
-
- // graph the result and display in a window
- var graph = sch.graph(x_values, x_legend, v_values, "Voltage", i_values, "Current");
- sch.window("Results of Transient Analysis", graph);
- }
- });
- };
-
- // t is the time at which we want a value
- // times is a list of timepoints from the simulation
- function interpolate(t, times, values) {
- if (values == undefined) return undefined;
-
- for (var i = 0; i < times.length; i++)
- if (t < times[i]) {
- // t falls between times[i-1] and times[i]
- var t1 = i == 0 ? times[0] : times[i - 1];
- var t2 = times[i];
-
- if (t2 == undefined) return undefined;
-
- var v1 = i == 0 ? values[0] : values[i - 1];
- var v2 = values[i];
- var v = v1;
- if (t != t1) v += ((t - t1) * (v2 - v1)) / (t2 - t1);
- return v;
- }
- }
-
- // external interface for setting the property value of a named component
- Schematic.prototype.set_property = function (component_name, property, value) {
- this.unselect_all(-1);
-
- for (var i = this.components.length - 1; i >= 0; --i) {
- var component = this.components[i];
- if (component.properties["name"] == component_name) {
- component.properties[property] = value.toString();
- break;
- }
- }
-
- this.redraw_background();
- };
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Drawing support -- deals with scaling and scrolling of diagrama
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- // here to redraw background image containing static portions of the schematic.
- // Also redraws dynamic portion.
- Schematic.prototype.redraw_background = function () {
- var c = this.bg_image.getContext("2d");
-
- c.lineCap = "round";
-
- // paint background color
- c.fillStyle = element_style;
- c.fillRect(0, 0, this.width, this.height);
-
- if (!this.diagram_only && this.show_grid) {
- // grid
- c.strokeStyle = grid_style;
- var first_x = this.origin_x;
- var last_x = first_x + this.width / this.scale;
- var first_y = this.origin_y;
- var last_y = first_y + this.height / this.scale;
-
- for (var i = this.grid * Math.ceil(first_x / this.grid); i < last_x; i += this.grid)
- this.draw_line(c, i, first_y, i, last_y, 0.1);
-
- for (var i = this.grid * Math.ceil(first_y / this.grid); i < last_y; i += this.grid)
- this.draw_line(c, first_x, i, last_x, i, 0.1);
- }
-
- // unselected components
- var min_x = Infinity; // compute bounding box for diagram
- var max_x = -Infinity;
- var min_y = Infinity;
- var max_y = -Infinity;
- for (var i = this.components.length - 1; i >= 0; --i) {
- var component = this.components[i];
- if (!component.selected) {
- component.draw(c);
- min_x = Math.min(component.bbox[0], min_x);
- max_x = Math.max(component.bbox[2], max_x);
- min_y = Math.min(component.bbox[1], min_y);
- max_y = Math.max(component.bbox[3], max_y);
- }
- }
- this.unsel_bbox = [min_x, min_y, max_x, max_y];
- this.redraw(); // background changed, redraw on screen
- };
-
- // redraw what user sees = static image + dynamic parts
- Schematic.prototype.redraw = function () {
- var c = this.canvas.getContext("2d");
-
- // put static image in the background
- c.drawImage(this.bg_image, 0, 0);
-
- // selected components
- var min_x = this.unsel_bbox[0]; // compute bounding box for diagram
- var max_x = this.unsel_bbox[2];
- var min_y = this.unsel_bbox[1];
- var max_y = this.unsel_bbox[3];
- var selections = false;
- for (var i = this.components.length - 1; i >= 0; --i) {
- var component = this.components[i];
- if (component.selected) {
- component.draw(c);
- selections = true;
- min_x = Math.min(component.bbox[0], min_x);
- max_x = Math.max(component.bbox[2], max_x);
- min_y = Math.min(component.bbox[1], min_y);
- max_y = Math.max(component.bbox[3], max_y);
- }
- }
- if (min_x == Infinity) this.bbox = [0, 0, 0, 0];
- else this.bbox = [min_x, min_y, max_x, max_y];
- this.enable_tool("cut", selections);
- this.enable_tool("copy", selections);
- this.enable_tool("paste", sch_clipboard.length > 0);
-
- // connection points: draw one at each location
- for (var location in this.connection_points) {
- var cplist = this.connection_points[location];
- cplist[0].draw(c, cplist.length);
- }
-
- // draw new wire
- if (this.wire) {
- var r = this.wire;
- c.strokeStyle = selected_style;
- this.draw_line(c, r[0], r[1], r[2], r[3], 1);
- }
-
- // draw selection rectangle
- if (this.select_rect) {
- var r = this.select_rect;
- c.lineWidth = 1;
- c.strokeStyle = selected_style;
- c.beginPath();
- c.moveTo(r[0], r[1]);
- c.lineTo(r[0], r[3]);
- c.lineTo(r[2], r[3]);
- c.lineTo(r[2], r[1]);
- c.lineTo(r[0], r[1]);
- c.stroke();
- }
-
- // display operating point results
- if (this.operating_point) {
- if (typeof this.operating_point == "string") this.message(this.operating_point);
- else {
- // make a copy of the operating_point info so we can mess with it
- var temp = [];
- for (var i in this.operating_point) temp[i] = this.operating_point[i];
-
- // run through connection points displaying (once) the voltage
- // for each electrical node
- for (var location in this.connection_points) this.connection_points[location][0].display_voltage(c, temp);
-
- // let components display branch current info if available
- for (var i = this.components.length - 1; i >= 0; --i) this.components[i].display_current(c, temp);
- }
- }
-
- // add scrolling/zooming control
- if (!this.diagram_only) {
- var r = this.sctl_r;
- var x = this.sctl_x;
- var y = this.sctl_y;
-
- // circle with border
- c.fillStyle = element_style;
- c.beginPath();
- c.arc(x, y, r, 0, 2 * Math.PI);
- c.fill();
-
- c.strokeStyle = grid_style;
- c.lineWidth = 0.5;
- c.beginPath();
- c.arc(x, y, r, 0, 2 * Math.PI);
- c.stroke();
-
- // direction markers for scroll
- c.lineWidth = 3;
- c.beginPath();
-
- c.moveTo(x + 4, y - r + 8); // north
- c.lineTo(x, y - r + 4);
- c.lineTo(x - 4, y - r + 8);
-
- c.moveTo(x + r - 8, y + 4); // east
- c.lineTo(x + r - 4, y);
- c.lineTo(x + r - 8, y - 4);
-
- c.moveTo(x + 4, y + r - 8); // south
- c.lineTo(x, y + r - 4);
- c.lineTo(x - 4, y + r - 8);
-
- c.moveTo(x - r + 8, y + 4); // west
- c.lineTo(x - r + 4, y);
- c.lineTo(x - r + 8, y - 4);
-
- c.stroke();
-
- // zoom control
- x = this.zctl_left;
- y = this.zctl_top;
- c.lineWidth = 0.5;
- c.fillStyle = element_style; // background
- c.fillRect(x, y, 16, 48);
- c.strokeStyle = grid_style; // border
- c.strokeRect(x, y, 16, 48);
- c.lineWidth = 1.0;
- c.beginPath();
- // zoom in label
- c.moveTo(x + 4, y + 8);
- c.lineTo(x + 12, y + 8);
- c.moveTo(x + 8, y + 4);
- c.lineTo(x + 8, y + 12);
- // zoom out label
- c.moveTo(x + 4, y + 24);
- c.lineTo(x + 12, y + 24);
- // surround label
- c.strokeRect(x + 4, y + 36, 8, 8);
- c.stroke();
- }
- };
-
- // draws a cross cursor
- Schematic.prototype.cross_cursor = function (c, x, y) {
- this.draw_line(c, x - this.grid, y, x + this.grid, y, 1);
- this.draw_line(c, x, y - this.grid, x, y + this.grid, 1);
- };
-
- Schematic.prototype.moveTo = function (c, x, y) {
- c.moveTo((x - this.origin_x) * this.scale, (y - this.origin_y) * this.scale);
- };
-
- Schematic.prototype.lineTo = function (c, x, y) {
- c.lineTo((x - this.origin_x) * this.scale, (y - this.origin_y) * this.scale);
- };
-
- Schematic.prototype.draw_line = function (c, x1, y1, x2, y2, width) {
- c.lineWidth = width * this.scale;
- c.beginPath();
- c.moveTo((x1 - this.origin_x) * this.scale, (y1 - this.origin_y) * this.scale);
- c.lineTo((x2 - this.origin_x) * this.scale, (y2 - this.origin_y) * this.scale);
- c.stroke();
- };
-
- Schematic.prototype.draw_arc = function (c, x, y, radius, start_radians, end_radians, anticlockwise, width, filled) {
- c.lineWidth = width * this.scale;
- c.beginPath();
- c.arc(
- (x - this.origin_x) * this.scale,
- (y - this.origin_y) * this.scale,
- radius * this.scale,
- start_radians,
- end_radians,
- anticlockwise,
- );
- if (filled) c.fill();
- else c.stroke();
- };
-
- Schematic.prototype.draw_text = function (c, text, x, y, size) {
- c.font = size * this.scale + "pt sans-serif";
- c.fillText(text, (x - this.origin_x) * this.scale, (y - this.origin_y) * this.scale);
- };
-
- // add method to canvas to compute relative coords for event
- try {
- if (HTMLCanvasElement)
- HTMLCanvasElement.prototype.relMouseCoords = function (event) {
- // run up the DOM tree to figure out coords for top,left of canvas
- var totalOffsetX = 0;
- var totalOffsetY = 0;
- var currentElement = this;
- do {
- totalOffsetX += currentElement.offsetLeft;
- totalOffsetY += currentElement.offsetTop;
- } while ((currentElement = currentElement.offsetParent));
-
- // now compute relative position of click within the canvas
- this.mouse_x = event.pageX - totalOffsetX;
- this.mouse_y = event.pageY - totalOffsetY;
- this.page_x = event.pageX;
- this.page_y = event.pageY;
- };
- } catch (err) {
- // ignore
- }
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Event handling
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- // process keystrokes, consuming those that are meaningful to us
- function schematic_key_down(event) {
- if (!event) event = window.event;
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
- var code = event.keyCode;
-
- // keep track of modifier key state
- if (code == 16) sch.shiftKey = true;
- else if (code == 17) sch.ctrlKey = true;
- else if (code == 18) sch.altKey = true;
- else if (code == 91) sch.cmdKey = true;
- // backspace or delete: delete selected components
- else if (code == 8 || code == 46) {
- // delete selected components
- for (var i = sch.components.length - 1; i >= 0; --i) {
- var component = sch.components[i];
- if (component.selected) component.remove();
- }
- sch.clean_up_wires();
- sch.redraw_background();
- event.preventDefault();
- return false;
- }
-
- // cmd/ctrl x: cut
- else if ((sch.ctrlKey || sch.cmdKey) && code == 88) {
- sch.cut();
- event.preventDefault();
- return false;
- }
-
- // cmd/ctrl c: copy
- else if ((sch.ctrlKey || sch.cmdKey) && code == 67) {
- sch.copy();
- event.preventDefault();
- return false;
- }
-
- // cmd/ctrl v: paste
- else if ((sch.ctrlKey || sch.cmdKey) && code == 86) {
- sch.paste();
- event.preventDefault();
- return false;
- }
-
- // 'r': rotate component
- else if (!sch.ctrlKey && !sch.altKey && !sch.cmdKey && code == 82) {
- // rotate
- for (var i = sch.components.length - 1; i >= 0; --i) {
- var component = sch.components[i];
- if (component.selected) {
- component.rotate(1);
- sch.check_wires(component);
- }
- }
- sch.clean_up_wires();
- sch.redraw_background();
- event.preventDefault();
- return false;
- } else return true;
-
- // consume keystroke
- sch.redraw();
- event.preventDefault();
- return false;
- }
-
- function schematic_key_up(event) {
- if (!event) event = window.event;
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
- var code = event.keyCode;
-
- if (code == 16) sch.shiftKey = false;
- else if (code == 17) sch.ctrlKey = false;
- else if (code == 18) sch.altKey = false;
- else if (code == 91) sch.cmdKey = false;
- }
-
- function schematic_mouse_enter(event) {
- if (!event) event = window.event;
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
-
- // see if user has selected a new part
- if (sch.new_part) {
- // grab incoming part, turn off selection of parts bin
- var part = sch.new_part;
- sch.new_part = undefined;
- part.select(false);
-
- // unselect everything else in the schematic, add part and select it
- sch.unselect_all(-1);
- sch.redraw_background(); // so we see any components that got unselected
-
- // make a clone of the component in the parts bin
- part = part.component.clone(sch.cursor_x, sch.cursor_y);
- part.add(sch); // add it to schematic
- part.set_select(true);
-
- // and start dragging it
- sch.drag_begin();
- }
-
- sch.drawCursor = true;
- sch.redraw();
- sch.canvas.focus(); // capture key strokes
- return false;
- }
-
- function schematic_mouse_leave(event) {
- if (!event) event = window.event;
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
- sch.drawCursor = false;
- sch.redraw();
- return false;
- }
-
- function schematic_mouse_down(event) {
- if (!event) event = window.event;
- else event.preventDefault();
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
-
- // determine where event happened in schematic coordinates
- sch.canvas.relMouseCoords(event);
-
- var mx = sch.canvas.mouse_x;
- var my = sch.canvas.mouse_y;
- var sx = mx - sch.sctl_x;
- var sy = my - sch.sctl_y;
- var zx = mx - sch.zctl_left;
- var zy = my - sch.zctl_top;
- if (sx * sx + sy * sy <= sch.sctl_r * sch.sctl_r) {
- // click in scrolling control
- // click on scrolling control, check which quadrant
- if (Math.abs(sy) > Math.abs(sx)) {
- // N or S
- var delta = this.height / 8;
- if (sy > 0) delta = -delta;
- var temp = sch.origin_y - delta;
- if (temp > origin_min * sch.grid && temp < origin_max * sch.grid) sch.origin_y = temp;
- } else {
- // E or W
- var delta = this.width / 8;
- if (sx < 0) delta = -delta;
- var temp = sch.origin_x + delta;
- if (temp > origin_min * sch.grid && temp < origin_max * sch.grid) sch.origin_x = temp;
- }
- } else if (zx >= 0 && zx < 16 && zy >= 0 && zy < 48) {
- // click in zoom control
- if (zy < 16) sch.zoomin();
- else if (zy < 32) sch.zoomout();
- else sch.zoomall();
- } else {
- var x = mx / sch.scale + sch.origin_x;
- var y = my / sch.scale + sch.origin_y;
- sch.cursor_x = Math.round(x / sch.grid) * sch.grid;
- sch.cursor_y = Math.round(y / sch.grid) * sch.grid;
-
- // is mouse over a connection point? If so, start dragging a wire
- var cplist = sch.connection_points[sch.cursor_x + "," + sch.cursor_y];
- if (cplist && !event.shiftKey) {
- sch.unselect_all(-1);
- sch.wire = [sch.cursor_x, sch.cursor_y, sch.cursor_x, sch.cursor_y];
- } else {
- // give all components a shot at processing the selection event
- var which = -1;
- for (var i = sch.components.length - 1; i >= 0; --i)
- if (sch.components[i].select(x, y, event.shiftKey)) {
- if (sch.components[i].selected) {
- sch.drag_begin();
- which = i; // keep track of component we found
- }
- break;
- }
- // did we just click on a previously selected component?
- var reselect = which != -1 && sch.components[which].was_previously_selected;
-
- if (!event.shiftKey) {
- // if shift key isn't pressed and we didn't click on component
- // that was already selected, unselect everyone except component
- // we just clicked on
- if (!reselect) sch.unselect_all(which);
-
- // if there's nothing to drag, set up a selection rectangle
- if (!sch.dragging)
- sch.select_rect = [sch.canvas.mouse_x, sch.canvas.mouse_y, sch.canvas.mouse_x, sch.canvas.mouse_y];
- }
- }
- }
-
- sch.redraw_background();
- return false;
- }
-
- function schematic_mouse_move(event) {
- if (!event) event = window.event;
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
-
- sch.canvas.relMouseCoords(event);
- var x = sch.canvas.mouse_x / sch.scale + sch.origin_x;
- var y = sch.canvas.mouse_y / sch.scale + sch.origin_y;
- sch.cursor_x = Math.round(x / sch.grid) * sch.grid;
- sch.cursor_y = Math.round(y / sch.grid) * sch.grid;
-
- if (sch.wire) {
- // update new wire end point
- sch.wire[2] = sch.cursor_x;
- sch.wire[3] = sch.cursor_y;
- } else if (sch.dragging) {
- // see how far we moved
- var dx = sch.cursor_x - sch.drag_x;
- var dy = sch.cursor_y - sch.drag_y;
- if (dx != 0 || dy != 0) {
- // update position for next time
- sch.drag_x = sch.cursor_x;
- sch.drag_y = sch.cursor_y;
-
- // give all components a shot at processing the event
- for (var i = sch.components.length - 1; i >= 0; --i) {
- var component = sch.components[i];
- if (component.selected) component.move(dx, dy);
- }
- }
- } else if (sch.select_rect) {
- // update moving corner of selection rectangle
- sch.select_rect[2] = sch.canvas.mouse_x;
- sch.select_rect[3] = sch.canvas.mouse_y;
- }
-
- // just redraw dynamic components
- sch.redraw();
-
- return false;
- }
-
- function schematic_mouse_up(event) {
- if (!event) event = window.event;
- else event.preventDefault();
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
-
- // drawing a new wire
- if (sch.wire) {
- var r = sch.wire;
- sch.wire = undefined;
-
- if (r[0] != r[2] || r[1] != r[3]) {
- // insert wire component
- sch.add_wire(r[0], r[1], r[2], r[3]);
- sch.clean_up_wires();
- sch.redraw_background();
- } else sch.redraw();
- }
-
- // dragging
- if (sch.dragging) sch.drag_end();
-
- // selection rectangle
- if (sch.select_rect) {
- var r = sch.select_rect;
-
- // if select_rect is a point, we've already dealt with selection
- // in mouse_down handler
- if (r[0] != r[2] || r[1] != r[3]) {
- // convert to schematic coordinates
- var s = [
- r[0] / sch.scale + sch.origin_x,
- r[1] / sch.scale + sch.origin_y,
- r[2] / sch.scale + sch.origin_x,
- r[3] / sch.scale + sch.origin_y,
- ];
- canonicalize(s);
-
- if (!event.shiftKey) sch.unselect_all();
-
- // select components that intersect selection rectangle
- for (var i = sch.components.length - 1; i >= 0; --i) sch.components[i].select_rect(s, event.shiftKey);
- }
-
- sch.select_rect = undefined;
- sch.redraw_background();
- }
- return false;
- }
-
- function schematic_mouse_wheel(event) {
- if (!event) event = window.event;
- else event.preventDefault();
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
-
- var delta = 0;
- if (event.wheelDelta) delta = event.wheelDelta;
- else if (event.detail) delta = -event.detail;
-
- if (delta) {
- var nscale = delta > 0 ? sch.scale * zoom_factor : sch.scale / zoom_factor;
-
- if (nscale > zoom_min && nscale < zoom_max) {
- // zoom around current mouse position
- sch.canvas.relMouseCoords(event);
- var s = 1.0 / sch.scale - 1.0 / nscale;
- sch.origin_x += sch.canvas.mouse_x * s;
- sch.origin_y += sch.canvas.mouse_y * s;
- sch.scale = nscale;
- sch.redraw_background();
- }
- }
- }
-
- function schematic_double_click(event) {
- if (!event) event = window.event;
- else event.preventDefault();
- var sch = window.event ? event.srcElement.schematic : event.target.schematic;
-
- // determine where event happened in schematic coordinates
- sch.canvas.relMouseCoords(event);
- var x = sch.canvas.mouse_x / sch.scale + sch.origin_x;
- var y = sch.canvas.mouse_y / sch.scale + sch.origin_y;
- sch.cursor_x = Math.round(x / sch.grid) * sch.grid;
- sch.cursor_y = Math.round(y / sch.grid) * sch.grid;
-
- // see if we double-clicked a component. If so, edit it's properties
- for (var i = sch.components.length - 1; i >= 0; --i) if (sch.components[i].edit_properties(x, y)) break;
-
- return false;
- }
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Status message and dialogs
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- Schematic.prototype.message = function (message) {
- this.status.nodeValue = message;
- };
-
- Schematic.prototype.append_message = function (message) {
- this.status.nodeValue += " / " + message;
- };
-
- // set up a dialog with specified title, content and two buttons at
- // the bottom: OK and Cancel. If Cancel is clicked, dialog goes away
- // and we're done. If OK is clicked, dialog goes away and the
- // callback function is called with the content as an argument (so
- // that the values of any fields can be captured).
- Schematic.prototype.dialog = function (title, content, callback) {
- // create the div for the top level of the dialog, add to DOM
- var dialog = document.createElement("div");
- dialog.sch = this;
- dialog.content = content;
- dialog.callback = callback;
-
- // look for property input fields in the content and give
- // them a keypress listener that interprets ENTER as
- // clicking OK.
- var plist = content.getElementsByClassName("property");
- for (var i = plist.length - 1; i >= 0; --i) {
- var field = plist[i];
- field.dialog = dialog; // help event handler find us...
- field.addEventListener("keypress", dialog_check_for_ENTER, false);
- }
-
- // div to hold the content
- var body = document.createElement("div");
- content.style.marginBotton = "5px";
- body.appendChild(content);
- body.style.padding = "5px";
- dialog.appendChild(body);
-
- var ok_button = document.createElement("span");
- ok_button.appendChild(document.createTextNode("OK"));
- ok_button.dialog = dialog; // for the handler to use
- ok_button.addEventListener("click", dialog_okay, false);
- ok_button.style.display = "inline";
- ok_button.style.border = "1px solid";
- ok_button.style.padding = "5px";
- ok_button.style.margin = "10px";
-
- var cancel_button = document.createElement("span");
- cancel_button.appendChild(document.createTextNode("Cancel"));
- cancel_button.dialog = dialog; // for the handler to use
- cancel_button.addEventListener("click", dialog_cancel, false);
- cancel_button.style.display = "inline";
- cancel_button.style.border = "1px solid";
- cancel_button.style.padding = "5px";
- cancel_button.style.margin = "10px";
-
- // div to hold the two buttons
- var buttons = document.createElement("div");
- buttons.style.textAlign = "center";
- buttons.appendChild(ok_button);
- buttons.appendChild(cancel_button);
- buttons.style.padding = "5px";
- buttons.style.margin = "10px";
- dialog.appendChild(buttons);
-
- // put into an overlay window
- this.window(title, dialog);
- };
-
- function dialog_cancel(event) {
- if (!event) event = window.event;
- var dialog = window.event ? event.srcElement.dialog : event.target.dialog;
-
- window_close(dialog.win);
- }
-
- function dialog_okay(event) {
- if (!event) event = window.event;
- var dialog = window.event ? event.srcElement.dialog : event.target.dialog;
-
- window_close(dialog.win);
-
- if (dialog.callback) dialog.callback(dialog.content);
- }
-
- // callback for keypress in input fields: if user typed ENTER, act
- // like they clicked OK button.
- function dialog_check_for_ENTER(event) {
- var key = window.event ? window.event.keyCode : event.keyCode;
- if (key == 13) dialog_okay(event);
- }
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Draggable, resizeable, closeable window
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- // build a 2-column HTML table from an associative array (keys as text in
- // column 1, values in column 2).
- function build_table(a) {
- var tbl = document.createElement("table");
-
- // build a row for each element in associative array
- for (var i in a) {
- var label = document.createTextNode(i + ": ");
- var col1 = document.createElement("td");
- col1.appendChild(label);
- var col2 = document.createElement("td");
- col2.appendChild(a[i]);
- var row = document.createElement("tr");
- row.appendChild(col1);
- row.appendChild(col2);
- row.style.verticalAlign = "center";
- tbl.appendChild(row);
- }
-
- return tbl;
- }
-
- function build_input(type, size, value) {
- var input = document.createElement("input");
- input.type = type;
- input.size = size;
- input.className = "property"; // make this easier to find later
- if (value == undefined) input.value = "";
- else input.value = value.toString();
- return input;
- }
-
- // build a select widget using the strings found in the options array
- function build_select(options, selected) {
- var select = document.createElement("select");
- for (var i = 0; i < options.length; i++) {
- var option = document.createElement("option");
- option.text = options[i];
- select.add(option);
- if (options[i] == selected) select.selectedIndex = i;
- }
- return select;
- }
-
- Schematic.prototype.window = function (title, content, offset) {
- // create the div for the top level of the window
- var win = document.createElement("div");
- win.sch = this;
- win.content = content;
- win.drag_x = undefined;
- win.draw_y = undefined;
-
- // div to hold the title
- var head = document.createElement("div");
- head.style.backgroundColor = "black";
- head.style.color = "white";
- head.style.textAlign = "center";
- head.style.padding = "5px";
- head.appendChild(document.createTextNode(title));
- head.win = win;
- win.head = head;
-
- var close_button = new Image();
- close_button.src = close_icon;
- close_button.style.cssFloat = "right";
- close_button.addEventListener("click", window_close_button, false);
- close_button.win = win;
- head.appendChild(close_button);
-
- win.appendChild(head);
-
- // capture mouse events in title bar
- head.addEventListener("mousedown", window_mouse_down, false);
-
- // div to hold the content
- //var body = document.createElement('div');
- //body.appendChild(content);
- win.appendChild(content);
- content.win = win; // so content can contact us
-
- // compute location relative to canvas
- if (offset == undefined) offset = 0;
- win.left = this.canvas.mouse_x + offset;
- win.top = this.canvas.mouse_y + offset;
-
- // add to DOM
- win.style.background = "white";
- win.style.position = "absolute";
- win.style.left = win.left + "px";
- win.style.top = win.top + "px";
- win.style.border = "2px solid";
-
- this.canvas.parentNode.insertBefore(win, this.canvas);
- bring_to_front(win, true);
- };
-
- // adjust zIndex of pop-up window so that it is in front
- function bring_to_front(win, insert) {
- var wlist = win.sch.window_list;
- var i = wlist.indexOf(win);
-
- // remove from current position (if any) in window list
- if (i != -1) wlist.splice(i, 1);
-
- // if requested, add to end of window list
- if (insert) wlist.push(win);
-
- // adjust all zIndex values
- for (i = 0; i < wlist.length; i += 1) wlist[i].style.zIndex = 1000 + i;
- }
-
- // close the window
- function window_close(win) {
- // remove the window from the top-level div of the schematic
- win.parentNode.removeChild(win);
-
- // remove from list of pop-up windows
- bring_to_front(win, false);
- }
-
- function window_close_button(event) {
- if (!event) event = window.event;
- var src = window.event ? event.srcElement : event.target;
- window_close(src.win);
- }
-
- // capture mouse events in title bar of window
- function window_mouse_down(event) {
- if (!event) event = window.event;
- var src = window.event ? event.srcElement : event.target;
- var win = src.win;
-
- bring_to_front(win, true);
-
- // add handlers to document so we capture them no matter what
- document.addEventListener("mousemove", window_mouse_move, false);
- document.addEventListener("mouseup", window_mouse_up, false);
- document.tracking_window = win;
-
- // remember where mouse is so we can compute dx,dy during drag
- win.drag_x = event.pageX;
- win.drag_y = event.pageY;
-
- return false;
- }
-
- function window_mouse_up(event) {
- var win = document.tracking_window;
-
- // show's over folks...
- document.removeEventListener("mousemove", window_mouse_move, false);
- document.removeEventListener("mouseup", window_mouse_up, false);
- document.tracking_window = undefined;
- win.drag_x = undefined;
- win.drag_y = undefined;
- return true; // consume event
- }
-
- function window_mouse_move(event) {
- var win = document.tracking_window;
-
- if (win.drag_x) {
- var dx = event.pageX - win.drag_x;
- var dy = event.pageY - win.drag_y;
-
- // move the window
- win.left += dx;
- win.top += dy;
- win.style.left = win.left + "px";
- win.style.top = win.top + "px";
-
- // update reference point
- win.drag_x += dx;
- win.drag_y += dy;
-
- return true; // consume event
- }
- }
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Toolbar
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- Schematic.prototype.add_tool = function (icon, tip, callback) {
- var tool, child, label, hidden;
-
- tool = document.createElement("button");
- child = document.createElement("img");
- label = document.createElement("span");
- hidden = document.createElement("span");
-
- tool.style.backgroundImage = "none";
- tool.setAttribute("title", tip);
- label.innerHTML = tip;
- label.classList.add("sr");
- hidden.setAttribute("aria-hidden", "true");
-
- if (icon.search("data:image") != -1) {
- child.setAttribute("src", icon);
- child.setAttribute("alt", "");
- tool.appendChild(child);
- } else {
- tool.style.font = "small-caps small sans-serif";
- hidden.innerHTML = icon;
- tool.appendChild(hidden);
- tool.appendChild(label);
- }
-
- // decorate tool
- tool.style.height = "32px";
- tool.style.width = "auto";
- tool.style.verticalAlign = "top";
-
- // set up event processing
- tool.addEventListener("mouseover", tool_enter, false);
- tool.addEventListener("mouseout", tool_leave, false);
- tool.addEventListener("click", tool_click, false);
-
- // add to toolbar
- tool.sch = this;
- tool.tip = tip;
- tool.callback = callback;
- this.toolbar.push(tool);
-
- tool.enabled = false;
-
- return tool;
- };
-
- Schematic.prototype.enable_tool = function (tname, which) {
- var tool = this.tools[tname];
-
- if (tool != undefined) {
- tool.removeAttribute("disabled");
- tool.enabled = which;
-
- // if disabling tool, remove border and tip
- if (!which) {
- tool.sch.message("");
- tool.setAttribute("disabled", "true");
- }
- }
- };
-
- // highlight tool button by turning on border, changing background
- function tool_enter(event) {
- if (!event) event = window.event;
- var tool = event.target;
- if (event.target.tagName.toLowerCase() == "img" || event.target.tagName.toLowerCase() == "span") {
- tool = event.target.parentNode;
- }
- if (tool.enabled) {
- tool.sch.message(tool.tip);
- }
- event.stopPropagation();
- }
-
- // unhighlight tool button by turning off border, reverting to normal background
- function tool_leave(event) {
- if (!event) event = window.event;
- var tool = event.target;
- if (event.target.tagName.toLowerCase() == "img" || event.target.tagName.toLowerCase() == "span") {
- tool = event.target.parentNode;
- }
- if (tool.enabled) {
- tool.sch.message("");
- }
- event.stopPropagation();
- }
-
- // handle click on a tool
- function tool_click(event) {
- if (!event) event = window.event;
- var tool = event.target;
- if (event.target.tagName.toLowerCase() == "img" || event.target.tagName.toLowerCase() == "span") {
- tool = event.target.parentNode;
- }
- if (tool.enabled) {
- tool.sch.canvas.relMouseCoords(event); // so we can position pop-up window correctly
- tool.callback.call(tool.sch);
- }
- event.stopPropagation();
- }
-
- var help_icon =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAABgFBMVEUARMwAZsz///8AM5kGqP+s1/8AeuARp/8AZ803qPsAXNYAdNwAZcxZuv9auf99xv8AdNoEd9uTzP8AYsgAivQTl/cAj/V9xf8Ad90AeN5PtP8AUMoAj/oRiOgprf84qfwAb9UAifAAdtwAf+UAgOYActgAk/oAie8cqP8AbtQAXtkXof8AatBNuP8AbdMAgukXcOEAXdcAlP0AhuwJmv0prv8YceGCyP8AbtUAZd8elvMAZcsAW9Ucpv8AjfM3sP8AiO4AWdNOuP8AddsPgeIAhOoAbdT///8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABH5RCxAAAASHRSTlP//////////////////////////////////////////////////////////////////////////////////////////////wCc7PJgAAAAAWJLR0R/SL9x5QAAAAlwSFlzAAAASAAAAEgARslrPgAAAAl2cEFnAAAAEAAAABAAXMatwwAAAK5JREFUGNM1zwcOwjAMBVAHQ9PFbIG27L333pve/0bEBr5kyf9JkWIIKYLDK3CNcZgUiE0QFIuuu89kBIHIDy5TUOmXSknBsFxDRAXMJ4FIZOeqA41lxQVB+w/1L9RyO+B+fLwJ7q/OOUfd9FvDkQJDmyyuoLqWGtsVBq3RnAHcUlXbNgSEWPY8n570dH2F6h94Sqe3BAdd7xKEKKVTACg4UuL3OMQoB/F3LRGF1w/Arhm2Q9w2ZQAAACV0RVh0Y3JlYXRlLWRhdGUAMjAwOC0xMC0yM1QxMTo1ODozNiswODowMKkTWd4AAAAldEVYdG1vZGlmeS1kYXRlADIwMDgtMTAtMjNUMTE6NTk6NTArMDg6MDC833hpAAAAAElFTkSuQmCC";
-
- var cut_icon =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH1QocFh0xaEFkXgAAArRJREFUOMuFk11Ik1EYx59z3nevr9vUfaXbbPgVaHjRVRB0YZRJV0XeZCIRaGmWWKhpgZAElaV9gYgQlBjoEPRKkCS6DAK1MG0zNvJj7zZ1m+51X+92zttNzmFa5+78/w8//s/znIMg5TzrfXIOAN7zPO9tunm7dI/Xz7LspTvNrbpUHadeGIYZu9XYrI1Go8t9/a87Uz0Fq7hw5nS55sWrnk8HAggh/E+HHdfV1lcQQo7t6E97HpeZc82m7ZCIKKUnDgRgjENLS7+AT0tDsVisdCcFy7JThYWF4HF7KKXU8a8EFTabDVZdK6iutr44kUic6nnePVBSUqJAgMHhdAAAWA8E3G299xljvLy4aAc+jUeSJB3X6/TXZAqwvrFGAWCiraXj4YEAAABKaeXCjwV5bc0DjTeaVPFEHIliEObm5iQA6Npb/xegraVjGmM8ZF+00WBwC2s0GhDcgizL8ru2lo7p/wL+pJianZnGTqcD0jkeMt8ORhBCb/arRXuFMaOxl1B6Pb65qSblZTIz+REVGHNAIHQLITRQ6fG07wsYM5k6437/g6MmEyQoRd6tTdkX3h5mZVRVkJ3D8BxHJVkG5/o6KLKyrla63UPJFsbN5hrJ5+sqNhrpwsrKVDASlgJBEdfEaU2UIzqWwTQQEhOO1dUPR/R6EvP5BsfN5t2XOmowCPNFRWSEYe4DAMxYLCGrUpnY8UcYhnzJNQcBAIYxbv+Wn09GDQZhd4ixmF6SJFJFyKMJleqlgmV5hLE9OWmOm1Hz6arJjIy+y5R2gyxTIMSwC+A4Qa1UMl/z8mImna5pXhC8iszMK8mPpNU2fHe5Ng4fOtQwa7HECKUYMA4AADAAANVarc/l95/0SxIbAJA5tfrsRUFI7twqiu7q7GyPNxDI8YfDGl8k4lOoVOetouj+DaDzOgfcNME8AAAAAElFTkSuQmCC";
-
- var copy_icon =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAd0lEQVQ4y9WTsQ7AIAhE7wj//69dHKWLJqSlFOtUFpXA8SAIbBrHaR9yAAA6L2bvGiRvPtltQa+OqMrFPCo1jFhoRytBmXgqUCH5GUEkWCbova8TeBORfBNJVpYIrbVJdwDjY5hjJfk4vFnAzMDxiEqmo/fJAHACspMyA7UYnWgAAAAASUVORK5CYII=";
-
- var paste_icon =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAABZElEQVQ4y6WSMUsDMRiGn6RteoJSENS5oCdFR3+D0MHBciA4uujiVrq0o1NBcBEEFycXRzcnf4GDINTiL7BTr9LeJdfGoaX0ei21+MFHAsn7fG9eAjOqXCwoz/NKgAWs53mlcrGgZt0VE3s7fdhsfgHguttztTHA5+0ZjUaDzdM7HEeRy60C0G7/EASa78cLXNelcPkw1qYnkVprfN/n+6aEUgqlFFJKjDForclms2itYzZigH6/Tz6fp9PpAFC8fp3h/J2rw42P2ksLADkNMMbgOA6O4wzfZW2sAWovrb3janUn4cAYgzGGRWWtRQjRPKpUdmOAKIrGgCiK5gKEGGb/XK9/JhwEQUC32yUMw7nTJ0ExQK/Xw/d9BoPBeMqiigHCMEQIQSqV+pM4AbCAXEKcAGAtKSn/AYCE/UVZpIEVYA1ASkkmkxl/mqfzg5ExG1tP7t8AtoAOwDqwP4pgmd4H1n8B+QWeF/d+HLAAAAAASUVORK5CYII=";
-
- var close_icon =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACOUlEQVR42m2TzWtTQRTF30ysxE9E0VZKaapBa1OJxtRAW7XVGOrCKijFIEqXbgQFwf/BTReia7GUuBEUF5Y2YiMKKn6iKEqRom1DtAsjStGaGX9v3iQ+Y4Z35t6ZOfe8uXdmhFPVDqRSktYvpBzAxphaBQpKqSdalTLYO7fHxnWZL/zBfclkWAbksBBip7cmHEGvDd3raFlVUoOj2Wz+H4H9vT0RGQhMMLHGkxaienduuJV6p0ql5PjdiRlD2rurO0jwM9zWWnG1dPhyWql9ht3T1XmKwEtAaLNVMUT3AFyBsAz7g9lBbBc4+zcb54joTnQIKeRDZjrcvyNQxG9A5Bd2NwIZ+Gn8e2AxmHNFbTbXRWc8FoRcJLjObtDtbjB3DLtALZbws3l8d/0aOOzLZlYktkfX4eS9epuiehlqZ+TRi5cny8zEtuhVVo/7eabW8a2RFdivwlbPbu079uDT129yZYEd7W17oNzCXe4rdFHE2lrd0SRosbX5TXAK5EAd5NPYi9gF0AtGwSIrcN9IRTeFLxB8zp7RPAExMAUxAw7h3wRpdh+SQjzHBm0KZ4xA+8aWRgivzLU16TvuLZsB8UqyjvMYNDOu98rgfEQ8UklmS6hpQCs9ghuwdShfSKF9Ezb/n5x939upT7mKwOamRogqjchlhit9R+XbhGlfeGgn3k/Pjv33mNwWXl8f4sWdJ+Yow9W+JTetYSkDQ5P5wuear7HcNjSs5Upqd60ZLAXfwPSHwpyu5v4BhpTicEl0i9QAAAAASUVORK5CYII=";
-
- var grid_icon =
- "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAsSAAALEgHS3X78AAAAMklEQVQ4y2NkYGD4z0ABYGFgYGD4/x/VDEZGRqLFmCixnYGBYRAYwMgwGoijgTgsAhEAq84fH/l+ELYAAAAASUVORK5CYII=";
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Graphing
- //
- ///////////////////////////////////////////////////////////////////////////////
-
- // add dashed lines!
- // from http://davidowens.wordpress.com/2010/09/07/html-5-canvas-and-dashed-lines/
- try {
- if (CanvasRenderingContext2D)
- CanvasRenderingContext2D.prototype.dashedLineTo = function (fromX, fromY, toX, toY, pattern) {
- // Our growth rate for our line can be one of the following:
- // (+,+), (+,-), (-,+), (-,-)
- // Because of this, our algorithm needs to understand if the x-coord and
- // y-coord should be getting smaller or larger and properly cap the values
- // based on (x,y).
- var lt = function (a, b) {
- return a <= b;
- };
- var gt = function (a, b) {
- return a >= b;
- };
- var capmin = function (a, b) {
- return Math.min(a, b);
- };
- var capmax = function (a, b) {
- return Math.max(a, b);
- };
- var checkX = { thereYet: gt, cap: capmin };
- var checkY = { thereYet: gt, cap: capmin };
-
- if (fromY - toY > 0) {
- checkY.thereYet = lt;
- checkY.cap = capmax;
- }
- if (fromX - toX > 0) {
- checkX.thereYet = lt;
- checkX.cap = capmax;
- }
-
- this.moveTo(fromX, fromY);
- var offsetX = fromX;
- var offsetY = fromY;
- var idx = 0,
- dash = true;
- while (!(checkX.thereYet(offsetX, toX) && checkY.thereYet(offsetY, toY))) {
- var ang = Math.atan2(toY - fromY, toX - fromX);
- var len = pattern[idx];
-
- offsetX = checkX.cap(toX, offsetX + Math.cos(ang) * len);
- offsetY = checkY.cap(toY, offsetY + Math.sin(ang) * len);
-
- if (dash) this.lineTo(offsetX, offsetY);
- else this.moveTo(offsetX, offsetY);
-
- idx = (idx + 1) % pattern.length;
- dash = !dash;
- }
- };
- } catch (err) {
- //noop
- }
- // given a range of values, return a new range [vmin',vmax'] where the limits
- // have been chosen "nicely". Taken from matplotlib.ticker.LinearLocator
- function view_limits(vmin, vmax) {
- // deal with degenerate case...
- if (vmin == vmax) {
- if (vmin == 0) {
- vmin = -0.5;
- vmax = 0.5;
- } else {
- vmin = vmin > 0 ? 0.9 * vmin : 1.1 * vmin;
- vmax = vmax > 0 ? 1.1 * vmax : 0.9 * vmax;
- }
- }
-
- var log_range = Math.log(vmax - vmin) / Math.LN10;
- var exponent = Math.floor(log_range);
- //if (log_range - exponent < 0.5) exponent -= 1;
- var scale = Math.pow(10, -exponent);
- vmin = Math.floor(scale * vmin) / scale;
- vmax = Math.ceil(scale * vmax) / scale;
-
- return [vmin, vmax, 1.0 / scale];
- }
-
- function engineering_notation(n, nplaces, trim) {
- if (n == 0) return "0";
- if (n == undefined) return "undefined";
- if (trim == undefined) trim = true;
-
- var sign = n < 0 ? -1 : 1;
- var log10 = Math.log(sign * n) / Math.LN10;
- var exp = Math.floor(log10 / 3); // powers of 1000
- var mantissa = sign * Math.pow(10, log10 - 3 * exp);
-
- // keep specified number of places following decimal point
- var mstring = (mantissa + sign * 0.5 * Math.pow(10, -nplaces)).toString();
- var mlen = mstring.length;
- var endindex = mstring.indexOf(".");
- if (endindex != -1) {
- if (nplaces > 0) {
- endindex += nplaces + 1;
- if (endindex > mlen) endindex = mlen;
- if (trim) {
- while (mstring.charAt(endindex - 1) == "0") endindex -= 1;
- if (mstring.charAt(endindex - 1) == ".") endindex -= 1;
- }
- }
- if (endindex < mlen) mstring = mstring.substring(0, endindex);
- }
-
- switch (exp) {
- case -5:
- return mstring + "f";
- case -4:
- return mstring + "p";
- case -3:
- return mstring + "n";
- case -2:
- return mstring + "u";
- case -1:
- return mstring + "m";
- case 0:
- return mstring;
- case 1:
- return mstring + "K";
- case 2:
- return mstring + "M";
- case 3:
- return mstring + "G";
- }
-
- // don't have a good suffix, so just print the number
- return n.toString();
- }
-
- var grid_pattern = [1, 2];
- var cursor_pattern = [5, 5];
-
- // x_values is an array of x coordinates for each of the plots
- // y_values is an array of [color, value_array], one entry for each plot on left vertical axis
- // z_values is an array of [color, value_array], one entry for each plot on right vertical axis
- Schematic.prototype.graph = function (x_values, x_legend, y_values, y_legend, z_values, z_legend) {
- var pwidth = 400; // dimensions of actual plot
- var pheight = 300; // dimensions of actual plot
- var left_margin = y_values != undefined && y_values.length > 0 ? 55 : 25;
- var top_margin = 25;
- var right_margin = z_values != undefined && z_values.length > 0 ? 55 : 25;
- var bottom_margin = 45;
- var tick_length = 5;
-
- var w = pwidth + left_margin + right_margin;
- var h = pheight + top_margin + bottom_margin;
-
- var canvas = document.createElement("canvas");
- canvas.width = w;
- canvas.height = h;
-
- // the graph itself will be drawn here and this image will be copied
- // onto canvas, where it can be overlayed with mouse cursors, etc.
- var bg_image = document.createElement("canvas");
- bg_image.width = w;
- bg_image.height = h;
- canvas.bg_image = bg_image; // so we can find it during event handling
-
- // start by painting an opaque background
- var c = bg_image.getContext("2d");
- c.fillStyle = background_style;
- c.fillRect(0, 0, w, h);
- c.fillStyle = element_style;
- c.fillRect(left_margin, top_margin, pwidth, pheight);
-
- // figure out scaling for plots
- var x_min = array_min(x_values);
- var x_max = array_max(x_values);
- var x_limits = view_limits(x_min, x_max);
- x_min = x_limits[0];
- x_max = x_limits[1];
- var x_scale = pwidth / (x_max - x_min);
-
- function plot_x(x) {
- return (x - x_min) * x_scale + left_margin;
- }
-
- // draw x grid
- c.strokeStyle = grid_style;
- c.lineWidth = 1;
- c.fillStyle = normal_style;
- c.font = "10pt sans-serif";
- c.textAlign = "center";
- c.textBaseline = "top";
- var end = top_margin + pheight;
- for (var x = x_min; x <= x_max; x += x_limits[2]) {
- var temp = plot_x(x) + 0.5; // keep lines crisp!
-
- // grid line
- c.beginPath();
- if (x == x_min) {
- c.moveTo(temp, top_margin);
- c.lineTo(temp, end);
- } else c.dashedLineTo(temp, top_margin, temp, end, grid_pattern);
- c.stroke();
-
- // tick mark
- c.beginPath();
- c.moveTo(temp, end);
- c.lineTo(temp, end + tick_length);
- c.stroke();
- c.fillText(engineering_notation(x, 2), temp, end + tick_length);
- }
-
- if (y_values != undefined && y_values.length > 0) {
- var y_min = Infinity;
- var y_max = -Infinity;
- var plot;
- for (plot = y_values.length - 1; plot >= 0; --plot) {
- var values = y_values[plot][2];
- if (values == undefined) continue; // no data points
- var offset = y_values[plot][1];
- var temp = array_min(values) + offset;
- if (temp < y_min) y_min = temp;
- temp = array_max(values) + offset;
- if (temp > y_max) y_max = temp;
- }
- var y_limits = view_limits(y_min, y_max);
- y_min = y_limits[0];
- y_max = y_limits[1];
- var y_scale = pheight / (y_max - y_min);
-
- function plot_y(y) {
- return (y_max - y) * y_scale + top_margin;
- }
-
- // draw y grid
- c.textAlign = "right";
- c.textBaseline = "middle";
- for (var y = y_min; y <= y_max; y += y_limits[2]) {
- if (Math.abs(y / y_max) < 0.001) y = 0.0; // Just 3 digits
- var temp = plot_y(y) + 0.5; // keep lines crisp!
-
- // grid line
- c.beginPath();
- if (y == y_min) {
- c.moveTo(left_margin, temp);
- c.lineTo(left_margin + pwidth, temp);
- } else c.dashedLineTo(left_margin, temp, left_margin + pwidth, temp, grid_pattern);
- c.stroke();
-
- // tick mark
- c.beginPath();
- c.moveTo(left_margin - tick_length, temp);
- c.lineTo(left_margin, temp);
- c.stroke();
- c.fillText(engineering_notation(y, 2), left_margin - tick_length - 2, temp);
- }
-
- // now draw each plot
- var x, y;
- var nx, ny;
- c.lineWidth = 3;
- c.lineCap = "round";
- for (plot = y_values.length - 1; plot >= 0; --plot) {
- var color = probe_colors_rgb[y_values[plot][0]];
- if (color == undefined) continue; // no plot color (== x-axis)
- c.strokeStyle = color;
- var values = y_values[plot][2];
- if (values == undefined) continue; // no data points
- var offset = y_values[plot][1];
-
- x = plot_x(x_values[0]);
- y = plot_y(values[0] + offset);
- c.beginPath();
- c.moveTo(x, y);
- for (var i = 1; i < x_values.length; i++) {
- nx = plot_x(x_values[i]);
- ny = plot_y(values[i] + offset);
- c.lineTo(nx, ny);
- x = nx;
- y = ny;
- if (i % 100 == 99) {
- // too many lineTo's cause canvas to break
- c.stroke();
- c.beginPath();
- c.moveTo(x, y);
- }
- }
- c.stroke();
- }
- }
-
- if (z_values != undefined && z_values.length > 0) {
- var z_min = Infinity;
- var z_max = -Infinity;
- for (plot = z_values.length - 1; plot >= 0; --plot) {
- var values = z_values[plot][2];
- if (values == undefined) continue; // no data points
- var offset = z_values[plot][1];
- var temp = array_min(values) + offset;
- if (temp < z_min) z_min = temp;
- temp = array_max(values) + offset;
- if (temp > z_max) z_max = temp;
- }
- var z_limits = view_limits(z_min, z_max);
- z_min = z_limits[0];
- z_max = z_limits[1];
- var z_scale = pheight / (z_max - z_min);
-
- function plot_z(z) {
- return (z_max - z) * z_scale + top_margin;
- }
-
- // draw z ticks
- c.textAlign = "left";
- c.textBaseline = "middle";
- c.lineWidth = 1;
- c.strokeStyle = normal_style;
- var tick_length_half = Math.floor(tick_length / 2);
- var tick_delta = tick_length - tick_length_half;
- for (var z = z_min; z <= z_max; z += z_limits[2]) {
- if (Math.abs(z / z_max) < 0.001) z = 0.0; // Just 3 digits
- var temp = plot_z(z) + 0.5; // keep lines crisp!
-
- // tick mark
- c.beginPath();
- c.moveTo(left_margin + pwidth - tick_length_half, temp);
- c.lineTo(left_margin + pwidth + tick_delta, temp);
- c.stroke();
- c.fillText(engineering_notation(z, 2), left_margin + pwidth + tick_length + 2, temp);
- }
-
- var z;
- var nz;
- c.lineWidth = 3;
- for (plot = z_values.length - 1; plot >= 0; --plot) {
- var color = probe_colors_rgb[z_values[plot][0]];
- if (color == undefined) continue; // no plot color (== x-axis)
- c.strokeStyle = color;
- var values = z_values[plot][2];
- if (values == undefined) continue; // no data points
- var offset = z_values[plot][1];
-
- x = plot_x(x_values[0]);
- z = plot_z(values[0] + offset);
- c.beginPath();
- c.moveTo(x, z);
- for (var i = 1; i < x_values.length; i++) {
- nx = plot_x(x_values[i]);
- nz = plot_z(values[i] + offset);
- c.lineTo(nx, nz);
- x = nx;
- z = nz;
- if (i % 100 == 99) {
- // too many lineTo's cause canvas to break
- c.stroke();
- c.beginPath();
- c.moveTo(x, z);
- }
- }
- c.stroke();
- }
- }
-
- // draw legends
- c.font = "12pt sans-serif";
- c.textAlign = "center";
- c.textBaseline = "bottom";
- c.fillText(x_legend, left_margin + pwidth / 2, h - 5);
-
- if (y_values != undefined && y_values.length > 0) {
- c.textBaseline = "top";
- c.save();
- c.translate(5, top_margin + pheight / 2);
- c.rotate(-Math.PI / 2);
- c.fillText(y_legend, 0, 0);
- c.restore();
- }
-
- if (z_values != undefined && z_values.length > 0) {
- c.textBaseline = "bottom";
- c.save();
- c.translate(w - 5, top_margin + pheight / 2);
- c.rotate(-Math.PI / 2);
- c.fillText(z_legend, 0, 0);
- c.restore();
- }
-
- // save info need for interactions with the graph
- canvas.x_values = x_values;
- canvas.y_values = y_values;
- canvas.z_values = z_values;
- canvas.x_legend = x_legend;
- canvas.y_legend = y_legend;
- canvas.z_legend = y_legend;
- canvas.x_min = x_min;
- canvas.x_scale = x_scale;
- canvas.y_min = y_min;
- canvas.y_scale = y_scale;
- canvas.z_min = z_min;
- canvas.z_scale = z_scale;
- canvas.left_margin = left_margin;
- canvas.top_margin = top_margin;
- canvas.pwidth = pwidth;
- canvas.pheight = pheight;
- canvas.tick_length = tick_length;
-
- canvas.cursor1_x = undefined;
- canvas.cursor2_x = undefined;
- canvas.sch = this;
-
- // do something useful when user mouses over graph
- canvas.addEventListener("mousemove", graph_mouse_move, false);
-
- // return our masterpiece
- redraw_plot(canvas);
- return canvas;
- };
-
- function array_max(a) {
- var max = -Infinity;
- for (var i = a.length - 1; i >= 0; --i) if (a[i] > max) max = a[i];
- return max;
- }
-
- function array_min(a) {
- var min = Infinity;
- for (var i = a.length - 1; i >= 0; --i) if (a[i] < min) min = a[i];
- return min;
- }
-
- function plot_cursor(c, graph, cursor_x, left_margin) {
- // draw dashed vertical marker that follows mouse
- var x = graph.left_margin + cursor_x;
- var end_y = graph.top_margin + graph.pheight + graph.tick_length;
- c.strokeStyle = grid_style;
- c.lineWidth = 1;
- c.beginPath();
- c.dashedLineTo(x, graph.top_margin, x, end_y, cursor_pattern);
- c.stroke();
-
- // add x label at bottom of marker
- var graph_x = cursor_x / graph.x_scale + graph.x_min;
- c.font = "10pt sans-serif";
- c.textAlign = "center";
- c.textBaseline = "top";
- c.fillStyle = background_style;
- c.fillText("\u2588\u2588\u2588\u2588\u2588", x, end_y);
- c.fillStyle = normal_style;
- c.fillText(engineering_notation(graph_x, 3, false), x, end_y);
-
- // compute which points marker is between
- var x_values = graph.x_values;
- var len = x_values.length;
- var index = 0;
- while (index < len && graph_x >= x_values[index]) index += 1;
- var x1 = index == 0 ? x_values[0] : x_values[index - 1];
- var x2 = x_values[index];
-
- if (x2 != undefined) {
- // for each plot, interpolate and output value at intersection with marker
- c.textAlign = "left";
- var tx = graph.left_margin + left_margin;
- var ty = graph.top_margin;
- if (graph.y_values != undefined) {
- for (var plot = 0; plot < graph.y_values.length; plot++) {
- var values = graph.y_values[plot][2];
- var color = probe_colors_rgb[graph.y_values[plot][0]];
- if (values == undefined || color == undefined) continue; // no data points or x-axis
-
- // interpolate signal value at graph_x using values[index-1] and values[index]
- var y1 = index == 0 ? values[0] : values[index - 1];
- var y2 = values[index];
- var y = y1;
- if (graph_x != x1) y += ((graph_x - x1) * (y2 - y1)) / (x2 - x1);
-
- // annotate plot with value of signal at marker
- c.fillStyle = element_style;
- c.fillText("\u2588\u2588\u2588\u2588\u2588", tx - 3, ty);
- c.fillStyle = color;
- c.fillText(engineering_notation(y, 3, false), tx, ty);
- ty += 14;
- }
- }
-
- c.textAlign = "right";
- if (graph.z_values != undefined) {
- var tx = graph.left_margin + graph.pwidth - left_margin;
- var ty = graph.top_margin;
- for (var plot = 0; plot < graph.z_values.length; plot++) {
- var values = graph.z_values[plot][2];
- var color = probe_colors_rgb[graph.z_values[plot][0]];
- if (values == undefined || color == undefined) continue; // no data points or x-axis
-
- // interpolate signal value at graph_x using values[index-1] and values[index]
- var z1 = index == 0 ? values[0] : values[index - 1];
- var z2 = values[index];
- var z = z1;
- if (graph_x != x1) z += ((graph_x - x1) * (z2 - z1)) / (x2 - x1);
-
- // annotate plot with value of signal at marker
- c.fillStyle = element_style;
- c.fillText("\u2588\u2588\u2588\u2588\u2588", tx + 3, ty);
- c.fillStyle = color;
- c.fillText(engineering_notation(z, 3, false), tx, ty);
- ty += 14;
- }
- }
- }
- }
-
- function redraw_plot(graph) {
- var c = graph.getContext("2d");
- c.drawImage(graph.bg_image, 0, 0);
-
- if (graph.cursor1_x != undefined) plot_cursor(c, graph, graph.cursor1_x, 4);
- if (graph.cursor2_x != undefined) plot_cursor(c, graph, graph.cursor2_x, 30);
-
- /*
- if (graph.cursor1_x != undefined) {
- // draw dashed vertical marker that follows mouse
- var x = graph.left_margin + graph.cursor1_x;
- var end_y = graph.top_margin + graph.pheight + graph.tick_length;
- c.strokeStyle = grid_style;
- c.lineWidth = 1;
- c.beginPath();
- c.dashedLineTo(x,graph.top_margin,x,end_y,cursor_pattern);
- c.stroke();
-
- // add x label at bottom of marker
- var graph_x = graph.cursor1_x/graph.x_scale + graph.x_min;
- c.font = '10pt sans-serif';
- c.textAlign = 'center';
- c.textBaseline = 'top';
- c.fillStyle = background_style;
- c.fillText('\u2588\u2588\u2588\u2588\u2588',x,end_y);
- c.fillStyle = normal_style;
- c.fillText(engineering_notation(graph_x,3,false),x,end_y);
-
- // compute which points marker is between
- var x_values = graph.x_values;
- var len = x_values.length;
- var index = 0;
- while (index < len && graph_x >= x_values[index]) index += 1;
- var x1 = (index == 0) ? x_values[0] : x_values[index-1];
- var x2 = x_values[index];
-
- if (x2 != undefined) {
- // for each plot, interpolate and output value at intersection with marker
- c.textAlign = 'left';
- var tx = graph.left_margin + 4;
- var ty = graph.top_margin;
- for (var plot = 0; plot < graph.y_values.length; plot++) {
- var values = graph.y_values[plot][1];
-
- // interpolate signal value at graph_x using values[index-1] and values[index]
- var y1 = (index == 0) ? values[0] : values[index-1];
- var y2 = values[index];
- var y = y1;
- if (graph_x != x1) y += (graph_x - x1)*(y2 - y1)/(x2 - x1);
-
- // annotate plot with value of signal at marker
- c.fillStyle = element_style;
- c.fillText('\u2588\u2588\u2588\u2588\u2588',tx-3,ty);
- c.fillStyle = probe_colors_rgb[graph.y_values[plot][0]];
- c.fillText(engineering_notation(y,3,false),tx,ty);
- ty += 14;
- }
- }
- }
- */
- }
-
- function graph_mouse_move(event) {
- if (!event) event = window.event;
- var g = window.event ? event.srcElement : event.target;
-
- g.relMouseCoords(event);
- // not sure yet where the 3,-3 offset correction comes from (borders? padding?)
- var gx = g.mouse_x - g.left_margin - 3;
- var gy = g.pheight - (g.mouse_y - g.top_margin) + 3;
- if (gx >= 0 && gx <= g.pwidth && gy >= 0 && gy <= g.pheight) {
- //g.sch.message('button: '+event.button+', which: '+event.which);
- g.cursor1_x = gx;
- } else {
- g.cursor1_x = undefined;
- g.cursor2_x = undefined;
- }
-
- redraw_plot(g);
- }
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Parts bin
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- // one instance will be created for each part in the parts bin
- function Part(sch) {
- this.sch = sch;
- this.component = undefined;
- this.selected = false;
-
- // set up canvas
- this.canvas = document.createElement("canvas");
- this.canvas.style.borderStyle = "solid";
- this.canvas.style.borderWidth = "1px";
- this.canvas.style.borderColor = background_style;
- //this.canvas.style.position = 'absolute';
- this.canvas.style.cursor = "default";
- this.canvas.height = part_w;
- this.canvas.width = part_h;
- this.canvas.xpart = this;
-
- this.canvas.addEventListener("mouseover", part_enter, false);
- this.canvas.addEventListener("mouseout", part_leave, false);
- this.canvas.addEventListener("mousedown", part_mouse_down, false);
- this.canvas.addEventListener("mouseup", part_mouse_up, false);
-
- // make the part "clickable" by registering a dummy click handler
- // this should make things work on the iPad
- this.canvas.addEventListener("click", function () {}, false);
- }
-
- Part.prototype.set_location = function (left, top) {
- this.canvas.style.left = left + "px";
- this.canvas.style.top = top + "px";
- };
-
- Part.prototype.right = function () {
- return this.canvas.offsetLeft + this.canvas.offsetWidth;
- };
-
- Part.prototype.bottom = function () {
- return this.canvas.offsetTop + this.canvas.offsetHeight;
- };
-
- Part.prototype.set_component = function (component, tip) {
- component.sch = this;
- this.component = component;
- this.tip = tip;
-
- // figure out scaling and centering of parts icon
- var b = component.bounding_box;
- var dx = b[2] - b[0];
- var dy = b[3] - b[1];
- this.scale = 0.8; //Math.min(part_w/(1.2*dx),part_h/(1.2*dy));
- this.origin_x = b[0] + dx / 2.0 - part_w / (2.0 * this.scale);
- this.origin_y = b[1] + dy / 2.0 - part_h / (2.0 * this.scale);
-
- this.redraw();
- };
-
- Part.prototype.redraw = function (part) {
- var c = this.canvas.getContext("2d");
-
- // paint background color
- c.fillStyle = this.selected ? selected_style : background_style;
- c.fillRect(0, 0, part_w, part_h);
-
- if (this.component) this.component.draw(c);
- };
-
- Part.prototype.select = function (which) {
- this.selected = which;
- this.redraw();
- };
-
- Part.prototype.update_connection_point = function (cp, old_location) {
- // no connection points in the parts bin
- };
-
- Part.prototype.moveTo = function (c, x, y) {
- c.moveTo((x - this.origin_x) * this.scale, (y - this.origin_y) * this.scale);
- };
-
- Part.prototype.lineTo = function (c, x, y) {
- c.lineTo((x - this.origin_x) * this.scale, (y - this.origin_y) * this.scale);
- };
-
- Part.prototype.draw_line = function (c, x1, y1, x2, y2, width) {
- c.lineWidth = width * this.scale;
- c.beginPath();
- c.moveTo((x1 - this.origin_x) * this.scale, (y1 - this.origin_y) * this.scale);
- c.lineTo((x2 - this.origin_x) * this.scale, (y2 - this.origin_y) * this.scale);
- c.stroke();
- };
-
- Part.prototype.draw_arc = function (c, x, y, radius, start_radians, end_radians, anticlockwise, width, filled) {
- c.lineWidth = width * this.scale;
- c.beginPath();
- c.arc(
- (x - this.origin_x) * this.scale,
- (y - this.origin_y) * this.scale,
- radius * this.scale,
- start_radians,
- end_radians,
- anticlockwise,
- );
- if (filled) c.fill();
- else c.stroke();
- };
-
- Part.prototype.draw_text = function (c, text, x, y, size) {
- // no text displayed for the parts icon
- };
-
- function part_enter(event) {
- if (!event) event = window.event;
- var canvas = window.event ? event.srcElement : event.target;
- var part = canvas.xpart;
-
- // avoid Chrome bug that changes to text cursor whenever
- // drag starts. We'll restore the default handler at
- // the appropriate point so behavior in other parts of
- // the document are unaffected.
- //part.sch.saved_onselectstart = document.onselectstart;
- //document.onselectstart = function () { return false; };
-
- canvas.style.borderColor = normal_style;
- part.sch.message(part.tip + ": drag onto diagram to insert");
- return false;
- }
-
- function part_leave(event) {
- if (!event) event = window.event;
- var canvas = window.event ? event.srcElement : event.target;
- var part = canvas.xpart;
-
- if (typeof part.sch.new_part == "undefined") {
- // leaving with no part selected? revert handler
- //document.onselectstart = part.sch.saved_onselectstart;
- }
-
- canvas.style.borderColor = background_style;
- part.sch.message("");
- return false;
- }
-
- function part_mouse_down(event) {
- if (!event) event = window.event;
- var part = window.event ? event.srcElement.xpart : event.target.xpart;
-
- part.select(true);
- part.sch.new_part = part;
- return false;
- }
-
- function part_mouse_up(event) {
- if (!event) event = window.event;
- var part = window.event ? event.srcElement.xpart : event.target.xpart;
-
- part.select(false);
- part.sch.new_part = undefined;
- return false;
- }
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Rectangle helper functions
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- // rect is an array of the form [left,top,right,bottom]
-
- // ensure left < right, top < bottom
- function canonicalize(r) {
- var temp;
-
- // canonicalize bounding box
- if (r[0] > r[2]) {
- temp = r[0];
- r[0] = r[2];
- r[2] = temp;
- }
- if (r[1] > r[3]) {
- temp = r[1];
- r[1] = r[3];
- r[3] = temp;
- }
- }
-
- function between(x, x1, x2) {
- return x1 <= x && x <= x2;
- }
-
- function inside(rect, x, y) {
- return between(x, rect[0], rect[2]) && between(y, rect[1], rect[3]);
- }
-
- // only works for manhattan rectangles
- function intersect(r1, r2) {
- // look for non-intersection, negate result
- var result = !(r2[0] > r1[2] || r2[2] < r1[0] || r2[1] > r1[3] || r2[3] < r1[1]);
-
- // if I try to return the above expression, javascript returns undefined!!!
- return result;
- }
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Component base class
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Component(type, x, y, rotation) {
- this.sch = undefined;
- this.type = type;
- this.x = x;
- this.y = y;
- this.rotation = rotation;
- this.selected = false;
- this.properties = [];
- this.bounding_box = [0, 0, 0, 0]; // in device coords [left,top,right,bottom]
- this.bbox = this.bounding_box; // in absolute coords
- this.connections = [];
- }
-
- Component.prototype.json = function (index) {
- this.properties["_json_"] = index; // remember where we are in the JSON list
-
- var props = {};
- for (var p in this.properties) props[p] = this.properties[p];
-
- var conns = [];
- for (var i = 0; i < this.connections.length; i++) conns.push(this.connections[i].json());
-
- var json = [this.type, [this.x, this.y, this.rotation], props, conns];
- return json;
- };
-
- Component.prototype.add_connection = function (offset_x, offset_y) {
- this.connections.push(new ConnectionPoint(this, offset_x, offset_y));
- };
-
- Component.prototype.update_coords = function () {
- var x = this.x;
- var y = this.y;
-
- // update bbox
- var b = this.bounding_box;
- this.bbox[0] = this.transform_x(b[0], b[1]) + x;
- this.bbox[1] = this.transform_y(b[0], b[1]) + y;
- this.bbox[2] = this.transform_x(b[2], b[3]) + x;
- this.bbox[3] = this.transform_y(b[2], b[3]) + y;
- canonicalize(this.bbox);
-
- // update connections
- for (var i = this.connections.length - 1; i >= 0; --i) this.connections[i].update_location();
- };
-
- Component.prototype.rotate = function (amount) {
- var old_rotation = this.rotation;
- this.rotation = (this.rotation + amount) % 8;
- this.update_coords();
-
- // create an undoable edit record here
- // using old_rotation
- };
-
- Component.prototype.move_begin = function () {
- // remember where we started this move
- this.move_x = this.x;
- this.move_y = this.y;
- };
-
- Component.prototype.move = function (dx, dy) {
- // update coordinates
- this.x += dx;
- this.y += dy;
- this.update_coords();
- };
-
- Component.prototype.move_end = function () {
- var dx = this.x - this.move_x;
- var dy = this.y - this.move_y;
-
- if (dx != 0 || dy != 0) {
- // create an undoable edit record here
-
- this.sch.check_wires(this);
- }
- };
-
- Component.prototype.add = function (sch) {
- this.sch = sch; // we now belong to a schematic!
- sch.add_component(this);
- this.update_coords();
- };
-
- Component.prototype.remove = function () {
- // remove connection points from schematic
- for (var i = this.connections.length - 1; i >= 0; --i) {
- var cp = this.connections[i];
- this.sch.remove_connection_point(cp, cp.location);
- }
-
- // remove component from schematic
- this.sch.remove_component(this);
- this.sch = undefined;
-
- // create an undoable edit record here
- };
-
- Component.prototype.transform_x = function (x, y) {
- var rot = this.rotation;
- if (rot == 0 || rot == 6) return x;
- else if (rot == 1 || rot == 5) return -y;
- else if (rot == 2 || rot == 4) return -x;
- else return y;
- };
-
- Component.prototype.transform_y = function (x, y) {
- var rot = this.rotation;
- if (rot == 1 || rot == 7) return x;
- else if (rot == 2 || rot == 6) return -y;
- else if (rot == 3 || rot == 5) return -x;
- else return y;
- };
-
- Component.prototype.moveTo = function (c, x, y) {
- var nx = this.transform_x(x, y) + this.x;
- var ny = this.transform_y(x, y) + this.y;
- this.sch.moveTo(c, nx, ny);
- };
-
- Component.prototype.lineTo = function (c, x, y) {
- var nx = this.transform_x(x, y) + this.x;
- var ny = this.transform_y(x, y) + this.y;
- this.sch.lineTo(c, nx, ny);
- };
-
- Component.prototype.draw_line = function (c, x1, y1, x2, y2) {
- c.strokeStyle = this.selected ? selected_style : this.type == "w" ? normal_style : component_style;
- var nx1 = this.transform_x(x1, y1) + this.x;
- var ny1 = this.transform_y(x1, y1) + this.y;
- var nx2 = this.transform_x(x2, y2) + this.x;
- var ny2 = this.transform_y(x2, y2) + this.y;
- this.sch.draw_line(c, nx1, ny1, nx2, ny2, 1);
- };
-
- Component.prototype.draw_circle = function (c, x, y, radius, filled) {
- if (filled) c.fillStyle = this.selected ? selected_style : normal_style;
- else c.strokeStyle = this.selected ? selected_style : this.type == "w" ? normal_style : component_style;
- var nx = this.transform_x(x, y) + this.x;
- var ny = this.transform_y(x, y) + this.y;
-
- this.sch.draw_arc(c, nx, ny, radius, 0, 2 * Math.PI, false, 1, filled);
- };
-
- var rot_angle = [
- 0.0, // NORTH (identity)
- Math.PI / 2, // EAST (rot270)
- Math.PI, // SOUTH (rot180)
- (3 * Math.PI) / 2, // WEST (rot90)
- 0.0, // RNORTH (negy)
- Math.PI / 2, // REAST (int-neg)
- Math.PI, // RSOUTH (negx)
- (3 * Math.PI) / 2, // RWEST (int-pos)
- ];
-
- Component.prototype.draw_arc = function (c, x, y, radius, start_radians, end_radians) {
- c.strokeStyle = this.selected ? selected_style : this.type == "w" ? normal_style : component_style;
- var nx = this.transform_x(x, y) + this.x;
- var ny = this.transform_y(x, y) + this.y;
- this.sch.draw_arc(
- c,
- nx,
- ny,
- radius,
- start_radians + rot_angle[this.rotation],
- end_radians + rot_angle[this.rotation],
- false,
- 1,
- false,
- );
- };
-
- Component.prototype.draw = function (c) {
- /*
- for (var i = this.connections.length - 1; i >= 0; --i) {
- var cp = this.connections[i];
- cp.draw_x(c);
- }
- */
- };
-
- // result of rotating an alignment [rot*9 + align]
- var aOrient = [
- 0,
- 1,
- 2,
- 3,
- 4,
- 5,
- 6,
- 7,
- 8, // NORTH (identity)
- 2,
- 5,
- 8,
- 1,
- 4,
- 7,
- 0,
- 3,
- 6, // EAST (rot270)
- 8,
- 7,
- 6,
- 5,
- 4,
- 3,
- 2,
- 1,
- 0, // SOUTH (rot180)
- 6,
- 3,
- 0,
- 7,
- 4,
- 1,
- 8,
- 5,
- 3, // WEST (rot90)
- 2,
- 1,
- 0,
- 5,
- 4,
- 3,
- 8,
- 7,
- 6, // RNORTH (negy)
- 8,
- 5,
- 2,
- 7,
- 4,
- 1,
- 6,
- 3,
- 0, // REAST (int-neg)
- 6,
- 7,
- 8,
- 3,
- 4,
- 5,
- 0,
- 1,
- 2, // RSOUTH (negx)
- 0,
- 3,
- 6,
- 1,
- 4,
- 7,
- 2,
- 5,
- 8, // RWEST (int-pos)
- ];
-
- var textAlign = ["left", "center", "right", "left", "center", "right", "left", "center", "right"];
-
- var textBaseline = ["top", "top", "top", "middle", "middle", "middle", "bottom", "bottom", "bottom"];
-
- Component.prototype.draw_text = function (c, text, x, y, alignment, size, fill) {
- var a = aOrient[this.rotation * 9 + alignment];
- c.textAlign = textAlign[a];
- c.textBaseline = textBaseline[a];
- if (fill == undefined) c.fillStyle = this.selected ? selected_style : normal_style;
- else c.fillStyle = fill;
- this.sch.draw_text(c, text, this.transform_x(x, y) + this.x, this.transform_y(x, y) + this.y, size);
- };
-
- Component.prototype.set_select = function (which) {
- if (which != this.selected) {
- this.selected = which;
- // create an undoable edit record here
- }
- };
-
- Component.prototype.select = function (x, y, shiftKey) {
- this.was_previously_selected = this.selected;
- if (this.near(x, y)) {
- this.set_select(shiftKey ? !this.selected : true);
- return true;
- } else return false;
- };
-
- Component.prototype.select_rect = function (s) {
- this.was_previously_selected = this.selected;
- if (intersect(this.bbox, s)) this.set_select(true);
- };
-
- // if connection point of component c bisects the
- // wire represented by this compononent, return that
- // connection point. Otherwise return null.
- Component.prototype.bisect = function (c) {
- return null;
- };
-
- // does mouse click fall on this component?
- Component.prototype.near = function (x, y) {
- return inside(this.bbox, x, y);
- };
-
- Component.prototype.edit_properties = function (x, y) {
- if (this.near(x, y)) {
- // make an
widget for each property
- var fields = [];
- for (var i in this.properties)
- // underscore at beginning of property name => system property
- if (i.charAt(0) != "_") fields[i] = build_input("text", 10, this.properties[i]);
-
- var content = build_table(fields);
- content.fields = fields;
- content.component = this;
-
- this.sch.dialog("Edit Properties", content, function (content) {
- for (var i in content.fields) content.component.properties[i] = content.fields[i].value;
- content.component.sch.redraw_background();
- });
- return true;
- } else return false;
- };
-
- Component.prototype.clear_labels = function () {
- for (var i = this.connections.length - 1; i >= 0; --i) {
- this.connections[i].clear_label();
- }
- };
-
- // default action: don't propagate label
- Component.prototype.propagate_label = function (label) {};
-
- // give components a chance to generate default labels for their connection(s)
- // default action: do nothing
- Component.prototype.add_default_labels = function () {};
-
- // component should generate labels for all unlabeled connections
- Component.prototype.label_connections = function () {
- for (var i = this.connections.length - 1; i >= 0; --i) {
- var cp = this.connections[i];
- if (!cp.label) cp.propagate_label(this.sch.get_next_label());
- }
- };
-
- // default behavior: no probe info
- Component.prototype.probe_info = function () {
- return undefined;
- };
-
- // default behavior: nothing to display for DC analysis
- Component.prototype.display_current = function (c, vmap) {};
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Connection point
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- var connection_point_radius = 2;
-
- function ConnectionPoint(parent, x, y) {
- this.parent = parent;
- this.offset_x = x;
- this.offset_y = y;
- this.location = "";
- this.update_location();
- this.label = undefined;
- }
-
- ConnectionPoint.prototype.toString = function () {
- return edx.StringUtils.interpolate("
", {
- offset_x: this.offset_x,
- offset_y: this.offset_y,
- parent: edx.HtmlUtils.ensureHTML(this.parent.toString()),
- });
- };
-
- ConnectionPoint.prototype.json = function () {
- return this.label;
- };
-
- ConnectionPoint.prototype.clear_label = function () {
- this.label = undefined;
- };
-
- ConnectionPoint.prototype.propagate_label = function (label) {
- // should we check if existing label is the same? it should be...
-
- if (this.label === undefined) {
- // label this connection point
- this.label = label;
-
- // propagate label to coincident connection points
- this.parent.sch.propagate_label(label, this.location);
-
- // possibly label other cp's for this device?
- this.parent.propagate_label(label);
- } else if (this.label != "0" && label != "0" && this.label != label)
- alert("Node has two conflicting labels: " + this.label + ", " + label);
- };
-
- ConnectionPoint.prototype.update_location = function () {
- // update location string which we use as a key to find coincident connection points
- var old_location = this.location;
- var parent = this.parent;
- var nx = parent.transform_x(this.offset_x, this.offset_y) + parent.x;
- var ny = parent.transform_y(this.offset_x, this.offset_y) + parent.y;
- this.x = nx;
- this.y = ny;
- this.location = nx + "," + ny;
-
- // add ourselves to the connection list for the new location
- if (parent.sch) parent.sch.update_connection_point(this, old_location);
- };
-
- ConnectionPoint.prototype.coincident = function (x, y) {
- return this.x == x && this.y == y;
- };
-
- ConnectionPoint.prototype.draw = function (c, n) {
- if (n != 2) this.parent.draw_circle(c, this.offset_x, this.offset_y, connection_point_radius, n > 2);
- };
-
- ConnectionPoint.prototype.draw_x = function (c) {
- this.parent.draw_line(c, this.offset_x - 2, this.offset_y - 2, this.offset_x + 2, this.offset_y + 2, grid_style);
- this.parent.draw_line(c, this.offset_x + 2, this.offset_y - 2, this.offset_x - 2, this.offset_y + 2, grid_style);
- };
-
- ConnectionPoint.prototype.display_voltage = function (c, vmap) {
- var v = vmap[this.label];
- if (v != undefined) {
- var label = v.toFixed(2) + "V";
-
- // first draw some solid blocks in the background
- c.globalAlpha = 0.85;
- this.parent.draw_text(c, "\u2588\u2588\u2588", this.offset_x, this.offset_y, 4, annotation_size, element_style);
- c.globalAlpha = 1.0;
-
- // display the node voltage at this connection point
- this.parent.draw_text(c, label, this.offset_x, this.offset_y, 4, annotation_size, annotation_style);
-
- // only display each node voltage once
- delete vmap[this.label];
- }
- };
-
- // see if three connection points are collinear
- function collinear(p1, p2, p3) {
- // from http://mathworld.wolfram.com/Collinear.html
- var area = p1.x * (p2.y - p3.y) + p2.x * (p3.y - p1.y) + p3.x * (p1.y - p2.y);
- return area == 0;
- }
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Wire
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- var near_distance = 2; // how close to wire counts as "near by"
-
- function Wire(x1, y1, x2, y2) {
- // arbitrarily call x1,y1 the origin
- Component.call(this, "w", x1, y1, 0);
- this.dx = x2 - x1;
- this.dy = y2 - y1;
- this.add_connection(0, 0);
- this.add_connection(this.dx, this.dy);
-
- // compute bounding box (expanded slightly)
- var r = [0, 0, this.dx, this.dy];
- canonicalize(r);
- r[0] -= near_distance;
- r[1] -= near_distance;
- r[2] += near_distance;
- r[3] += near_distance;
- this.bounding_box = r;
- this.update_coords(); // update bbox
-
- // used in selection calculations
- this.len = Math.sqrt(this.dx * this.dx + this.dy * this.dy);
- }
- Wire.prototype = new Component();
- Wire.prototype.constructor = Wire;
-
- Wire.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- x: this.x,
- y: this.y,
- x_plus_dx: this.x + this.dx,
- y_plus_dy: this.y + this.dy,
- });
- };
-
- // return connection point at other end of wire from specified cp
- Wire.prototype.other_end = function (cp) {
- if (cp == this.connections[0]) return this.connections[1];
- else if (cp == this.connections[1]) return this.connections[0];
- else return undefined;
- };
-
- Wire.prototype.json = function (index) {
- var json = ["w", [this.x, this.y, this.x + this.dx, this.y + this.dy]];
- return json;
- };
-
- Wire.prototype.draw = function (c) {
- this.draw_line(c, 0, 0, this.dx, this.dy);
- };
-
- Wire.prototype.clone = function (x, y) {
- return new Wire(x, y, x + this.dx, y + this.dy);
- };
-
- Wire.prototype.near = function (x, y) {
- // crude check: (x,y) within expanded bounding box of wire
- if (inside(this.bbox, x, y)) {
- // compute distance between x,y and nearst point on line
- // http://www.allegro.cc/forums/thread/589720
- var D = Math.abs((x - this.x) * this.dy - (y - this.y) * this.dx) / this.len;
- if (D <= near_distance) return true;
- }
- return false;
- };
-
- // selection rectangle selects wire only if it includes
- // one of the end points
- Wire.prototype.select_rect = function (s) {
- this.was_previously_selected = this.selected;
- if (inside(s, this.x, this.y) || inside(s, this.x + this.dx, this.y + this.dy)) this.set_select(true);
- };
-
- // if connection point cp bisects the
- // wire represented by this compononent, return true
- Wire.prototype.bisect_cp = function (cp) {
- var x = cp.x;
- var y = cp.y;
-
- // crude check: (x,y) within expanded bounding box of wire
- if (inside(this.bbox, x, y)) {
- // compute distance between x,y and nearst point on line
- // http://www.allegro.cc/forums/thread/589720
- var D = Math.abs((x - this.x) * this.dy - (y - this.y) * this.dx) / this.len;
- // final check: ensure point isn't an end point of the wire
- if (D < 1 && !this.connections[0].coincident(x, y) && !this.connections[1].coincident(x, y)) return true;
- }
- return false;
- };
-
- // if some connection point of component c bisects the
- // wire represented by this compononent, return that
- // connection point. Otherwise return null.
- Wire.prototype.bisect = function (c) {
- if (c == undefined) return;
- for (var i = c.connections.length - 1; i >= 0; --i) {
- var cp = c.connections[i];
- if (this.bisect_cp(cp)) return cp;
- }
- return null;
- };
-
- Wire.prototype.move_end = function () {
- // look for wires bisected by this wire
- this.sch.check_wires(this);
-
- // look for connection points that might bisect us
- this.sch.check_connection_points(this);
- };
-
- // wires "conduct" their label to the other end
- Wire.prototype.propagate_label = function (label) {
- // don't worry about relabeling a cp, it won't recurse!
- this.connections[0].propagate_label(label);
- this.connections[1].propagate_label(label);
- };
-
- // Wires have no properties to edit
- Wire.prototype.edit_properties = function (x, y) {
- return false;
- };
-
- // some actual component will start the labeling of electrical nodes,
- // so do nothing here
- Wire.prototype.label_connections = function () {};
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Ground
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Ground(x, y, rotation) {
- Component.call(this, "g", x, y, rotation);
- this.add_connection(0, 0);
- this.bounding_box = [-6, 0, 6, 8];
- this.update_coords();
- }
- Ground.prototype = new Component();
- Ground.prototype.constructor = Ground;
-
- Ground.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- x: this.x,
- y: this.y,
- });
- };
-
- Ground.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 8);
- this.draw_line(c, -6, 8, 6, 8);
- };
-
- Ground.prototype.clone = function (x, y) {
- return new Ground(x, y, this.rotation);
- };
-
- // Grounds no properties to edit
- Ground.prototype.edit_properties = function (x, y) {
- return false;
- };
-
- // give components a chance to generate a label for their connection(s)
- // default action: do nothing
- Ground.prototype.add_default_labels = function () {
- this.connections[0].propagate_label("0"); // canonical label for GND node
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Label
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Label(x, y, rotation, label) {
- Component.call(this, "L", x, y, rotation);
- this.properties["label"] = label ? label : "???";
- this.add_connection(0, 0);
- this.bounding_box = [-2, 0, 2, 8];
- this.update_coords();
- }
- Label.prototype = new Component();
- Label.prototype.constructor = Label;
-
- Label.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- x: this.x,
- y: this.y,
- });
- };
-
- Label.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 8);
- this.draw_text(c, this.properties["label"], 0, 9, 1, property_size);
- };
-
- Label.prototype.clone = function (x, y) {
- return new Label(x, y, this.rotation, this.properties["label"]);
- };
-
- // give components a chance to generate a label for their connection(s)
- // default action: do nothing
- Label.prototype.add_default_labels = function () {
- this.connections[0].propagate_label(this.properties["label"]);
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Voltage Probe
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- var probe_colors = ["red", "green", "blue", "cyan", "magenta", "yellow", "black", "x-axis"];
- var probe_colors_rgb = {
- red: "rgb(255,64,64)",
- green: "rgb(64,255,64)",
- blue: "rgb(64,64,255)",
- cyan: "rgb(64,255,255)",
- magenta: "rgb(255,64,255)",
- yellow: "rgb(255,255,64)",
- black: "rgb(0,0,0)",
- "x-axis": undefined,
- };
-
- function Probe(x, y, rotation, color, offset) {
- Component.call(this, "s", x, y, rotation);
- this.add_connection(0, 0);
- this.properties["color"] = color ? color : "cyan";
- this.properties["offset"] = offset == undefined || offset == "" ? "0" : offset;
- this.bounding_box = [0, 0, 27, -21];
- this.update_coords();
- }
- Probe.prototype = new Component();
- Probe.prototype.constructor = Probe;
-
- Probe.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- x: this.x,
- y: this.y,
- });
- };
-
- Probe.prototype.draw = function (c) {
- // draw outline
- this.draw_line(c, 0, 0, 4, -4);
- this.draw_line(c, 2, -6, 6, -2);
- this.draw_line(c, 2, -6, 17, -21);
- this.draw_line(c, 6, -2, 21, -17);
- this.draw_line(c, 17, -21, 21, -17);
- this.draw_arc(c, 19, -11, 8, (3 * Math.PI) / 2, 0);
-
- // fill body with plot color
- var color = probe_colors_rgb[this.properties["color"]];
- if (color != undefined) {
- c.fillStyle = color;
- c.beginPath();
- this.moveTo(c, 2, -6);
- this.lineTo(c, 6, -2);
- this.lineTo(c, 21, -17);
- this.lineTo(c, 17, -21);
- this.lineTo(c, 2, -6);
- c.fill();
- } else {
- this.draw_text(c, this.properties["color"], 27, -11, 1, property_size);
- }
- };
-
- Probe.prototype.clone = function (x, y) {
- return new Probe(x, y, this.rotation, this.properties["color"], this.properties["offset"]);
- };
-
- Probe.prototype.edit_properties = function (x, y) {
- if (inside(this.bbox, x, y)) {
- var fields = [];
- fields["Plot color"] = build_select(probe_colors, this.properties["color"]);
- fields["Plot offset"] = build_input("text", 10, this.properties["offset"]);
-
- var content = build_table(fields);
- content.fields = fields;
- content.component = this;
-
- this.sch.dialog("Edit Properties", content, function (content) {
- var color_choice = content.fields["Plot color"];
- content.component.properties["color"] = probe_colors[color_choice.selectedIndex];
- content.component.properties["offset"] = content.fields["Plot offset"].value;
- content.component.sch.redraw_background();
- });
- return true;
- } else return false;
- };
-
- // return [color, node_label, offset, type] for this probe
- Probe.prototype.probe_info = function () {
- var color = this.properties["color"];
- var offset = this.properties["offset"];
- if (offset == undefined || offset == "") offset = "0";
- return [color, this.connections[0].label, offset, "voltage"];
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Ammeter Probe
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Ammeter(x, y, rotation, color, offset) {
- Component.call(this, "a", x, y, rotation);
- this.add_connection(0, 0); // pos
- this.add_connection(16, 0); // neg
- this.properties["color"] = color ? color : "magenta";
- this.properties["offset"] = offset == undefined || offset == "" ? "0" : offset;
- this.bounding_box = [-3, 0, 16, 3];
- this.update_coords();
- }
- Ammeter.prototype = new Component();
- Ammeter.prototype.constructor = Ammeter;
-
- Ammeter.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- x: this.x,
- y: this.y,
- });
- };
-
- Ammeter.prototype.move_end = function () {
- Component.prototype.move_end.call(this); // do the normal processing
-
- // special for current probes: see if probe has been placed
- // in the middle of wire, creating three wire segments one
- // of which is shorting the two terminals of the probe. If
- // so, auto remove the shorting segment.
- var e1 = this.connections[0].location;
- var e2 = this.connections[1].location;
- var cplist = this.sch.find_connections(this.connections[0]);
- for (var i = cplist.length - 1; i >= 0; --i) {
- var c = cplist[i].parent; // a component connected to ammeter terminal
- // look for a wire whose end points match those of the ammeter
- if (c.type == "w") {
- var c_e1 = c.connections[0].location;
- var c_e2 = c.connections[1].location;
- if ((e1 == c_e1 && c2 == c_e2) || (e1 == c_e2 && e2 == c_e1)) {
- c.remove();
- break;
- }
- }
- }
- };
-
- Ammeter.prototype.draw = function (c) {
- this.draw_line(c, 0, 0, 16, 0);
-
- // draw chevron in probe color
- c.strokeStyle = probe_colors_rgb[this.properties["color"]];
- if (c.strokeStyle != undefined) {
- c.beginPath();
- this.moveTo(c, 6, -3);
- this.lineTo(c, 10, 0);
- this.lineTo(c, 6, 3);
- c.stroke();
- }
- };
-
- Ammeter.prototype.clone = function (x, y) {
- return new Ammeter(x, y, this.rotation, this.properties["color"], this.properties["offset"]);
- };
-
- // share code with voltage probe
- Ammeter.prototype.edit_properties = Probe.prototype.edit_properties;
-
- Ammeter.prototype.label = function () {
- var name = this.properties["name"];
- var label = "I(" + (name ? name : "_" + this.properties["_json_"]) + ")";
- return label;
- };
-
- // display current for DC analysis
- Ammeter.prototype.display_current = function (c, vmap) {
- var label = this.label();
- var v = vmap[label];
- if (v != undefined) {
- var i = engineering_notation(v, 2) + "A";
- this.draw_text(c, i, 8, -5, 7, annotation_size, annotation_style);
-
- // only display each current once
- delete vmap[label];
- }
- };
-
- // return [color, current_label, offset, type] for this probe
- Ammeter.prototype.probe_info = function () {
- var color = this.properties["color"];
- var offset = this.properties["offset"];
- if (offset == undefined || offset == "") offset = "0";
- return [color, this.label(), offset, "current"];
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Resistor
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Resistor(x, y, rotation, name, r) {
- Component.call(this, "r", x, y, rotation);
- this.properties["name"] = name;
- this.properties["r"] = r ? r : "1";
- this.add_connection(0, 0);
- this.add_connection(0, 48);
- this.bounding_box = [-5, 0, 5, 48];
- this.update_coords();
- }
- Resistor.prototype = new Component();
- Resistor.prototype.constructor = Resistor;
-
- Resistor.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- r: this.properties["r"],
- x: this.x,
- y: this.y,
- });
- };
-
- Resistor.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 12);
- this.draw_line(c, 0, 12, 4, 14);
- this.draw_line(c, 4, 14, -4, 18);
- this.draw_line(c, -4, 18, 4, 22);
- this.draw_line(c, 4, 22, -4, 26);
- this.draw_line(c, -4, 26, 4, 30);
- this.draw_line(c, 4, 30, -4, 34);
- this.draw_line(c, -4, 34, 0, 36);
- this.draw_line(c, 0, 36, 0, 48);
- if (this.properties["r"]) this.draw_text(c, this.properties["r"] + "\u03A9", 5, 24, 3, property_size);
- if (this.properties["name"]) this.draw_text(c, this.properties["name"], -5, 24, 5, property_size);
- };
-
- Resistor.prototype.clone = function (x, y) {
- return new Resistor(x, y, this.rotation, this.properties["name"], this.properties["r"]);
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Capacitor
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Capacitor(x, y, rotation, name, c) {
- Component.call(this, "c", x, y, rotation);
- this.properties["name"] = name;
- this.properties["c"] = c ? c : "1p";
- this.add_connection(0, 0);
- this.add_connection(0, 48);
- this.bounding_box = [-8, 0, 8, 48];
- this.update_coords();
- }
- Capacitor.prototype = new Component();
- Capacitor.prototype.constructor = Capacitor;
-
- Capacitor.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- r: this.properties["r"],
- x: this.x,
- y: this.y,
- });
- };
-
- Capacitor.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 22);
- this.draw_line(c, -8, 22, 8, 22);
- this.draw_line(c, -8, 26, 8, 26);
- this.draw_line(c, 0, 26, 0, 48);
- if (this.properties["c"]) this.draw_text(c, this.properties["c"] + "F", 9, 24, 3, property_size);
- if (this.properties["name"]) this.draw_text(c, this.properties["name"], -9, 24, 5, property_size);
- };
-
- Capacitor.prototype.clone = function (x, y) {
- return new Capacitor(x, y, this.rotation, this.properties["name"], this.properties["c"]);
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Inductor
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Inductor(x, y, rotation, name, l) {
- Component.call(this, "l", x, y, rotation);
- this.properties["name"] = name;
- this.properties["l"] = l ? l : "1n";
- this.add_connection(0, 0);
- this.add_connection(0, 48);
- this.bounding_box = [-4, 0, 5, 48];
- this.update_coords();
- }
- Inductor.prototype = new Component();
- Inductor.prototype.constructor = Inductor;
-
- Inductor.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- l: this.properties["l"],
- x: this.x,
- y: this.y,
- });
- };
-
- Inductor.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 14);
- this.draw_arc(c, 0, 18, 4, (6 * Math.PI) / 4, (3 * Math.PI) / 4);
- this.draw_arc(c, 0, 24, 4, (5 * Math.PI) / 4, (3 * Math.PI) / 4);
- this.draw_arc(c, 0, 30, 4, (5 * Math.PI) / 4, (2 * Math.PI) / 4);
- this.draw_line(c, 0, 34, 0, 48);
-
- if (this.properties["l"]) this.draw_text(c, this.properties["l"] + "H", 6, 24, 3, property_size);
- if (this.properties["name"]) this.draw_text(c, this.properties["name"], -3, 24, 5, property_size);
- };
-
- Inductor.prototype.clone = function (x, y) {
- return new Inductor(x, y, this.rotation, this.properties["name"], this.properties["l"]);
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Diode
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- var diode_types = ["normal", "ideal"];
-
- function Diode(x, y, rotation, name, area, type) {
- Component.call(this, "d", x, y, rotation);
- this.properties["name"] = name;
- this.properties["area"] = area ? area : "1";
- this.properties["type"] = type ? type : "normal";
- this.add_connection(0, 0); // anode
- this.add_connection(0, 48); // cathode
- this.bounding_box = type == "ideal" ? [-12, 0, 12, 48] : [-8, 0, 8, 48];
- this.update_coords();
- }
- Diode.prototype = new Component();
- Diode.prototype.constructor = Diode;
-
- Diode.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- area: this.properties["area"],
- x: this.x,
- y: this.y,
- });
- };
-
- Diode.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 16);
- this.draw_line(c, -8, 16, 8, 16);
- this.draw_line(c, -8, 16, 0, 32);
- this.draw_line(c, 8, 16, 0, 32);
- this.draw_line(c, -8, 32, 8, 32);
- this.draw_line(c, 0, 32, 0, 48);
-
- if (this.properties["type"] == "ideal") {
- // put a box around an ideal diode
- this.draw_line(c, -10, 12, 10, 12);
- this.draw_line(c, -10, 12, -10, 36);
- this.draw_line(c, 10, 12, 10, 36);
- this.draw_line(c, -10, 36, 10, 36);
- }
-
- if (this.properties["area"]) this.draw_text(c, this.properties["area"], 10, 24, 3, property_size);
- if (this.properties["name"]) this.draw_text(c, this.properties["name"], -10, 24, 5, property_size);
- };
-
- Diode.prototype.clone = function (x, y) {
- return new Diode(x, y, this.rotation, this.properties["name"], this.properties["area"], this.properties["type"]);
- };
-
- Diode.prototype.edit_properties = function (x, y) {
- if (inside(this.bbox, x, y)) {
- var fields = [];
- fields["name"] = build_input("text", 10, this.properties["name"]);
- fields["area"] = build_input("text", 10, this.properties["area"]);
- fields["type"] = build_select(diode_types, this.properties["type"]);
-
- var content = build_table(fields);
- content.fields = fields;
- content.component = this;
-
- this.sch.dialog("Edit Properties", content, function (content) {
- content.component.properties["name"] = content.fields["name"].value;
- content.component.properties["area"] = content.fields["area"].value;
- content.component.properties["type"] = diode_types[content.fields["type"].selectedIndex];
- content.component.sch.redraw_background();
- });
- return true;
- } else return false;
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // N-channel Mosfet
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function NFet(x, y, rotation, name, w_over_l) {
- Component.call(this, "n", x, y, rotation);
- this.properties["name"] = name;
- this.properties["W/L"] = w_over_l ? w_over_l : "2";
- this.add_connection(0, 0); // drain
- this.add_connection(-24, 24); // gate
- this.add_connection(0, 48); // source
- this.bounding_box = [-24, 0, 8, 48];
- this.update_coords();
- }
- NFet.prototype = new Component();
- NFet.prototype.constructor = NFet;
-
- NFet.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- W_L: this.properties["W/L"],
- x: this.x,
- y: this.y,
- });
- };
-
- NFet.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 16);
- this.draw_line(c, -8, 16, 0, 16);
- this.draw_line(c, -8, 16, -8, 32);
- this.draw_line(c, -8, 32, 0, 32);
- this.draw_line(c, 0, 32, 0, 48);
- this.draw_line(c, -24, 24, -12, 24);
- this.draw_line(c, -12, 16, -12, 32);
-
- var dim = this.properties["W/L"];
- if (this.properties["name"]) {
- this.draw_text(c, this.properties["name"], 2, 22, 6, property_size);
- this.draw_text(c, dim, 2, 26, 0, property_size);
- } else this.draw_text(c, dim, 2, 24, 3, property_size);
- };
-
- NFet.prototype.clone = function (x, y) {
- return new NFet(x, y, this.rotation, this.properties["name"], this.properties["W/L"]);
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // P-channel Mosfet
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function PFet(x, y, rotation, name, w_over_l) {
- Component.call(this, "p", x, y, rotation);
- this.properties["name"] = name;
- this.properties["W/L"] = w_over_l ? w_over_l : "2";
- this.add_connection(0, 0); // drain
- this.add_connection(-24, 24); // gate
- this.add_connection(0, 48); // source
- this.bounding_box = [-24, 0, 8, 48];
- this.update_coords();
- }
- PFet.prototype = new Component();
- PFet.prototype.constructor = PFet;
-
- PFet.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- W_L: this.properties["W/L"],
- x: this.x,
- y: this.y,
- });
- };
-
- PFet.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 16);
- this.draw_line(c, -8, 16, 0, 16);
- this.draw_line(c, -8, 16, -8, 32);
- this.draw_line(c, -8, 32, 0, 32);
- this.draw_line(c, 0, 32, 0, 48);
- this.draw_line(c, -24, 24, -16, 24);
- this.draw_circle(c, -14, 24, 2, false);
- this.draw_line(c, -12, 16, -12, 32);
-
- var dim = this.properties["W/L"];
- if (this.properties["name"]) {
- this.draw_text(c, this.properties["name"], 2, 22, 6, property_size);
- this.draw_text(c, dim, 2, 26, 0, property_size);
- } else this.draw_text(c, dim, 2, 24, 3, property_size);
- };
-
- PFet.prototype.clone = function (x, y) {
- return new PFet(x, y, this.rotation, this.properties["name"], this.properties["W/L"]);
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Op Amp
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function OpAmp(x, y, rotation, name, A) {
- Component.call(this, "o", x, y, rotation);
- this.properties["name"] = name;
- this.properties["A"] = A ? A : "30000";
- this.add_connection(0, 0); // +
- this.add_connection(0, 16); // -
- this.add_connection(48, 8); // output
- this.add_connection(24, 32); // ground
- this.bounding_box = [0, -8, 48, 32];
- this.update_coords();
- }
- OpAmp.prototype = new Component();
- OpAmp.prototype.constructor = OpAmp;
-
- OpAmp.prototype.toString = function () {
- return edx.StringUtils.interpolate("", {
- A: this.properties["A"],
- x: this.x,
- y: this.y,
- });
- };
-
- OpAmp.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- // triangle
- this.draw_line(c, 8, -8, 8, 24);
- this.draw_line(c, 8, -8, 40, 8);
- this.draw_line(c, 8, 24, 40, 8);
- // inputs and output
- this.draw_line(c, 0, 0, 8, 0);
- this.draw_line(c, 0, 16, 8, 16);
- this.draw_text(c, "gnd", 37, 18, property_size);
- this.draw_line(c, 40, 8, 48, 8);
- this.draw_line(c, 24, 16, 24, 32);
- // + and -
- this.draw_line(c, 10, 0, 16, 0);
- this.draw_line(c, 13, -3, 13, 3);
- this.draw_line(c, 10, 16, 16, 16);
-
- if (this.properties["name"]) this.draw_text(c, this.properties["name"], 32, 16, 0, property_size);
- };
-
- OpAmp.prototype.clone = function (x, y) {
- return new OpAmp(x, y, this.rotation, this.properties["name"], this.properties["A"]);
- };
-
- ////////////////////////////////////////////////////////////////////////////////
- //
- // Source
- //
- ////////////////////////////////////////////////////////////////////////////////
-
- function Source(x, y, rotation, name, type, value) {
- Component.call(this, type, x, y, rotation);
- this.properties["name"] = name;
- if (value == undefined) value = "dc(1)";
- this.properties["value"] = value;
- this.add_connection(0, 0);
- this.add_connection(0, 48);
- this.bounding_box = [-12, 0, 12, 48];
- this.update_coords();
- this.content = document.createElement("div"); // used by edit_properties
- }
- Source.prototype = new Component();
- Source.prototype.constructor = Source;
-
- Source.prototype.toString = function () {
- return edx.StringUtils.interpolate("<{type}source {params} ({x},{y})>", {
- type: this.type,
- params: this.properties["params"],
- x: this.x,
- y: this.y,
- });
- };
-
- Source.prototype.draw = function (c) {
- Component.prototype.draw.call(this, c); // give superclass a shot
- this.draw_line(c, 0, 0, 0, 12);
- this.draw_circle(c, 0, 24, 12, false);
- this.draw_line(c, 0, 36, 0, 48);
-
- if (this.type == "v") {
- // voltage source
- // draw + and -
- this.draw_line(c, 0, 15, 0, 21);
- this.draw_line(c, -3, 18, 3, 18);
- this.draw_line(c, -3, 30, 3, 30);
- } else if (this.type == "i") {
- // current source
- // draw arrow: pos to neg
- this.draw_line(c, 0, 15, 0, 32);
- this.draw_line(c, -3, 26, 0, 32);
- this.draw_line(c, 3, 26, 0, 32);
- }
-
- if (this.properties["name"]) this.draw_text(c, this.properties["name"], -13, 24, 5, property_size);
- if (this.properties["value"]) this.draw_text(c, this.properties["value"], 13, 24, 3, property_size);
- };
-
- // map source function name to labels for each source parameter
- var source_functions = {
- dc: ["DC value"],
-
- impulse: ["Height", "Width (secs)"],
-
- step: ["Initial value", "Plateau value", "Delay until step (secs)", "Rise time (secs)"],
-
- square: ["Initial value", "Plateau value", "Frequency (Hz)", "Duty cycle (%)"],
-
- triangle: ["Initial value", "Plateau value", "Frequency (Hz)"],
-
- pwl: ["Comma-separated list of alternating times and values"],
-
- pwl_repeating: ["Comma-separated list of alternating times and values"],
-
- pulse: [
- "Initial value",
- "Plateau value",
- "Delay until pulse (secs)",
- "Time for first transition (secs)",
- "Time for second transition (secs)",
- "Pulse width (secs)",
- "Period (secs)",
- ],
-
- sin: ["Offset value", "Amplitude", "Frequency (Hz)", "Delay until sin starts (secs)", "Phase offset (degrees)"],
- };
-
- // build property editor div
- Source.prototype.build_content = function (src) {
- // make an widget for each property
- var fields = [];
- fields["name"] = build_input("text", 10, this.properties["name"]);
-
- if (src == undefined) {
- fields["value"] = this.properties["value"];
- } else {
- // fancy version: add select tag for source type
- var src_types = [];
- for (var t in source_functions) src_types.push(t);
- var type_select = build_select(src_types, src.fun);
- type_select.component = this;
- type_select.addEventListener("change", source_type_changed, false);
- fields["type"] = type_select;
-
- if (src.fun == "pwl" || src.run == "pwl_repeating") {
- var v = "";
- var first = true;
- for (var i = 0; i < src.args.length; i++) {
- if (first) first = false;
- else v += ",";
- v += engineering_notation(src.args[i], 3);
- if (i % 2 == 0) v += "s";
- }
- fields[source_functions[src.fun][0]] = build_input("text", 30, v);
- } else {
- // followed separate input tag for each parameter
- var labels = source_functions[src.fun];
- for (var i = 0; i < labels.length; i++) {
- var v = engineering_notation(src.args[i], 3);
- fields[labels[i]] = build_input("text", 10, v);
- }
- }
- }
-
- var div = this.content;
- if (div.hasChildNodes()) div.removeChild(div.firstChild); // remove table of input fields
- div.appendChild(build_table(fields));
- div.fields = fields;
- div.component = this;
- return div;
- };
-
- function source_type_changed(event) {
- if (!event) event = window.event;
- var select = window.event ? event.srcElement : event.target;
-
- // see where to get source parameters from
- var type = select.options[select.selectedIndex].value;
- var src = undefined;
- if (this.src != undefined && type == this.src.fun) src = this.src;
- else if (typeof cktsim != "undefined") src = cktsim.parse_source(type + "()");
-
- select.component.build_content(src);
- }
-
- Source.prototype.edit_properties = function (x, y) {
- if (this.near(x, y)) {
- this.src = undefined;
- if (typeof cktsim != "undefined") this.src = cktsim.parse_source(this.properties["value"]);
- var content = this.build_content(this.src);
-
- this.sch.dialog("Edit Properties", content, function (content) {
- var c = content.component;
- var fields = content.fields;
-
- var first = true;
- var value = "";
- for (var label in fields) {
- if (label == "name") c.properties["name"] = fields["name"].value;
- else if (label == "value") {
- // if unknown source type
- value = fields["value"].value;
- c.sch.redraw_background();
- return;
- } else if (label == "type") {
- var select = fields["type"];
- value = select.options[select.selectedIndex].value + "(";
- } else {
- if (first) first = false;
- else value += ",";
- value += fields[label].value;
- }
- }
- c.properties["value"] = value + ")";
- c.sch.redraw_background();
- });
- return true;
- } else return false;
- };
-
- function VSource(x, y, rotation, name, value) {
- Source.call(this, x, y, rotation, name, "v", value);
- this.type = "v";
- }
- VSource.prototype = new Component();
- VSource.prototype.constructor = VSource;
- VSource.prototype.toString = Source.prototype.toString;
- VSource.prototype.draw = Source.prototype.draw;
- VSource.prototype.clone = Source.prototype.clone;
- VSource.prototype.build_content = Source.prototype.build_content;
- VSource.prototype.edit_properties = Source.prototype.edit_properties;
-
- // display current for DC analysis
- VSource.prototype.display_current = function (c, vmap) {
- var name = this.properties["name"];
- var label = "I(" + (name ? name : "_" + this.properties["_json_"]) + ")";
- var v = vmap[label];
- if (v != undefined) {
- // first draw some solid blocks in the background
- c.globalAlpha = 0.5;
- this.draw_text(c, "\u2588\u2588\u2588", -8, 8, 4, annotation_size, element_style);
- c.globalAlpha = 1.0;
-
- // display the element current
- var i = engineering_notation(v, 2) + "A";
- this.draw_text(c, i, -3, 5, 5, annotation_size, annotation_style);
- // draw arrow for current
- this.draw_line(c, -3, 4, 0, 8);
- this.draw_line(c, 3, 4, 0, 8);
- // only display each current once
- delete vmap[label];
- }
- };
-
- VSource.prototype.clone = function (x, y) {
- return new VSource(x, y, this.rotation, this.properties["name"], this.properties["value"]);
- };
-
- function ISource(x, y, rotation, name, value) {
- Source.call(this, x, y, rotation, name, "i", value);
- this.type = "i";
- }
- ISource.prototype = new Component();
- ISource.prototype.constructor = ISource;
- ISource.prototype.toString = Source.prototype.toString;
- ISource.prototype.draw = Source.prototype.draw;
- ISource.prototype.clone = Source.prototype.clone;
- ISource.prototype.build_content = Source.prototype.build_content;
- ISource.prototype.edit_properties = Source.prototype.edit_properties;
-
- ISource.prototype.clone = function (x, y) {
- return new ISource(x, y, this.rotation, this.properties["name"], this.properties["value"]);
- };
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // JQuery slider support for setting a component value
- //
- ///////////////////////////////////////////////////////////////////////////////
-
- function component_slider(event, ui) {
- var sname = $(this).slider("option", "schematic");
-
- // set value of specified component
- var cname = $(this).slider("option", "component");
- var pname = $(this).slider("option", "property");
- var suffix = $(this).slider("option", "suffix");
- if (typeof suffix != "string") suffix = "";
-
- var v = ui.value;
- $(this).slider("value", v); // move slider's indicator
-
- var choices = $(this).slider("option", "choices");
- if (choices instanceof Array) v = choices[v];
-
- // selector may match several schematics
- $("." + sname).each(function (index, element) {
- element.schematic.set_property(cname, pname, v.toString() + suffix);
- });
-
- // perform requested analysis
- var analysis = $(this).slider("option", "analysis");
- if (analysis == "dc")
- $("." + sname).each(function (index, element) {
- element.schematic.dc_analysis();
- });
-
- return false;
- }
-
- ///////////////////////////////////////////////////////////////////////////////
- //
- // Module definition
- //
- ///////////////////////////////////////////////////////////////////////////////
-
- var module = {
- Schematic: Schematic,
- component_slider: component_slider,
- };
- return module;
-})();
diff --git a/xmodule/services.py b/xmodule/services.py
index 55e3b1a37a96..6c230cd3cb24 100644
--- a/xmodule/services.py
+++ b/xmodule/services.py
@@ -29,7 +29,7 @@
from xmodule.modulestore.django import modulestore
if TYPE_CHECKING:
- from xmodule.capa_block import ProblemBlock
+ from xblocks_contrib.problem import ProblemBlock
log = logging.getLogger(__name__)
diff --git a/xmodule/static/css-builtin-blocks/ProblemBlockDisplay.css b/xmodule/static/css-builtin-blocks/ProblemBlockDisplay.css
deleted file mode 100644
index aeb9348048ee..000000000000
--- a/xmodule/static/css-builtin-blocks/ProblemBlockDisplay.css
+++ /dev/null
@@ -1,2304 +0,0 @@
-@import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,600,700");
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_correct .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicegroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + section.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + section.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_incorrect .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicegroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + section.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.correct .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.partially-correct .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.incorrect .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .incorrect .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .incorrect .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .capa_inputtype.textline
- > .partially-correct
- .status
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .inputtype.formulaequationinput
- > .partially-correct
- .status
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .correct .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .correct .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .correct .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .incorrect .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .partially-correct .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .correct .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .incorrect .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .partially-correct .status-icon::after {
- font-family: FontAwesome;
- -webkit-font-smoothing: antialiased;
- display: inline-block;
- speak: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock h2 {
- margin-top: 0;
- margin-bottom: calc((var(--baseline, 20px) * 0.75));
-}
-
-.xmodule_display.xmodule_ProblemBlock h2.problem-header {
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock h2.problem-header section.staff {
- margin-top: calc((var(--baseline, 20px) * 1.5));
- font-size: 80%;
-}
-
-@media print {
- .xmodule_display.xmodule_ProblemBlock h2 {
- display: block;
- width: auto;
- border-right: 0;
- }
-}
-
-.xmodule_display.xmodule_ProblemBlock .explanation-title {
- font-weight: bold;
-}
-
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-incorrect,
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-partially-correct,
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-correct {
- margin-top: calc((var(--baseline, 20px) / 4));
-}
-
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-incorrect .icon,
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-partially-correct .icon,
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-correct .icon {
- margin-right: calc((var(--baseline, 20px) / 4));
-}
-
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-incorrect .icon {
- color: var(--incorrect, #b20610);
-}
-
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-partially-correct .icon,
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-correct .icon {
- color: var(--correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-text {
- color: #646464;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem-hint {
- margin-bottom: 20px;
- width: 100%;
-}
-
-.xmodule_display.xmodule_ProblemBlock .hint-label {
- display: inline-block;
- padding-right: 0.5em;
-}
-
-.xmodule_display.xmodule_ProblemBlock .hint-text {
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock .feedback-hint-multi .hint-text {
- display: block;
-}
-
-.xmodule_display.xmodule_ProblemBlock iframe[seamless] {
- overflow: hidden;
- padding: 0;
- border: 0 none transparent;
- background-color: transparent;
-}
-
-.xmodule_display.xmodule_ProblemBlock .inline-error {
- color: var(--error-color-dark, #95050d);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem-progress {
- display: inline-block;
- color: var(--gray-d1, #5e5e5e);
- font-size: 0.875em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem {
- padding-top: var(--baseline, 20px);
-}
-
-@media print {
- .xmodule_display.xmodule_ProblemBlock div.problem {
- display: block;
- padding: 0;
- width: auto;
- }
-
- .xmodule_display.xmodule_ProblemBlock div.problem canvas,
- .xmodule_display.xmodule_ProblemBlock div.problem img {
- page-break-inside: avoid;
- }
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem input.math {
- direction: ltr;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .inline {
- display: inline;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .inline + p {
- margin-top: var(--baseline, 20px);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .question-description {
- color: var(--gray-d1, #5e5e5e);
- font-size: var(--small-font-size, 80%);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem form > label,
-.xmodule_display.xmodule_ProblemBlock div.problem .problem-group-label {
- display: block;
- margin-bottom: var(--baseline, 20px);
- font: inherit;
- color: inherit;
- -webkit-font-smoothing: initial;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .problem-group-label + .question-description {
- margin-top: calc(-1 * var(--baseline, 20px));
-}
-
-.xmodule_display.xmodule_ProblemBlock .wrapper-problem-response + .wrapper-problem-response,
-.xmodule_display.xmodule_ProblemBlock .wrapper-problem-response + p {
- margin-top: calc((var(--baseline, 20px) * 1.5));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup {
- margin: var(--baseline, 20px) 0 0 0;
- min-width: 100px;
- width: auto !important;
- width: 100px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup:after,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup:after {
- content: "";
- display: table;
- clear: both;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup label,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label {
- box-sizing: border-box;
- display: inline-block;
- clear: both;
- margin-bottom: calc((var(--baseline, 20px) / 2));
- border: 2px solid var(--gray-l4, #e4e4e4);
- border-radius: 3px;
- padding: calc((var(--baseline, 20px) / 2));
- width: 100%;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup label::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label::after {
- margin-left: calc((var(--baseline, 20px) * 0.75));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup .indicator-container,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .indicator-container {
- min-height: 1px;
- width: 25px;
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup fieldset,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup fieldset {
- box-sizing: border-box;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="radio"],
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input[type="radio"],
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="checkbox"],
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input[type="checkbox"] {
- margin: calc((var(--baseline, 20px) / 4));
- margin-right: calc((var(--baseline, 20px) / 2));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label {
- border: 2px solid var(--blue, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + label.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + section.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + label.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + section.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + section.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + label.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + section.choicetextgroup_correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + section.choicetextgroup_correct {
- border: 2px solid var(--correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_correct .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicegroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + section.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input:focus
- + label.choicegroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + label.choicegroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + section.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + section.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input:hover
- + label.choicegroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + label.choicegroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + label.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + section.choicetextgroup_correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + section.choicetextgroup_correct
- .status-icon::after {
- color: var(--correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + label.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + label.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + label.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + section.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + section.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + label.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + label.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + section.choicetextgroup_partially-correct,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + section.choicetextgroup_partially-correct {
- border: 2px solid var(--partially-correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + section.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input:focus
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + section.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + section.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input:hover
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + label.choicegroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + label.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + section.choicetextgroup_partially-correct
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + section.choicetextgroup_partially-correct
- .status-icon::after {
- color: var(--partially-correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + label.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + label.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + section.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + section.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + label.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + section.choicetextgroup_incorrect,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + section.choicetextgroup_incorrect {
- border: 2px solid var(--incorrect, #b20610);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_incorrect .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicegroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input
- + section.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input:focus
- + label.choicegroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + label.choicegroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + section.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:focus
- + section.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicegroup
- input:hover
- + label.choicegroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + label.choicegroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + label.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + section.choicetextgroup_incorrect
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- input:hover
- + section.choicetextgroup_incorrect
- .status-icon::after {
- color: var(--incorrect, #b20610);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input + label.choicegroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicegroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup .choicegroup input + label.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + label.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input
- + section.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input + section.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:focus + label.choicegroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicegroup_submitted,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + label.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + label.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:focus
- + section.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:focus + section.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input:hover + label.choicegroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicegroup_submitted,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + label.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + label.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- .choicegroup
- input:hover
- + section.choicetextgroup_submitted,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input:hover + section.choicetextgroup_submitted {
- border: 2px solid var(--submitted, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup .field {
- position: relative;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup label {
- padding: calc((var(--baseline, 20px) / 2));
- padding-left: calc((var(--baseline, 20px) * 2.3));
- position: relative;
- font-size: var(--base-font-size, 18px);
- line-height: normal;
- cursor: pointer;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="radio"],
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup input[type="checkbox"] {
- left: 0.5625em;
- position: absolute;
- top: 0.43em;
- width: calc(var(--baseline, 20px) * 1.1);
- height: calc(var(--baseline, 20px) * 1.1);
- z-index: 1;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup legend {
- margin-bottom: var(--baseline, 20px);
- max-width: 100%;
- white-space: normal;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicegroup legend + .question-description {
- margin-top: calc(-1 * var(--baseline, 20px));
- max-width: 100%;
- white-space: normal;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container {
- margin-left: calc((var(--baseline, 20px) * 0.75));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status {
- width: var(--baseline, 20px);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.correct .status-icon::after {
- color: var(--correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.partially-correct .status-icon::after {
- color: var(--partially-correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.incorrect .status-icon::after {
- color: var(--incorrect, #b20610);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.submitted .status-icon,
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.unsubmitted .status-icon,
-.xmodule_display.xmodule_ProblemBlock div.problem .indicator-container .status.unanswered .status-icon {
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem ol.enumerate li::before {
- display: block;
- visibility: hidden;
- height: 0;
- content: " ";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .solution-span > span {
- margin: var(--baseline, 20px) 0;
- display: block;
- position: relative;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .solution-span > span:empty {
- display: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .targeted-feedback-span > span {
- display: block;
- position: relative;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .targeted-feedback-span > span:empty {
- display: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div p.answer {
- margin-top: -2px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div p span.clarification i {
- font-style: normal;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div p span.clarification i:hover {
- color: var(--blue, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.correct input,
-.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-check input {
- border-color: var(--correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.partially-correct input,
-.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-check input {
- border-color: var(--partially-correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.processing input {
- border-color: #aaa;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-close input {
- border-color: var(--incorrect, #b20610);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.incorrect input,
-.xmodule_display.xmodule_ProblemBlock div.problem div.incomplete input {
- border-color: var(--incorrect, #b20610);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.submitted input,
-.xmodule_display.xmodule_ProblemBlock div.problem div.ui-icon-check input {
- border-color: var(--submitted, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div p.answer {
- display: inline-block;
- margin-top: calc((var(--baseline, 20px) / 2));
- margin-bottom: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div p.answer::before {
- display: inline;
- content: "Answer: ";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div p.answer:empty::before {
- display: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div div.equation {
- clear: both;
- margin-top: 3px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div div.equation .MathJax_Display {
- width: auto;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div div.equation img.loading {
- padding-left: calc((var(--baseline, 20px) / 2));
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span {
- margin-bottom: 0;
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span.MathJax_CHTML,
-.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span.MathJax,
-.xmodule_display.xmodule_ProblemBlock div.problem div div.equation span.MathJax_SVG {
- padding: 6px;
- min-width: 30px;
- border: 1px solid #e3e3e3;
- border-radius: 4px;
- background: #f1f1f1;
-}
-
-@media print {
- .xmodule_display.xmodule_ProblemBlock div.problem div [id^="display"].equation {
- display: none;
- }
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-bullet {
- display: inline-block;
- position: relative;
- top: 4px;
- width: 14px;
- height: 14px;
- background: var(--icon-unanswered) center center no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div span.processing,
-.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-processing {
- display: inline-block;
- position: relative;
- top: 6px;
- width: 25px;
- height: 20px;
- background: var(--icon-spinner) center center no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-check {
- display: inline-block;
- position: relative;
- top: 3px;
- width: 25px;
- height: 20px;
- background: var(--icon-correct) center center no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div span.incomplete,
-.xmodule_display.xmodule_ProblemBlock div.problem div span.ui-icon-close {
- display: inline-block;
- position: relative;
- top: 3px;
- width: 20px;
- height: 20px;
- background: var(--icon-incorrect) center center no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .reload {
- float: right;
- margin: calc((var(--baseline, 20px) / 2));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status {
- margin: calc(var(--baseline, 20px) / 2) 0;
- padding: calc(var(--baseline, 20px) / 2);
- border-radius: 5px;
- background: var(--gray-l6, #f8f8f8);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status:after {
- content: "";
- display: table;
- clear: both;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status span {
- display: block;
- float: left;
- overflow: hidden;
- margin: -7px 7px 0 0;
- text-indent: -9999px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status .grading {
- margin: 0 7px 0 0;
- padding-left: 25px;
- background: var(--icon-info) left center no-repeat;
- text-indent: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status p {
- float: left;
- margin-bottom: 0;
- text-transform: capitalize;
- line-height: 20px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status.file {
- margin-top: var(--baseline, 20px);
- padding: var(--baseline, 20px) 0 0 0;
- border: 0;
- border-top: 1px solid #eee;
- background: var(--white, #fff);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status.file p.debug {
- display: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .grader-status.file input {
- float: left;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation p {
- margin-bottom: calc((var(--baseline, 20px) / 5));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .feedback-on-feedback {
- margin-right: var(--baseline, 20px);
- height: 100px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-response header {
- text-align: right;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-response header a {
- font-size: 0.85em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list {
- margin-left: 3px;
- list-style-type: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list li {
- display: inline;
- margin-left: 50px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list li:first-child {
- margin-left: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .evaluation-scoring .scoring-list li label {
- font-size: 0.9em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div .submit-message-container {
- margin: var(--baseline, 20px) 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.inline > span {
- display: inline;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem ul {
- padding-left: 1em;
- margin-bottom: lh();
- margin-left: 0.75em;
- margin-left: 0.75rem;
- list-style: disc outside none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem ol {
- padding-left: 1em;
- margin-bottom: lh();
- margin-left: 0.75em;
- margin-left: 0.75rem;
- list-style: decimal outside none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem dl {
- line-height: 1.4em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem dl dd {
- margin-bottom: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem dd {
- margin-left: 0.5em;
- margin-left: 0.5rem;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem li {
- margin-bottom: lh(0.5);
- line-height: 1.4em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem li:last-child {
- margin-bottom: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem p {
- margin-bottom: lh();
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table {
- margin: lh() 0;
- border-collapse: collapse;
- table-layout: auto;
- max-width: 100%;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table td.cont-justified-left,
-.xmodule_display.xmodule_ProblemBlock div.problem table th.cont-justified-left {
- text-align: left !important;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table td.cont-justified-right,
-.xmodule_display.xmodule_ProblemBlock div.problem table th.cont-justified-right {
- text-align: right !important;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table td.cont-justified-center,
-.xmodule_display.xmodule_ProblemBlock div.problem table th.cont-justified-center {
- text-align: center !important;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table th {
- text-align: left;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table td {
- text-align: left;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table caption,
-.xmodule_display.xmodule_ProblemBlock div.problem table th,
-.xmodule_display.xmodule_ProblemBlock div.problem table td {
- padding: 0.25em 0.75em 0.25em 0;
- padding: 0.25rem 0.75rem 0.25rem 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table caption {
- margin-bottom: 0.75em;
- margin-bottom: 0.75rem;
- padding: 0.75em 0;
- padding: 0.75rem 0;
- background: #f1f1f1;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem table tr,
-.xmodule_display.xmodule_ProblemBlock div.problem table td,
-.xmodule_display.xmodule_ProblemBlock div.problem table th {
- vertical-align: middle;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem code {
- margin: 0 2px;
- padding: 0 5px;
- border: 1px solid #eaeaea;
- border-radius: 3px;
- background-color: var(--gray-l6, #f8f8f8);
- white-space: nowrap;
- font-size: 0.9em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem pre {
- overflow: auto;
- padding: 6px calc(var(--baseline, 20px) / 2);
- border: 1px solid var(--gray-l3, #c8c8c8);
- border-radius: 3px;
- background-color: var(--gray-l6, #f8f8f8);
- font-size: 0.9em;
- line-height: 1.4;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem pre > code {
- margin: 0;
- padding: 0;
- border: none;
- background: transparent;
- white-space: pre;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline input,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput input {
- box-sizing: border-box;
- border: 2px solid var(--gray-l4, #e4e4e4);
- border-radius: 3px;
- min-width: 160px;
- height: 46px;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline .status,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput .status {
- display: inline-block;
- margin-top: calc((var(--baseline, 20px) / 2));
- background: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .incorrect input,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .incorrect input {
- border: 2px solid var(--incorrect, #b20610);
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .incorrect .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .inputtype.formulaequationinput
- > .incorrect
- .status
- .status-icon::after {
- color: var(--incorrect, #b20610);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .partially-correct input,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .partially-correct input {
- border: 2px solid var(--partially-correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .capa_inputtype.textline
- > .partially-correct
- .status
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .inputtype.formulaequationinput
- > .partially-correct
- .status
- .status-icon::after {
- color: var(--partially-correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .correct input,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .correct input {
- border: 2px solid var(--correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .correct .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .correct .status .status-icon::after {
- color: var(--correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .submitted,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .submitted {
- margin: var(--baseline, 20px) 0 0 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .submitted input,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .submitted input {
- border: 2px solid var(--submitted, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .submitted .status,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .submitted .status {
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unanswered input,
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unsubmitted input,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .unanswered input,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > .unsubmitted input {
- border: 2px solid var(--gray-l4, #e4e4e4);
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unanswered .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock .problem .capa_inputtype.textline > .unsubmitted .status .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .inputtype.formulaequationinput
- > .unanswered
- .status
- .status-icon::after,
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .inputtype.formulaequationinput
- > .unsubmitted
- .status
- .status-icon::after {
- content: "";
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.formulaequationinput > div input {
- direction: ltr;
- text-align: left;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .trailing_text {
- margin-right: calc((var(--baseline, 20px) / 2));
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input {
- margin: var(--baseline, 20px) 0 0 0 !important;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container {
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.correct::after,
-.xmodule_display.xmodule_ProblemBlock
- .problem
- .inputtype.option-input
- .indicator-container
- .status.partially-correct::after,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.incorrect::after,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.submitted::after,
-.xmodule_display.xmodule_ProblemBlock .problem .inputtype.option-input .indicator-container .status.unanswered::after {
- margin-left: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror {
- border: 1px solid black;
- font-size: 14px;
- line-height: 18px;
- resize: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror .cm-tab {
- background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAYAAAAkuj5RAAAAAXNSR0IArs4c6QAAAGFJREFUSMft1LsRQFAQheHPowAKoACx3IgEKtaEHujDjORSgWTH/ZOdnZOcM/sgk/kFFWY0qV8foQwS4MKBCS3qR6ixBJvElOobYAtivseIE120FaowJPN75GMu8j/LfMwNjh4HUpwg4LUAAAAASUVORK5CYII=);
- background-position: right;
- background-repeat: no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror pre {
- overflow: hidden;
- margin: 0;
- padding: 0;
- border-width: 0;
- border-radius: 0;
- background: transparent;
- white-space: pre;
- word-wrap: normal;
- font-size: inherit;
- font-family: inherit;
- resize: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror pre.CodeMirror-cursor {
- position: absolute;
- visibility: hidden;
- width: 0;
- border-right: none;
- border-left: 1px solid var(--black, #000);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror-focused pre.CodeMirror-cursor {
- visibility: visible;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror-code pre {
- width: -webkit-fit-content;
- width: -moz-fit-content;
- width: fit-content;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .CodeMirror-scroll {
- margin-right: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock .capa-message {
- display: inline-block;
- color: var(--gray-d1, #5e5e5e);
- -webkit-font-smoothing: antialiased;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action {
- min-height: var(--baseline, 20px);
- width: 100%;
- display: flex;
- display: -ms-flexbox;
- -ms-flex-align: start;
- flex-direction: row;
- align-items: center;
- flex-wrap: wrap;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-buttons-wrapper {
- display: inline-flex;
- justify-content: flex-end;
- width: 100%;
- padding-bottom: var(--baseline, 20px);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-button-wrapper {
- border-right: 1px solid var(--gray-300, #d9d9d9);
- padding: 0 13px;
- display: inline-block;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-button-wrapper:last-child {
- border: none;
- padding-right: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn {
- border: none;
- max-width: 110px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn:hover,
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn:focus,
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn:active {
- color: var(--primary, #0075b4) !important;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn .icon {
- margin-bottom: calc(var(--baseline, 20px) / 10);
- display: block;
-}
-
-@media print {
- .xmodule_display.xmodule_ProblemBlock div.problem .action .problem-action-btn {
- display: none;
- }
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container {
- padding-bottom: var(--baseline, 20px);
- flex-grow: 1;
- display: flex;
- align-items: center;
-}
-
-@media (max-width: var(--bp-screen-lg, 1024px)) {
- .xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container {
- max-width: 100%;
- padding-bottom: var(--baseline, 20px);
- }
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container .submit {
- margin-right: calc((var(--baseline, 20px) / 2));
- float: left;
- white-space: nowrap;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container .submit-cta-description {
- color: var(--primary, #0075b4);
- font-size: small;
- padding-right: calc(var(--baseline, 20px) / 2);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .submit-attempt-container .submit-cta-link-button {
- color: var(--primary, #0075b4);
- padding-right: calc(var(--baseline, 20px) / 4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .submission-feedback {
- margin-right: calc((var(--baseline, 20px) / 2));
- margin-top: calc(var(--baseline, 20px) / 2);
- display: inline-block;
- color: var(--gray-d1, #5e5e5e);
- font-size: var(--medium-font-size, 0.9em);
- -webkit-font-smoothing: antialiased;
- vertical-align: middle;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .action .submission-feedback.cta-enabled {
- margin-top: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem hr {
- float: none;
- clear: both;
- margin: 0 0 0.75rem;
- width: 100%;
- height: 1px;
- border: none;
- background: #ddd;
- color: #ddd;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hidden {
- display: none;
- visibility: hidden;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem var (--all-text-inputs) {
- display: inline;
- width: auto;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem center {
- display: block;
- margin: lh() 0;
- padding: lh();
- border: 1px solid var(--gray-l3, #c8c8c8);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .message {
- font-size: inherit;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .detailed-solution > p {
- margin: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .detailed-solution > p:first-child {
- margin-bottom: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .detailed-targeted-feedback > p,
-.xmodule_display.xmodule_ProblemBlock div.problem .detailed-targeted-feedback-partially-correct > p,
-.xmodule_display.xmodule_ProblemBlock div.problem .detailed-targeted-feedback-correct > p {
- margin: 0;
- font-weight: normal;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.capa_alert {
- margin-top: var(--baseline, 20px);
- padding: 8px 12px;
- border: 1px solid var(--warning-color, #ffc01f);
- border-radius: 3px;
- background: var(--warning-color-accent, #fffcdd);
- font-size: 0.9em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification {
- float: left;
- margin-top: calc(var(--baseline, 20px) / 2);
- padding: calc((var(--baseline, 20px) / 2.5)) calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 5))
- calc((var(--baseline, 20px) / 2));
- line-height: var(--base-line-height, 1.5em);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.success {
- border-top: 3px solid var(--success, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.success .icon {
- margin-right: 15px;
- color: var(--success, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.error {
- border-top: 3px solid var(--danger, #b20610);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.error .icon {
- margin-right: 15px;
- color: var(--danger, #b20610);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.warning {
- border-top: 3px solid var(--warning, #e2c01f);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.warning .icon {
- margin-right: 15px;
- color: var(--warning, #e2c01f);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.general {
- border-top: 3px solid var(--general-color-accent, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.general .icon {
- margin-right: 15px;
- color: var(--general-color-accent, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint {
- border: 1px solid var(--uxpl-gray-background, #d9d9d9);
- border-radius: 6px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint .icon {
- margin-right: calc(3 * var(--baseline, 20px) / 4);
- color: var(--uxpl-gray-dark, #111111);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint li[class*="hint-index-"] {
- color: var(--uxpl-gray-base, #414141);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification.problem-hint li[class*="hint-index-"] > strong {
- color: var(--uxpl-gray-dark, #111111);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification .icon {
- float: left;
- position: relative;
- top: calc(var(--baseline, 20px) / 5);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-message {
- display: inline-block;
- width: 69.38776%;
- margin-bottom: 8px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-message ol {
- list-style: none outside none;
- padding: 0;
- margin: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-message ol li:not(:last-child) {
- margin-bottom: calc(var(--baseline, 20px) / 4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification li[class*="hint-index-"] ul,
-.xmodule_display.xmodule_ProblemBlock div.problem .notification li[class*="hint-index-"] ol {
- padding: 0 0 0 1em;
- margin-left: 0.75rem;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification li[class*="hint-index-"] ol {
- list-style: decimal outside none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification .notification-btn-wrapper {
- float: right;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification-btn {
- float: right;
- padding: calc((var(--baseline, 20px) / 10)) calc((var(--baseline, 20px) / 4));
- min-width: calc((var(--baseline, 20px) * 3));
- display: block;
- clear: both;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .notification-btn:first-child {
- margin-bottom: calc(var(--baseline, 20px) / 4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem button:hover {
- background-image: none;
- box-shadow: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem button:focus {
- box-shadow: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem button.btn-default {
- background-color: transparent;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem button.btn-brand:hover {
- background-color: var(--btn-brand-focus-background, #065683);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .review-btn {
- color: var(--blue, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .review-btn.sr {
- color: var(--blue, #0075b4);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem div.capa_reset {
- padding: 25px;
- background-color: var(--error-color-light, #f95861);
- border: 1px solid var(--error-color, #cb0712);
- border-radius: 3px;
- font-size: 1em;
- margin-top: calc(var(--baseline, 20px) / 2);
- margin-bottom: calc(var(--baseline, 20px) / 2);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .capa_reset > h2 {
- color: #a00;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .capa_reset li {
- font-size: 0.9em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hints {
- border: 1px solid var(--gray-l3, #c8c8c8);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hints h3 {
- padding: 9px;
- border-bottom: 1px solid #e3e3e3;
- background: #eee;
- text-shadow: 0 1px 0 var(--white, #fff);
- font-size: 1em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hints div {
- border-bottom: 1px solid #ddd;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hints div:last-child {
- border-bottom: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hints div p {
- margin-bottom: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hints div header a {
- display: block;
- padding: 9px;
- background: var(--gray-l6, #f8f8f8);
- box-shadow: inset 0 0 0 1px var(--white, #fff);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .hints div > section {
- padding: 9px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test {
- padding-top: 18px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test header {
- margin-bottom: 12px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test header h3 {
- color: #aaa;
- font-style: normal;
- font-size: 0.9em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test > section {
- position: relative;
- margin-bottom: calc((var(--baseline, 20px) / 2));
- padding: 9px 9px var(--baseline, 20px);
- border: 1px solid #ddd;
- border-radius: 3px;
- background: var(--white, #fff);
- box-shadow: inset 0 0 0 1px #eee;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test > section p:last-of-type {
- margin-bottom: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test > section .shortform {
- margin-bottom: 0.6em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test > section a.full {
- position: absolute;
- top: 0;
- right: 0;
- bottom: 1px;
- left: 0;
- box-sizing: border-box;
- display: block;
- padding: calc((var(--baseline, 20px) / 5));
- background: var(--gray-l4, #e4e4e4);
- text-align: right;
- font-size: 1em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test > section a.full.full-top {
- position: absolute;
- top: 1px;
- right: 0;
- bottom: auto;
- left: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .test > section a.full.full-bottom {
- position: absolute;
- top: auto;
- right: 0;
- bottom: 1px;
- left: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section {
- padding-top: calc((var(--baseline, 20px) * 1.5));
- padding-left: var(--baseline, 20px);
- background-color: #fafafa;
- color: #2c2c2c;
- font-size: 1em;
- font-family: monospace;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section header {
- font-size: 1.4em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform {
- margin: 0;
- padding: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-errors {
- margin: calc((var(--baseline, 20px) / 4));
- padding: calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 2)) calc((var(--baseline, 20px) / 2))
- calc((var(--baseline, 20px) * 2));
- background: var(--icon-incorrect) center left no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-errors li {
- color: #b00;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output {
- margin: calc(var(--baseline, 20px) / 4);
- padding: var(--baseline, 20px) 0 calc((var(--baseline, 20px) * 0.75)) 50px;
- border-top: 1px solid #ddd;
- border-left: var(--baseline, 20px) solid #fafafa;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output h4 {
- font-size: 1em;
- font-family: monospace;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output dl {
- margin: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output dt {
- margin-top: var(--baseline, 20px);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-output dd {
- margin-left: 24pt;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-correct {
- background: var(--icon-correct) left 20px no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .external-grader-message
- section
- .longform
- .result-correct
- .result-actual-output {
- color: #090;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-partially-correct {
- background: var(--icon-partially-correct) left 20px no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .external-grader-message
- section
- .longform
- .result-partially-correct
- .result-actual-output {
- color: #090;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .result-incorrect {
- background: var(--icon-incorrect) left 20px no-repeat;
-}
-
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .external-grader-message
- section
- .longform
- .result-incorrect
- .result-actual-output {
- color: #b00;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .markup-text {
- margin: calc((var(--baseline, 20px) / 4));
- padding: var(--baseline, 20px) 0 15px 50px;
- border-top: 1px solid #ddd;
- border-left: 20px solid #fafafa;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .markup-text bs {
- color: #b00;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .external-grader-message section .longform .markup-text bg {
- color: #bda046;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric tr {
- margin: calc((var(--baseline, 20px) / 2)) 0;
- height: 100%;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric td {
- margin: calc((var(--baseline, 20px) / 2)) 0;
- padding: var(--baseline, 20px) 0;
- height: 100%;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric th {
- margin: calc((var(--baseline, 20px) / 4));
- padding: calc((var(--baseline, 20px) / 4));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric label,
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric .view-only {
- position: relative;
- display: inline-block;
- margin: 3px;
- padding: calc((var(--baseline, 20px) * 0.75));
- min-width: 50px;
- min-height: 50px;
- width: 150px;
- height: 100%;
- background-color: var(--gray-l3, #c8c8c8);
- font-size: 0.9em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric .grade {
- position: absolute;
- right: 0;
- bottom: 0;
- margin: calc((var(--baseline, 20px) / 2));
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric .selected-grade {
- background: #666;
- color: white;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric input[type="radio"]:checked + label {
- background: #666;
- color: white;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .rubric input[class="score-selection"] {
- display: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input {
- margin: 0 0 1em 0;
- border: 1px solid var(--gray-l3, #c8c8c8);
- border-radius: 1em;
- /* for debugging the input value field. enable the debug flag on the inputtype */
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .annotation-header {
- padding: 0.5em 1em;
- border-bottom: 1px solid var(--gray-l3, #c8c8c8);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .annotation-body {
- padding: 0.5em 1em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input a.annotation-return {
- float: right;
- font: inherit;
- font-weight: normal;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input a.annotation-return::after {
- content: " \2191";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .block,
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags {
- margin: 0.5em 0;
- padding: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .block-highlight {
- padding: 0.5em;
- border: 1px solid rgba(214, 214, 0, 0.3);
- background-color: rgba(255, 255, 10, 0.3);
- color: #333;
- font-style: normal;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .block-comment {
- font-style: italic;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags {
- display: block;
- margin-left: 1em;
- list-style-type: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li {
- position: relative;
- display: block;
- margin: 1em 0 0 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag {
- display: inline-block;
- margin-left: calc((var(--baseline, 20px) * 2));
- border: 1px solid #666666;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag.selected {
- background-color: rgba(255, 255, 10, 0.3);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag-status {
- position: absolute;
- left: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag-status,
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input ul.tags li .tag {
- padding: 0.25em 0.5em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input textarea.comment {
- padding: 0.2em 0.4em;
- width: 100%;
- height: 7.2em;
- line-height: 1.4em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .answer-annotation {
- display: block;
- margin: 0;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value {
- margin: 1em 0;
- padding: 1em;
- border: 1px solid var(--black, #000);
- background-color: #999;
- color: var(--white, #fff);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value input[type="text"] {
- width: 100%;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value pre {
- background-color: var(--gray-l3, #c8c8c8);
- color: var(--black, #000);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .debug-value::before {
- display: block;
- content: "debug input value";
- font-size: 1.5em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup input[type="text"] {
- margin-bottom: 0.5em;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label.choicetextgroup_correct input[type="text"],
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup section.choicetextgroup_correct input[type="text"] {
- border-color: var(--correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- label.choicetextgroup_partially-correct
- input[type="text"],
-.xmodule_display.xmodule_ProblemBlock
- div.problem
- .choicetextgroup
- section.choicetextgroup_partially-correct
- input[type="text"] {
- border-color: var(--partially-correct, #008100);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup label.choicetextgroup_show_correct::after,
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup section.choicetextgroup_show_correct::after {
- margin-left: calc((var(--baseline, 20px) * 0.75));
- content: var(--icon-correct);
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .choicetextgroup span.mock_label {
- cursor: default;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status {
- display: inline-block;
- position: relative;
- top: 3px;
- width: 25px;
- height: 20px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unsubmitted .status-icon,
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unanswered .status-icon {
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unsubmitted .status-message,
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .status.unanswered .status-message {
- display: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .correct .status-icon::after {
- color: var(--correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .incorrect .status-icon::after {
- color: var(--incorrect, #b20610);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .partially-correct .status-icon::after {
- color: var(--partially-correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .imageinput.capa_inputtype .submitted {
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status {
- display: inline-block;
- position: relative;
- top: 3px;
- width: 25px;
- height: 20px;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unsubmitted .status-icon,
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unanswered .status-icon {
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unsubmitted .status-message,
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .tag-status.unanswered .status-message {
- display: none;
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .correct .status-icon::after {
- color: var(--correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .incorrect .status-icon::after {
- color: var(--incorrect, #b20610);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .partially-correct .status-icon::after {
- color: var(--partially-correct, #008100);
- font-size: 1.2em;
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock div.problem .annotation-input .submitted {
- content: "";
-}
-
-.xmodule_display.xmodule_ProblemBlock .problems-wrapper .loading-spinner {
- text-align: center;
- color: var(--gray-d1, #5e5e5e);
-}
diff --git a/xmodule/tests/test_capa_block.py b/xmodule/tests/test_capa_block.py
deleted file mode 100644
index 49860b8fa3cc..000000000000
--- a/xmodule/tests/test_capa_block.py
+++ /dev/null
@@ -1,4149 +0,0 @@
-# pylint: disable=too-many-lines
-"""
-Tests of the Capa XModule
-"""
-
-import datetime
-import json
-import os
-import random
-import textwrap
-import unittest
-from unittest.mock import DEFAULT, Mock, PropertyMock, patch
-from zoneinfo import ZoneInfo
-
-import ddt
-import pytest
-import requests
-import webob
-from codejail.safe_exec import SafeExecException
-from django.test import override_settings
-from django.utils.encoding import smart_str
-from lxml import etree
-from opaque_keys.edx.locator import BlockUsageLocator, CourseLocator
-from webob.multidict import MultiDict
-from xblock.exceptions import NotFoundError
-from xblock.field_data import DictFieldData
-from xblock.fields import ScopeIds
-from xblock.scorable import Score
-from xblocks_contrib.problem.capa import responsetypes
-from xblocks_contrib.problem.capa.correctmap import CorrectMap
-from xblocks_contrib.problem.capa.responsetypes import LoncapaProblemError, ResponseError, StudentInputError
-from xblocks_contrib.problem.capa.tests.test_util import UseUnsafeCodejail
-from xblocks_contrib.problem.capa.xqueue_interface import XQueueInterface
-
-from lms.djangoapps.courseware.user_state_client import XBlockUserState
-from openedx.core.djangolib.testing.utils import skip_unless_lms
-from xmodule.capa_block import ComplexEncoder
-from xmodule.capa_block import _BuiltInProblemBlock as ProblemBlock
-from xmodule.tests import DATA_DIR
-
-from ..capa_block import RANDOMIZATION, SHOWANSWER
-from . import get_test_system
-
-
-class CapaFactory:
- """
- A helper class to create problem blocks with various parameters for testing.
- """
-
- sample_problem_xml = textwrap.dedent(
- """\
-
-
-
- What is pi, to two decimal places?
-
-
-
-
-
- """
- )
-
- num = 0
-
- @classmethod
- def next_num(cls):
- """Increment and return a unique number for naming problems."""
- cls.num += 1
- return cls.num
-
- @classmethod
- def input_key(cls, response_num=2, input_num=1):
- """
- Return the input key to use when passing GET parameters
- """
- return "input_" + cls.answer_key(response_num, input_num)
-
- @classmethod
- def answer_key(cls, response_num=2, input_num=1):
- """
- Return the key stored in the capa problem answer dict
- """
- return (
- f"{'-'.join(['i4x', 'edX', 'capa_test', 'problem', f'SampleProblem{cls.num}'])}_{response_num}_{input_num}"
- )
-
- @classmethod
- def create( # pylint: disable=too-many-arguments,too-many-positional-arguments
- cls,
- attempts=None,
- problem_state=None,
- correct=False,
- xml=None,
- override_get_score=True,
- render_template=None,
- **kwargs,
- ):
- """
- All parameters are optional, and are added to the created problem if specified.
-
- Arguments:
- graceperiod:
- due:
- max_attempts:
- showanswer:
- force_save_button:
- rerandomize: all strings, as specified in the policy for the problem
-
- problem_state: a dict to be serialized into the instance_state of the block.
-
- attempts: also added to instance state. Will be converted to an int.
-
- render_template: pass function or Mock for testing
- """
- location = BlockUsageLocator(
- CourseLocator("edX", "capa_test", "2012_Fall", deprecated=True),
- "problem",
- f"SampleProblem{cls.next_num()}",
- deprecated=True,
- )
- if xml is None:
- xml = cls.sample_problem_xml
- field_data = {"data": xml}
- field_data.update(kwargs)
- if problem_state is not None:
- field_data.update(problem_state)
- if attempts is not None:
- # converting to int here because I keep putting "0" and "1" in the tests
- # since everything else is a string.
- field_data["attempts"] = int(attempts)
-
- system = get_test_system(
- course_id=location.course_key,
- user_is_staff=kwargs.get("user_is_staff", False),
- render_template=render_template or Mock(return_value="Test Template HTML
"),
- )
- block = ProblemBlock(
- system,
- DictFieldData(field_data),
- ScopeIds(None, "problem", location, location),
- )
- assert block.lcp
-
- if override_get_score:
- if correct:
- # TODO: probably better to actually set the internal state properly, but...
- block.score = Score(raw_earned=1, raw_possible=1)
- else:
- block.score = Score(raw_earned=0, raw_possible=1)
-
- block.graded = "False"
- block.weight = 1
- return block
-
-
-class CapaFactoryWithFiles(CapaFactory):
- """
- A factory for creating a Capa problem with files attached.
- """
-
- sample_problem_xml = textwrap.dedent(
- """\
-
-
-
-
-
-
- If you're having trouble with this Project,
- please refer to the Lecture Slides and attend office hours.
-
- {"project": "p3"}
-
-
-
-
-
- If you worked with a partner, enter their username or email address. If you
- worked alone, enter None.
-
-
-
-
-correct=['correct']
-s = str(submission[0]).strip()
-if submission[0] == '':
- correct[0] = 'incorrect'
-
-
-
- """
- )
-
-
-@ddt.ddt
-@skip_unless_lms
-@pytest.mark.django_db
-class ProblemBlockTest(unittest.TestCase): # pylint: disable=too-many-public-methods
- """Tests for various problem types in XBlocks."""
-
- def setUp(self):
- super().setUp()
-
- now = datetime.datetime.now(ZoneInfo("UTC"))
- day_delta = datetime.timedelta(days=1)
- self.yesterday_str = str(now - day_delta)
- self.today_str = str(now)
- self.tomorrow_str = str(now + day_delta)
-
- # in the capa grace period format, not in time delta format
- self.two_day_delta_str = "2 days"
-
- def test_import(self):
- """Verify CapaFactory creates blocks with zero initial score and unique URLs."""
- block = CapaFactory.create()
- assert block.get_score().raw_earned == 0
-
- other_block = CapaFactory.create()
- assert block.get_score().raw_earned == 0
- assert block.url_name != other_block.url_name, "Factory should be creating unique names for each problem"
-
- def test_correct(self):
- """
- Check that the factory creates correct and incorrect problems properly.
- """
- block = CapaFactory.create()
- assert block.get_score().raw_earned == 0
-
- other_block = CapaFactory.create(correct=True)
- assert other_block.get_score().raw_earned == 1
-
- def test_get_score(self):
- """
- Tests the internals of get_score. In keeping with the ScorableXBlock spec,
- Capa blocks store their score independently of the LCP internals, so it must
- be explicitly updated.
- """
- student_answers = {"1_2_1": "abcd"}
- correct_map = CorrectMap(answer_id="1_2_1", correctness="correct", npoints=0.9)
- block = CapaFactory.create(correct=True, override_get_score=False)
- block.lcp.correct_map = correct_map
- block.lcp.student_answers = student_answers
- assert block.get_score().raw_earned == 0.0
- block.set_score(block.score_from_lcp(block.lcp))
- assert block.get_score().raw_earned == 0.9
-
- other_correct_map = CorrectMap(answer_id="1_2_1", correctness="incorrect", npoints=0.1)
- other_block = CapaFactory.create(correct=False, override_get_score=False)
- other_block.lcp.correct_map = other_correct_map
- other_block.lcp.student_answers = student_answers
- assert other_block.get_score().raw_earned == 0.0
- other_block.set_score(other_block.score_from_lcp(other_block.lcp))
- assert other_block.get_score().raw_earned == 0.1
-
- def test_showanswer_default(self):
- """
- Make sure the show answer logic does the right thing.
- """
- # default, no due date, showanswer 'closed', so problem is open, and show_answer
- # not visible.
- problem = CapaFactory.create()
- assert not problem.answer_available()
-
- @ddt.data(
- (requests.exceptions.ReadTimeout, (1, "failed to read from the server")),
- (requests.exceptions.ConnectionError, (1, "cannot connect to server")),
- )
- @ddt.unpack
- def test_xqueue_request_exception(self, exception, result):
- """
- Makes sure that platform will raise appropriate exception in case of
- connect/read timeout(s) to request to xqueue
- """
- xqueue_interface = XQueueInterface("http://example.com/xqueue", Mock())
- with patch.object(xqueue_interface.session, "post", side_effect=exception):
- response = xqueue_interface._http_post("http://some/fake/url", {}) # pylint: disable=protected-access
- assert response == result
-
- def test_showanswer_attempted(self):
- """Check answer availability changes after attempting the problem."""
- problem = CapaFactory.create(showanswer="attempted")
- assert not problem.answer_available()
- problem.attempts = 1
- assert problem.answer_available()
-
- @ddt.data(
- # If show_correctness=always, Answer is visible after attempted
- (
- {
- "showanswer": "attempted",
- "max_attempts": "1",
- "show_correctness": "always",
- },
- False,
- True,
- ),
- # If show_correctness=never, Answer is never visible
- (
- {
- "showanswer": "attempted",
- "max_attempts": "1",
- "show_correctness": "never",
- },
- False,
- False,
- ),
- # If show_correctness=past_due, answer is not visible before due date
- (
- {
- "showanswer": "attempted",
- "show_correctness": "past_due",
- "max_attempts": "1",
- "due": "tomorrow_str",
- },
- False,
- False,
- ),
- # If show_correctness=past_due, answer is visible after due date
- (
- {
- "showanswer": "attempted",
- "show_correctness": "past_due",
- "max_attempts": "1",
- "due": "yesterday_str",
- },
- True,
- True,
- ),
- )
- @ddt.unpack
- def test_showanswer_hide_correctness(
- self, problem_data, answer_available_no_attempt, answer_available_after_attempt
- ):
- """
- Ensure that the answer will not be shown when correctness is being hidden.
- """
- if "due" in problem_data:
- problem_data["due"] = getattr(self, problem_data["due"])
- problem = CapaFactory.create(**problem_data)
- assert problem.answer_available() == answer_available_no_attempt
- problem.attempts = 1
- assert problem.answer_available() == answer_available_after_attempt
-
- def test_showanswer_closed(self):
- """Check show answer visibility with showanswer='closed' and various conditions."""
-
- # can see after attempts used up, even with due date in the future
- used_all_attempts = CapaFactory.create(
- showanswer="closed", max_attempts="1", attempts="1", due=self.tomorrow_str
- )
- assert used_all_attempts.answer_available()
-
- # can see after due date
- after_due_date = CapaFactory.create(showanswer="closed", max_attempts="1", attempts="0", due=self.yesterday_str)
-
- assert after_due_date.answer_available()
-
- # can't see because attempts left
- attempts_left_open = CapaFactory.create(
- showanswer="closed", max_attempts="1", attempts="0", due=self.tomorrow_str
- )
- assert not attempts_left_open.answer_available()
-
- # Can't see because grace period hasn't expired
- still_in_grace = CapaFactory.create(
- showanswer="closed",
- max_attempts="1",
- attempts="0",
- due=self.yesterday_str,
- graceperiod=self.two_day_delta_str,
- )
- assert not still_in_grace.answer_available()
-
- def test_showanswer_correct_or_past_due(self):
- """
- With showanswer="correct_or_past_due" should show answer after the answer is correct
- or after the problem is closed for everyone--e.g. after due date + grace period.
- """
-
- # can see because answer is correct, even with due date in the future
- answer_correct = CapaFactory.create(
- showanswer="correct_or_past_due", max_attempts="1", attempts="0", due=self.tomorrow_str, correct=True
- )
- assert answer_correct.answer_available()
-
- # can see after due date, even when answer isn't correct
- past_due_date = CapaFactory.create(
- showanswer="correct_or_past_due", max_attempts="1", attempts="0", due=self.yesterday_str
- )
- assert past_due_date.answer_available()
-
- # can also see after due date when answer _is_ correct
- past_due_date_correct = CapaFactory.create(
- showanswer="correct_or_past_due", max_attempts="1", attempts="0", due=self.yesterday_str, correct=True
- )
- assert past_due_date_correct.answer_available()
-
- # Can't see because grace period hasn't expired and answer isn't correct
- still_in_grace = CapaFactory.create(
- showanswer="correct_or_past_due",
- max_attempts="1",
- attempts="1",
- due=self.yesterday_str,
- graceperiod=self.two_day_delta_str,
- )
- assert not still_in_grace.answer_available()
-
- def test_showanswer_past_due(self):
- """
- With showanswer="past_due" should only show answer after the problem is closed
- for everyone--e.g. after due date + grace period.
- """
-
- # can't see after attempts used up, even with due date in the future
- used_all_attempts = CapaFactory.create(
- showanswer="past_due", max_attempts="1", attempts="1", due=self.tomorrow_str
- )
- assert not used_all_attempts.answer_available()
-
- # can see after due date
- past_due_date = CapaFactory.create(
- showanswer="past_due", max_attempts="1", attempts="0", due=self.yesterday_str
- )
- assert past_due_date.answer_available()
-
- # can't see because attempts left
- attempts_left_open = CapaFactory.create(
- showanswer="past_due", max_attempts="1", attempts="0", due=self.tomorrow_str
- )
- assert not attempts_left_open.answer_available()
-
- # Can't see because grace period hasn't expired, even though have no more
- # attempts.
- still_in_grace = CapaFactory.create(
- showanswer="past_due",
- max_attempts="1",
- attempts="1",
- due=self.yesterday_str,
- graceperiod=self.two_day_delta_str,
- )
- assert not still_in_grace.answer_available()
-
- def test_showanswer_after_attempts_with_max(self):
- """
- Button should not be visible when attempts < required attempts.
-
- Even with max attempts set, the show answer button should only
- show up after the user has attempted answering the question for
- the requisite number of times, i.e `attempts_before_showanswer_button`
- """
- problem = CapaFactory.create(
- showanswer="after_attempts",
- attempts="2",
- attempts_before_showanswer_button="3",
- max_attempts="5",
- )
- assert not problem.answer_available()
-
- def test_showanswer_after_attempts_no_max(self):
- """
- Button should not be visible when attempts < required attempts.
-
- Even when max attempts is NOT set, the answer should still
- only be available after the student has attempted the
- problem at least `attempts_before_showanswer_button` times
- """
- problem = CapaFactory.create(
- showanswer="after_attempts",
- attempts="2",
- attempts_before_showanswer_button="3",
- )
- assert not problem.answer_available()
-
- def test_showanswer_after_attempts_used_all_attempts(self):
- """
- Button should be visible even after all attempts are used up.
-
- As long as the student has attempted the question for
- the requisite number of times, then the show ans. button is
- visible even after they have exhausted their attempts.
- """
- problem = CapaFactory.create(
- showanswer="after_attempts",
- attempts_before_showanswer_button="2",
- max_attempts="3",
- attempts="3",
- due=self.tomorrow_str,
- )
- assert problem.answer_available()
-
- def test_showanswer_after_attempts_past_due_date(self):
- """
- Show Answer button should be visible even after the due date.
-
- As long as the student has attempted the problem for the requisite
- number of times, the answer should be available past the due date.
- """
- problem = CapaFactory.create(
- showanswer="after_attempts",
- attempts_before_showanswer_button="2",
- attempts="2",
- due=self.yesterday_str,
- )
- assert problem.answer_available()
-
- def test_showanswer_after_attempts_still_in_grace(self):
- """
- If attempts > required attempts, ans. is available in grace period.
-
- As long as the user has attempted for the requisite # of times,
- the show answer button is visible throughout the grace period.
- """
- problem = CapaFactory.create(
- showanswer="after_attempts",
- after_attempts="3",
- attempts="4",
- due=self.yesterday_str,
- graceperiod=self.two_day_delta_str,
- )
- assert problem.answer_available()
-
- def test_showanswer_after_attempts_large(self):
- """
- If required attempts > max attempts then required attempts = max attempts.
-
- Ensure that if attempts_before_showanswer_button > max_attempts,
- the button should show up after all attempts are used up,
- i.e after_attempts falls back to max_attempts
- """
- problem = CapaFactory.create(
- showanswer="after_attempts",
- attempts_before_showanswer_button="5",
- max_attempts="3",
- attempts="3",
- )
- assert problem.answer_available()
-
- def test_showanswer_after_attempts_zero(self):
- """
- Button should always be visible if required min attempts = 0.
-
- If attempts_before_showanswer_button = 0, then the show answer
- button should be visible at all times.
- """
- problem = CapaFactory.create(
- showanswer="after_attempts",
- attempts_before_showanswer_button="0",
- attempts="0",
- )
- assert problem.answer_available()
-
- def test_showanswer_finished(self):
- """
- With showanswer="finished" should show answer after the problem is closed,
- or after the answer is correct.
- """
-
- # can see after attempts used up, even with due date in the future
- used_all_attempts = CapaFactory.create(
- showanswer="finished", max_attempts="1", attempts="1", due=self.tomorrow_str
- )
- assert used_all_attempts.answer_available()
-
- # can see after due date
- past_due_date = CapaFactory.create(
- showanswer="finished", max_attempts="1", attempts="0", due=self.yesterday_str
- )
- assert past_due_date.answer_available()
-
- # can't see because attempts left and wrong
- attempts_left_open = CapaFactory.create(
- showanswer="finished", max_attempts="1", attempts="0", due=self.tomorrow_str
- )
- assert not attempts_left_open.answer_available()
-
- # _can_ see because attempts left and right
- correct_ans = CapaFactory.create(
- showanswer="finished", max_attempts="1", attempts="0", due=self.tomorrow_str, correct=True
- )
- assert correct_ans.answer_available()
-
- # Can see even though grace period hasn't expired, because have no more
- # attempts.
- still_in_grace = CapaFactory.create(
- showanswer="finished",
- max_attempts="1",
- attempts="1",
- due=self.yesterday_str,
- graceperiod=self.two_day_delta_str,
- )
- assert still_in_grace.answer_available()
-
- def test_showanswer_answered(self):
- """
- Tests that with showanswer="answered" should show answer after the problem is correctly answered.
- It should *NOT* show answer if the answer is incorrect.
- """
- # Can not see "Show Answer" when student answer is wrong
- answer_wrong = CapaFactory.create(
- showanswer=SHOWANSWER.ANSWERED, max_attempts="1", attempts="0", due=self.tomorrow_str, correct=False
- )
- assert not answer_wrong.answer_available()
-
- # Expect to see "Show Answer" when answer is correct
- answer_correct = CapaFactory.create(
- showanswer=SHOWANSWER.ANSWERED, max_attempts="1", attempts="0", due=self.tomorrow_str, correct=True
- )
- assert answer_correct.answer_available()
-
- @ddt.data("", "other-value")
- def test_show_correctness_other(self, show_correctness):
- """
- Test that correctness is visible if show_correctness is not set to one of the values
- from SHOW_CORRECTNESS constant.
- """
- problem = CapaFactory.create(show_correctness=show_correctness)
- assert problem.correctness_available()
-
- def test_show_correctness_default(self):
- """
- Test that correctness is visible by default.
- """
- problem = CapaFactory.create()
- assert problem.correctness_available()
-
- def test_show_correctness_never(self):
- """
- Test that correctness is hidden when show_correctness turned off.
- """
- problem = CapaFactory.create(show_correctness="never")
- assert not problem.correctness_available()
-
- @ddt.data(
- # Correctness not visible if due date in the future, even after using up all attempts
- (
- {
- "show_correctness": "past_due",
- "max_attempts": "1",
- "attempts": "1",
- "due": "tomorrow_str",
- },
- False,
- ),
- # Correctness visible if due date in the past
- (
- {
- "show_correctness": "past_due",
- "max_attempts": "1",
- "attempts": "0",
- "due": "yesterday_str",
- },
- True,
- ),
- # Correctness not visible if due date in the future
- (
- {
- "show_correctness": "past_due",
- "max_attempts": "1",
- "attempts": "0",
- "due": "tomorrow_str",
- },
- False,
- ),
- # Correctness not visible because grace period hasn't expired,
- # even after using up all attempts
- (
- {
- "show_correctness": "past_due",
- "max_attempts": "1",
- "attempts": "1",
- "due": "yesterday_str",
- "graceperiod": "two_day_delta_str",
- },
- False,
- ),
- )
- @ddt.unpack
- def test_show_correctness_past_due(self, problem_data, expected_result):
- """
- Test that with show_correctness="past_due", correctness will only be visible
- after the problem is closed for everyone--e.g. after due date + grace period.
- """
- problem_data["due"] = getattr(self, problem_data["due"])
- if "graceperiod" in problem_data:
- problem_data["graceperiod"] = getattr(self, problem_data["graceperiod"])
- problem = CapaFactory.create(**problem_data)
- assert problem.correctness_available() == expected_result
-
- def test_closed(self):
- """Verify problem closed status based on attempts and due date."""
-
- # Attempts < Max attempts --> NOT closed
- block = CapaFactory.create(max_attempts="1", attempts="0")
- assert not block.closed()
-
- # Attempts < Max attempts --> NOT closed
- block = CapaFactory.create(max_attempts="2", attempts="1")
- assert not block.closed()
-
- # Attempts = Max attempts --> closed
- block = CapaFactory.create(max_attempts="1", attempts="1")
- assert block.closed()
-
- # Attempts > Max attempts --> closed
- block = CapaFactory.create(max_attempts="1", attempts="2")
- assert block.closed()
-
- # Max attempts = 0 --> closed
- block = CapaFactory.create(max_attempts="0", attempts="2")
- assert block.closed()
-
- # Past due --> closed
- block = CapaFactory.create(max_attempts="1", attempts="0", due=self.yesterday_str)
- assert block.closed()
-
- @patch.object(ProblemBlock, "course_end_date", new_callable=PropertyMock)
- def test_closed_for_archive(self, mock_course_end_date):
- """Check closed status for archived and active courses with/without grace periods."""
-
- # Utility to create a datetime object in the past
- def past_datetime(days):
- return datetime.datetime.now(ZoneInfo("UTC")) - datetime.timedelta(days=days)
-
- # Utility to create a datetime object in the future
- def future_datetime(days):
- return datetime.datetime.now(ZoneInfo("UTC")) + datetime.timedelta(days=days)
-
- block = CapaFactory.create(max_attempts="1", attempts="0")
-
- # For active courses without graceperiod
- mock_course_end_date.return_value = future_datetime(10)
- assert not block.closed()
-
- # For archive courses without graceperiod
- mock_course_end_date.return_value = past_datetime(10)
- assert block.closed()
-
- # For active courses with graceperiod
- mock_course_end_date.return_value = future_datetime(10)
- block.graceperiod = datetime.timedelta(days=2)
- assert not block.closed()
-
- # For archive courses with graceperiod
- mock_course_end_date.return_value = past_datetime(2)
- block.graceperiod = datetime.timedelta(days=3)
- assert not block.closed()
-
- def test_parse_get_params(self):
- """Test parsing of GET parameters into response dictionaries with validation."""
-
- # Valid GET param dict
- # 'input_5' intentionally left unset,
- valid_get_dict = MultiDict(
- {
- "input_1": "test",
- "input_1_2": "test",
- "input_1_2_3": "test",
- "input_[]_3": "test",
- "input_4": None,
- "input_6": 5,
- }
- )
-
- result = ProblemBlock.make_dict_of_responses(valid_get_dict)
-
- # Expect that we get a dict with "input" stripped from key names
- # and that we get the same values back
- for key in result:
- original_key = "input_" + key
- assert original_key in valid_get_dict, f"Output dict should have key {original_key}"
- assert valid_get_dict[original_key] == result[key]
-
- # Valid GET param dict with list keys
- # Each tuple represents a single parameter in the query string
- valid_get_dict = MultiDict((("input_2[]", "test1"), ("input_2[]", "test2")))
- result = ProblemBlock.make_dict_of_responses(valid_get_dict)
- assert "2" in result
- assert ["test1", "test2"] == result["2"]
-
- # If we use [] at the end of a key name, we should always
- # get a list, even if there's just one value
- valid_get_dict = MultiDict({"input_1[]": "test"})
- result = ProblemBlock.make_dict_of_responses(valid_get_dict)
- assert result["1"] == ["test"]
-
- # If we have no underscores in the name, then the key is invalid
- invalid_get_dict = MultiDict({"input": "test"})
- with pytest.raises(ValueError): # noqa: PT011
- result = ProblemBlock.make_dict_of_responses(invalid_get_dict)
-
- # Two equivalent names (one list, one non-list)
- # One of the values would overwrite the other, so detect this
- # and raise an exception
- invalid_get_dict = MultiDict({"input_1[]": "test 1", "input_1": "test 2"})
- with pytest.raises(ValueError): # noqa: PT011
- result = ProblemBlock.make_dict_of_responses(invalid_get_dict)
-
- def test_submit_problem_correct(self):
- """Verify submitting a correct problem updates attempts, grading, and HTML content."""
-
- block = CapaFactory.create(attempts=1)
-
- # Simulate that all answers are marked correct, no matter
- # what the input is, by patching CorrectMap.is_correct()
- # Also simulate rendering the HTML
- with patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct") as mock_is_correct:
- with patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html") as mock_html:
- mock_is_correct.return_value = True
- mock_html.return_value = "Test HTML"
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- # Expect that the problem is marked correct
- assert result["success"] == "correct"
-
- # Expect that we get the (mocked) HTML
- assert result["contents"] == "Test HTML"
-
- # Expect that the number of attempts is incremented by 1
- assert block.attempts == 2
- # and that this was considered attempt number 2 for grading purposes
- assert block.lcp.context["attempt"] == 2
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_with_grading_method_disable(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test that without a specific grading method, the score behaves as
- standard (Last Attempt).
- """
- block = CapaFactory.create(attempts=0, max_attempts=3)
- mock_html.return_value = "Test HTML"
-
- # First Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Second Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.50"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 2
- assert block.lcp.context["attempt"] == 2
- assert block.score == Score(raw_earned=0, raw_possible=1)
-
- # Third Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 3
- assert block.lcp.context["attempt"] == 3
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_with_grading_method_enable(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test that the grading method is enabled when submit a problem.
- Then, the `get_score_with_grading_method` method should be called.
- """
- block = CapaFactory.create(attempts=0)
- mock_html.return_value = "Test HTML"
- mock_is_correct.return_value = True
-
- with patch.object(
- ProblemBlock, "get_score_with_grading_method", wraps=block.get_score_with_grading_method
- ) as mock_get_score:
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=1, raw_possible=1)
- mock_get_score.assert_called()
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_grading_method_always_enabled(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test problem submission when grading method is always enabled by default.
-
- The final score is calculated according to the grading method, as grading
- is now always enabled.
- """
- block = CapaFactory.create(attempts=0, max_attempts=4, grading_method="highest_score")
- mock_html.return_value = "Test HTML"
-
- # First Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Second Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.50"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 2
- assert block.lcp.context["attempt"] == 2
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Third Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.96"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 3
- assert block.lcp.context["attempt"] == 3
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Fourth Attempt
- block.grading_method = "highest_score"
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.99"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 4
- assert block.lcp.context["attempt"] == 4
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_grading_method_always_enabled_highest_score(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test problem submission when grading method is always enabled by default
- with 'highest_score' grading method.
-
- The final score is calculated according to the grading method, as grading
- is now always enabled.
- """
- block = CapaFactory.create(attempts=0, max_attempts=4, grading_method="highest_score")
- mock_html.return_value = "Test HTML"
-
- # First Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Second Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.50"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 2
- assert block.lcp.context["attempt"] == 2
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Third Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.96"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 3
- assert block.lcp.context["attempt"] == 3
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Fourth Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 4
- assert block.lcp.context["attempt"] == 4
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_correct_last_score(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test the `last_score` grading method.
-
- When the grading method is `last_score`,
- the final score is always the last attempt.
- """
- # default grading method is last_score
- block = CapaFactory.create(attempts=0, max_attempts=2)
- mock_html.return_value = "Test HTML"
-
- # First Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Second Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.54"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 2
- assert block.lcp.context["attempt"] == 2
- assert block.score == Score(raw_earned=0, raw_possible=1)
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_correct_highest_score(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test the `highest_score` grading method.
-
- When the grading method is `highest_score`,
- the final score is the highest score among all attempts.
- """
- block = CapaFactory.create(attempts=0, max_attempts=2, grading_method="highest_score")
- mock_html.return_value = "Test HTML"
-
- # First Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Second Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.54"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 2
- assert block.lcp.context["attempt"] == 2
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_correct_first_score(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test the `first_score` grading method.
-
- When the grading method is `first_score`,
- the final score is the first score among all attempts.
- """
- block = CapaFactory.create(attempts=0, max_attempts=2, grading_method="first_score")
- mock_html.return_value = "Test HTML"
-
- # First Attempt
- mock_is_correct.return_value = False
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=0, raw_possible=1)
-
- # Second Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.54"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 2
- assert block.lcp.context["attempt"] == 2
- assert block.score == Score(raw_earned=0, raw_possible=1)
-
- @patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct")
- @patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html")
- def test_submit_problem_correct_average_score(self, mock_html: Mock, mock_is_correct: Mock):
- """
- Test the `average_score` grading method.
-
- When the grading method is `average_score`,
- the final score is the average score among all attempts.
- """
- block = CapaFactory.create(attempts=0, max_attempts=4, grading_method="average_score")
- mock_html.return_value = "Test HTML"
-
- # First Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.14"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
- assert block.score == Score(raw_earned=0, raw_possible=1)
-
- # Second Attempt
- mock_is_correct.return_value = True
- get_request_dict = {CapaFactory.input_key(): "3.54"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 2
- assert block.lcp.context["attempt"] == 2
- assert block.score == Score(raw_earned=0.5, raw_possible=1)
-
- # Third Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "3.45"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 3
- assert block.lcp.context["attempt"] == 3
- assert block.score == Score(raw_earned=0.33, raw_possible=1)
-
- # Fourth Attempt
- mock_is_correct.return_value = False
- get_request_dict = {CapaFactory.input_key(): "41.3"}
-
- block.submit_problem(get_request_dict)
-
- assert block.attempts == 4
- assert block.lcp.context["attempt"] == 4
- assert block.score == Score(raw_earned=0.25, raw_possible=1)
-
- def test_submit_problem_incorrect(self):
- """Verify submitting an incorrect answer marks failure and increments attempts."""
-
- block = CapaFactory.create(attempts=0)
-
- # Simulate marking the input incorrect
- with patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct") as mock_is_correct:
- mock_is_correct.return_value = False
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "0"}
- result = block.submit_problem(get_request_dict)
-
- # Expect that the problem is marked correct
- assert result["success"] == "incorrect"
-
- # Expect that the number of attempts is incremented by 1
- assert block.attempts == 1
- # and that this is considered the first attempt
- assert block.lcp.context["attempt"] == 1
-
- def test_submit_problem_closed(self):
- """Ensure submitting a closed problem raises NotFoundError and does not increment attempts."""
- block = CapaFactory.create(attempts=3)
-
- # Problem closed -- cannot submit
- # Simulate that ProblemBlock.closed() always returns True
- with patch("xmodule.capa_block._BuiltInProblemBlock.closed") as mock_closed:
- mock_closed.return_value = True
- with pytest.raises(NotFoundError): # noqa: PT012
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- # Expect that number of attempts NOT incremented
- assert block.attempts == 3
-
- @ddt.data(RANDOMIZATION.ALWAYS, "true")
- def test_submit_problem_resubmitted_with_randomize(self, rerandomize):
- """Verify resubmission is blocked when rerandomization is enabled and problem is done."""
- # Randomize turned on
- block = CapaFactory.create(rerandomize=rerandomize, attempts=0)
-
- # Simulate that the problem is completed
- block.done = True
-
- # Expect that we cannot submit
- with pytest.raises(NotFoundError): # noqa: PT012
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- # Expect that number of attempts NOT incremented
- assert block.attempts == 0
-
- @ddt.data(RANDOMIZATION.NEVER, "false", RANDOMIZATION.PER_STUDENT)
- def test_submit_problem_resubmitted_no_randomize(self, rerandomize):
- """Verify resubmission succeeds when rerandomization is disabled."""
- # Randomize turned off
- block = CapaFactory.create(rerandomize=rerandomize, attempts=0, done=True)
-
- # Expect that we can submit successfully
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- assert result["success"] == "correct"
-
- # Expect that number of attempts IS incremented, still same attempt
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
-
- def test_submit_problem_queued(self):
- """Ensure queued problems return a wait message and do not increment attempts."""
- block = CapaFactory.create(attempts=1)
-
- # Simulate that the problem is queued
- multipatch = patch.multiple(
- "xblocks_contrib.problem.capa.capa_problem.LoncapaProblem",
- is_queued=DEFAULT,
- get_recentmost_queuetime=DEFAULT,
- )
- with multipatch as values:
- values["is_queued"].return_value = True
- values["get_recentmost_queuetime"].return_value = datetime.datetime.now(ZoneInfo("UTC"))
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- # Expect an AJAX alert message in 'success'
- assert "You must wait" in result["success"]
-
- # Expect that the number of attempts is NOT incremented
- assert block.attempts == 1
-
- @pytest.mark.django_db
- @patch.object(XQueueInterface, "_http_post")
- def test_submit_problem_with_files(self, mock_xqueue_post):
- """Verify file-upload submissions are sent correctly to XQueue via submit_problem."""
- # Check a problem with uploaded files, using the submit_problem API.
-
- # The files we'll be uploading.
- fnames = ["prog1.py", "prog2.py", "prog3.py"]
- fpaths = [os.path.join(DATA_DIR, "capa", fname) for fname in fnames]
- fileobjs = [open(fpath, encoding="utf-8") for fpath in fpaths] # pylint: disable=consider-using-with
- for fileobj in fileobjs:
- self.addCleanup(fileobj.close)
-
- block = CapaFactoryWithFiles.create()
-
- # Mock the XQueueInterface post method
- mock_xqueue_post.return_value = (0, "ok")
-
- # Create a request dictionary for submit_problem.
- get_request_dict = {
- CapaFactoryWithFiles.input_key(response_num=2): fileobjs,
- CapaFactoryWithFiles.input_key(response_num=3): "None",
- }
-
- block.submit_problem(get_request_dict)
-
- # _http_post is called like this:
- # _http_post(
- # 'http://example.com/xqueue/xqueue/submit/',
- # {
- # 'xqueue_header':
- # '{"lms_key": "df34fb702620d7ae892866ba57572491", '
- # '"lms_callback_url": "/", '
- # '"queue_name": "BerkeleyX-cs188x"}',
- # 'xqueue_body':
- # '{"student_info": "{\\"anonymous_student_id\\": '
- # '\\"student\\", \\"submission_time\\": '
- # '\\"20131117183318\\"}", '
- # '"grader_payload": "{\\"project\\": \\"p3\\"}", '
- # '"student_response": ""}',
- # },
- # files={
- # path(
- # u'/home/ned/edx/edx-platform/common/test/data/uploads/'
- # 'asset.html'
- # ):
- # ,
- # path(
- # u'/home/ned/edx/edx-platform/common/test/data/uploads/'
- # 'image.jpg'
- # ):
- # ,
- # path(
- # u'/home/ned/edx/edx-platform/common/test/data/uploads/'
- # 'textbook.pdf'
- # ):
- # ,
- # },
- # )
-
- assert mock_xqueue_post.call_count == 1
- _, kwargs = mock_xqueue_post.call_args
- self.assertCountEqual(fpaths, list(kwargs["files"].keys())) # noqa: PT009
- for fpath, fileobj in kwargs["files"].items():
- assert fpath == fileobj.name
-
- @pytest.mark.django_db
- @patch.object(XQueueInterface, "_http_post")
- def test_submit_problem_with_files_as_xblock(self, mock_xqueue_post):
- """Verify file-upload submissions work correctly via the XBlock handler API."""
- # Check a problem with uploaded files, using the XBlock API.
-
- # The files we'll be uploading.
- fnames = ["prog1.py", "prog2.py", "prog3.py"]
- fpaths = [os.path.join(DATA_DIR, "capa", fname) for fname in fnames]
- fileobjs = []
- for fpath in fpaths:
- with open(fpath, encoding="utf-8") as f:
- fileobjs.append(f.read())
-
- block = CapaFactoryWithFiles.create()
-
- # Mock the XQueueInterface post method
- mock_xqueue_post.return_value = (0, "ok")
-
- # Create a webob Request with the files uploaded.
- post_data = []
- for fname, fileobj in zip(fnames, fileobjs): # noqa: B905
- post_data.append((CapaFactoryWithFiles.input_key(response_num=2), (fname, fileobj)))
- post_data.append((CapaFactoryWithFiles.input_key(response_num=3), "None"))
- request = webob.Request.blank("/some/fake/url", POST=post_data, content_type="multipart/form-data")
-
- block.handle("xmodule_handler", request, "problem_check")
-
- assert mock_xqueue_post.call_count == 1
- _, kwargs = mock_xqueue_post.call_args
- self.assertCountEqual(fnames, list(kwargs["files"].keys())) # noqa: PT009
- for fpath, fileobj in kwargs["files"].items():
- assert fpath == fileobj.name
-
- def test_submit_problem_error(self):
- """Ensure expected grading errors return messages without incrementing attempts."""
-
- # Try each exception that capa_block should handle
- exception_classes = [StudentInputError, LoncapaProblemError, ResponseError]
- for exception_class in exception_classes:
- # Create the block
- block = CapaFactory.create(attempts=1, user_is_staff=False)
-
- # Simulate answering a problem that raises the exception
- with patch("xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.grade_answers") as mock_grade:
- mock_grade.side_effect = exception_class("test error")
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- # Expect an AJAX alert message in 'success'
- expected_msg = "test error"
-
- assert expected_msg == result["success"]
-
- # Expect that the number of attempts is NOT incremented
- assert block.attempts == 1
- # but that this was considered attempt number 2 for grading purposes
- assert block.lcp.context["attempt"] == 2
-
- def test_submit_problem_error_with_codejail_exception(self):
- """Verify codejail execution errors are sanitized and handled correctly."""
-
- # Try each exception that capa_block should handle
- exception_classes = [StudentInputError, LoncapaProblemError, ResponseError]
- for exception_class in exception_classes:
-
- # Create the block
- block = CapaFactory.create(attempts=1, user_is_staff=False)
-
- # Simulate a codejail exception "Exception: Couldn't execute jailed code"
- with patch("xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.grade_answers") as mock_grade:
- try:
- raise ResponseError(
- "Couldn't execute jailed code: stdout: '', "
- "stderr: 'Traceback (most recent call last):\\n"
- ' File "jailed_code", line 15, in \\n'
- ' exec code in g_dict\\n File "", line 67, in \\n'
- ' File "", line 65, in check_func\\n'
- "Exception: Couldn't execute jailed code\\n' with status code: 1",
- )
- except ResponseError as err:
- mock_grade.side_effect = exception_class(str(err))
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- # Expect an AJAX alert message in 'success' without the text of the stack trace
- expected_msg = "Couldn't execute jailed code"
- assert expected_msg == result["success"]
-
- # Expect that the number of attempts is NOT incremented
- assert block.attempts == 1
- # but that this was considered the second attempt for grading purposes
- assert block.lcp.context["attempt"] == 2
-
- @override_settings(DEBUG=True)
- def test_submit_problem_other_errors(self):
- """
- Test that errors other than the expected kinds give an appropriate message.
-
- See also `test_submit_problem_error` for the "expected kinds" or errors.
- """
- # Create the block
- block = CapaFactory.create(attempts=1, user_is_staff=False)
- block.runtime.is_author_mode = True
-
- # Simulate answering a problem that raises the exception
- with patch("xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.grade_answers") as mock_grade:
- error_msg = "Superterrible error happened: ☠"
- mock_grade.side_effect = Exception(error_msg)
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- # Expect an AJAX alert message in 'success'
- assert error_msg in result["success"]
-
- def test_submit_problem_zero_max_grade(self):
- """
- Test that a capa problem with a max grade of zero doesn't generate an error.
- """
- # Create the block
- block = CapaFactory.create(attempts=1)
-
- # Override the problem score to have a total of zero.
- block.lcp.get_score = lambda: {"score": 0, "total": 0}
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- def test_submit_problem_error_nonascii(self):
- """Ensure non-ASCII error messages are preserved and handled correctly."""
-
- # Try each exception that capa_block should handle
- exception_classes = [StudentInputError, LoncapaProblemError, ResponseError]
- for exception_class in exception_classes:
- # Create the block
- block = CapaFactory.create(attempts=1, user_is_staff=False)
-
- # Simulate answering a problem that raises the exception
- with patch("xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.grade_answers") as mock_grade:
- mock_grade.side_effect = exception_class("ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ")
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- # Expect an AJAX alert message in 'success'
- expected_msg = "ȧƈƈḗƞŧḗḓ ŧḗẋŧ ƒǿř ŧḗşŧīƞɠ"
-
- assert expected_msg == result["success"]
-
- # Expect that the number of attempts is NOT incremented
- assert block.attempts == 1
- # but that this was considered the second attempt for grading purposes
- assert block.lcp.context["attempt"] == 2
-
- def test_submit_problem_error_with_staff_user(self):
- """Verify staff users receive full traceback information on errors."""
-
- # Try each exception that capa block should handle
- for exception_class in [StudentInputError, LoncapaProblemError, ResponseError]:
- # Create the block
- block = CapaFactory.create(attempts=1, user_is_staff=True)
-
- # Simulate answering a problem that raises an exception
- with patch("xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.grade_answers") as mock_grade:
- mock_grade.side_effect = exception_class("test error")
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.submit_problem(get_request_dict)
-
- # Expect an AJAX alert message in 'success'
- assert "test error" in result["success"]
-
- # We DO include traceback information for staff users
- assert "Traceback" in result["success"]
-
- # Expect that the number of attempts is NOT incremented
- assert block.attempts == 1
- # but that it was considered the second attempt for grading purposes
- assert block.lcp.context["attempt"] == 2
-
- @ddt.data(
- ("never", True, None, "submitted"),
- ("never", False, None, "submitted"),
- ("past_due", True, None, "submitted"),
- ("past_due", False, None, "submitted"),
- ("always", True, 1, "correct"),
- ("always", False, 0, "incorrect"),
- )
- @ddt.unpack
- def test_handle_ajax_show_correctness(self, show_correctness, is_correct, expected_score, expected_success):
- """Verify AJAX submission respects show_correctness settings."""
- block = CapaFactory.create(show_correctness=show_correctness, due=self.tomorrow_str, correct=is_correct)
-
- # Simulate marking the input correct/incorrect
- with patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct") as mock_is_correct:
- mock_is_correct.return_value = is_correct
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "0"}
- json_result = block.handle_ajax("problem_check", get_request_dict)
- result = json.loads(json_result)
-
- # Expect that the AJAX result withholds correctness and score
- assert result["current_score"] == expected_score
- assert result["success"] == expected_success
-
- # Expect that the number of attempts is incremented by 1
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
-
- def test_reset_problem(self):
- """Ensure resetting a completed problem regenerates state and HTML."""
- block = CapaFactory.create(done=True)
- block.new_lcp = Mock(wraps=block.new_lcp)
- block.choose_new_seed = Mock(wraps=block.choose_new_seed)
-
- # Stub out HTML rendering
- with patch("xmodule.capa_block._BuiltInProblemBlock.get_problem_html") as mock_html:
- mock_html.return_value = "Test HTML
"
-
- # Reset the problem
- get_request_dict = {}
- result = block.reset_problem(get_request_dict)
-
- # Expect that the request was successful
- assert ("success" in result) and result["success"] # noqa: PT018
-
- # Expect that the problem HTML is retrieved
- assert "html" in result
- assert result["html"] == "Test HTML
"
-
- # Expect that the problem was reset
- block.new_lcp.assert_called_once_with(None)
-
- def test_reset_problem_closed(self):
- """Verify reset is blocked when the problem is closed."""
- # pre studio default
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS)
-
- # Simulate that the problem is closed
- with patch("xmodule.capa_block._BuiltInProblemBlock.closed") as mock_closed:
- mock_closed.return_value = True
-
- # Try to reset the problem
- get_request_dict = {}
- result = block.reset_problem(get_request_dict)
-
- # Expect that the problem was NOT reset
- assert ("success" in result) and (not result["success"]) # noqa: PT018
-
- def test_reset_problem_not_done(self):
- """Verify reset is blocked when the problem is not yet completed."""
- # Simulate that the problem is NOT done
- block = CapaFactory.create(done=False)
-
- # Try to reset the problem
- get_request_dict = {}
- result = block.reset_problem(get_request_dict)
-
- # Expect that the problem was NOT reset
- assert ("success" in result) and (not result["success"]) # noqa: PT018
-
- def test_rescore_problem_correct(self):
- """Ensure rescoring marks the problem correct without incrementing attempts."""
-
- block = CapaFactory.create(attempts=0, done=True)
-
- # Simulate that all answers are marked correct, no matter
- # what the input is, by patching LoncapaResponse.evaluate_answers()
- with patch(
- "xblocks_contrib.problem.capa.responsetypes.LoncapaResponse.evaluate_answers"
- ) as mock_evaluate_answers:
- mock_evaluate_answers.return_value = CorrectMap(
- answer_id=CapaFactory.answer_key(),
- correctness="correct",
- npoints=1,
- )
- with patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct") as mock_is_correct:
- mock_is_correct.return_value = True
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "1"}
- block.submit_problem(get_request_dict)
- block.rescore(only_if_higher=False)
-
- # Expect that the problem is marked correct
- assert block.is_correct() is True
-
- # Expect that the number of attempts is not incremented
- assert block.attempts == 1
- # and that this was considered attempt number 1 for grading purposes
- assert block.lcp.context["attempt"] == 1
-
- def test_rescore_problem_additional_correct(self):
- """Verify rescoring updates scores correctly when new correct answers are added."""
- # make sure it also works when new correct answer has been added
- block = CapaFactory.create(attempts=0)
- answer_id = CapaFactory.answer_key()
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "1"}
- result = block.submit_problem(get_request_dict)
-
- # Expect that the problem is marked incorrect and user didn't earn score
- assert result["success"] == "incorrect"
- assert block.get_score() == (0, 1)
- assert block.correct_map[answer_id]["correctness"] == "incorrect"
-
- # Expect that the number of attempts has incremented to 1
- assert block.attempts == 1
- assert block.lcp.context["attempt"] == 1
-
- # Simulate that after making an incorrect answer to the correct answer
- # the new calculated score is (1,1)
- # by patching CorrectMap.is_correct() and NumericalResponse.get_staff_ans()
- # In case of rescore with only_if_higher=True it should update score of block
- # if previous score was lower
-
- with patch("xblocks_contrib.problem.capa.correctmap.CorrectMap.is_correct") as mock_is_correct:
- mock_is_correct.return_value = True
- block.set_score(block.score_from_lcp(block.lcp))
- with patch("xblocks_contrib.problem.capa.responsetypes.NumericalResponse.get_staff_ans") as get_staff_ans:
- get_staff_ans.return_value = 1 + 0j
- block.rescore(only_if_higher=True)
-
- # Expect that the problem is marked correct and user earned the score
- assert block.get_score() == (1, 1)
- assert block.correct_map[answer_id]["correctness"] == "correct"
- # Expect that the number of attempts is not incremented
- assert block.attempts == 1
- # and hence that this was still considered the first attempt for grading purposes
- assert block.lcp.context["attempt"] == 1
-
- def test_rescore_problem_incorrect(self):
- """Ensure rescoring marks the problem incorrect without changing attempts."""
- # make sure it also works when attempts have been reset,
- # so add this to the test:
- block = CapaFactory.create(attempts=0, done=True)
-
- # Simulate that all answers are marked incorrect, no matter
- # what the input is, by patching LoncapaResponse.evaluate_answers()
- with patch(
- "xblocks_contrib.problem.capa.responsetypes.LoncapaResponse.evaluate_answers"
- ) as mock_evaluate_answers:
- mock_evaluate_answers.return_value = CorrectMap(CapaFactory.answer_key(), "incorrect")
- block.rescore(only_if_higher=False)
-
- # Expect that the problem is marked incorrect
- assert block.is_correct() is False
-
- # Expect that the number of attempts is not incremented
- assert block.attempts == 0
- # and that this is treated as the first attempt for grading purposes
- assert block.lcp.context["attempt"] == 1
-
- def test_rescore_problem_with_grading_method_disable(self):
- """
- Test the rescore method with grading method logic enabled by default.
- """
- block = CapaFactory.create(attempts=0, done=True, grading_method="highest_score")
-
- block.rescore(only_if_higher=False)
-
- assert block.attempts == 0
- assert block.lcp.context["attempt"] == 1
-
- def test_rescore_problem_with_grading_method_enable(self):
- """
- Test the rescore method with grading method enabled.
- In this case, the rescore method should call `get_rescore_with_grading_method` method.
- """
- block = CapaFactory.create(attempts=0, done=True)
-
- with patch.object(
- ProblemBlock, "get_rescore_with_grading_method", wraps=block.get_rescore_with_grading_method
- ) as mock_get_rescore:
-
- block.rescore(only_if_higher=False)
-
- assert block.attempts == 0
- assert block.lcp.context["attempt"] == 1
- mock_get_rescore.assert_called()
-
- @patch("xmodule.capa_block._BuiltInProblemBlock.publish_grade")
- def test_rescore_problem_grading_method_always_enabled(self, mock_publish_grade: Mock):
- """
- Test the rescore method when grading method is always enabled by default.
-
- The final score is calculated according to the grading method, as grading
- is now always enabled.
- """
- block = CapaFactory.create(attempts=0, max_attempts=3)
-
- get_request_dict = {CapaFactory.input_key(): "3.21"}
- block.submit_problem(get_request_dict)
-
- get_request_dict = {CapaFactory.input_key(): "3.45"}
- block.submit_problem(get_request_dict)
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- # Score is calculated according to the grading method
- assert block.score == Score(raw_earned=1, raw_possible=1)
- block.rescore(only_if_higher=False)
-
- # Still Score is the last score
- mock_publish_grade.assert_called_with(score=Score(raw_earned=1, raw_possible=1), only_if_higher=False)
-
- # Rescore with different grading methods
- block.grading_method = "first_score"
- block.rescore(only_if_higher=False)
- mock_publish_grade.assert_called_with(score=Score(raw_earned=0, raw_possible=1), only_if_higher=False)
-
- # Change grading method to 'highest_score'
- block.grading_method = "highest_score"
- block.rescore(only_if_higher=False)
- mock_publish_grade.assert_called_with(score=Score(raw_earned=1, raw_possible=1), only_if_higher=False)
-
- # Change grading method to 'average_score'
- block.grading_method = "average_score"
- block.rescore(only_if_higher=False)
-
- mock_publish_grade.assert_called_with(score=Score(raw_earned=0.33, raw_possible=1), only_if_higher=False)
-
- @patch("xmodule.capa_block._BuiltInProblemBlock.publish_grade")
- def test_rescore_problem_grading_method_always_enabled_with_various_methods(self, mock_publish_grade: Mock):
- """
- Test the rescore method when grading method is always enabled by default
- with different grading methods.
-
- The final score is calculated according to the grading method, as grading
- is now always enabled.
- """
- block = CapaFactory.create(attempts=0, max_attempts=3)
-
- get_request_dict = {CapaFactory.input_key(): "3.21"}
- block.submit_problem(get_request_dict)
-
- get_request_dict = {CapaFactory.input_key(): "3.45"}
- block.submit_problem(get_request_dict)
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- # Grading method is 'last_score' by default
- assert block.grading_method == "last_score"
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Change grading method to 'first_score'
- block.grading_method = "first_score"
- block.rescore(only_if_higher=False)
- mock_publish_grade.assert_called_with(score=Score(raw_earned=0, raw_possible=1), only_if_higher=False)
-
- # Change grading method to 'highest_score'
- block.grading_method = "highest_score"
- block.rescore(only_if_higher=False)
- mock_publish_grade.assert_called_with(score=Score(raw_earned=1, raw_possible=1), only_if_higher=False)
-
- # Change grading method to 'average_score'
- block.grading_method = "average_score"
- block.rescore(only_if_higher=False)
- mock_publish_grade.assert_called_with(score=Score(raw_earned=0.33, raw_possible=1), only_if_higher=False)
-
- block.rescore(only_if_higher=False)
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- @patch("xmodule.capa_block._BuiltInProblemBlock.publish_grade")
- def test_rescore_problem_update_grading_method(self, mock_publish_grade: Mock):
- """
- Test the rescore method when the grading method is updated.
-
- When the grading method is updated, the final
- score is the score based on the new grading method.
- """
- block = CapaFactory.create(attempts=0, max_attempts=3)
-
- get_request_dict = {CapaFactory.input_key(): "3.21"}
- block.submit_problem(get_request_dict)
-
- get_request_dict = {CapaFactory.input_key(): "3.45"}
- block.submit_problem(get_request_dict)
-
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- # Grading method is 'last_score'
- assert block.grading_method == "last_score"
- assert block.score == Score(raw_earned=1, raw_possible=1)
-
- # Change grading method to 'first_score'
- block.grading_method = "first_score"
- block.rescore(only_if_higher=False)
-
- mock_publish_grade.assert_called_with(score=Score(raw_earned=0, raw_possible=1), only_if_higher=False)
-
- # Change grading method to 'highest_score'
- block.grading_method = "highest_score"
- block.rescore(only_if_higher=False)
-
- mock_publish_grade.assert_called_with(score=Score(raw_earned=1, raw_possible=1), only_if_higher=False)
-
- # Change grading method to 'average_score'
- block.grading_method = "average_score"
- block.rescore(only_if_higher=False)
-
- mock_publish_grade.assert_called_with(score=Score(raw_earned=0.33, raw_possible=1), only_if_higher=False)
-
- def test_rescore_problem_not_done(self):
- """Ensure rescoring an unfinished problem raises NotFoundError."""
- # Simulate that the problem is NOT done
- block = CapaFactory.create(done=False)
-
- # Try to rescore the problem, and get exception
- with pytest.raises(NotFoundError):
- block.rescore(only_if_higher=False)
-
- def test_rescore_problem_not_supported(self):
- """Ensure rescoring raises NotImplementedError when unsupported by the problem."""
- block = CapaFactory.create(done=True)
-
- # Try to rescore the problem, and get exception
- with patch(
- "xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.supports_rescoring"
- ) as mock_supports_rescoring:
- mock_supports_rescoring.return_value = False
- with pytest.raises(NotImplementedError):
- block.rescore(only_if_higher=False)
-
- def test_calculate_score_list(self):
- """
- Test that the `calculate_score_list` method returns the correct list of scores.
- """
- block = CapaFactory.create(correct=True)
- correct_map = CorrectMap(answer_id="1_2_1", correctness="correct", npoints=1)
- block.lcp.correct_map_history = [correct_map, correct_map]
-
- with patch.object(block.lcp, "calculate_score", return_value={"score": 1, "total": 2}):
- result = block.calculate_score_list()
- expected_result = [Score(raw_earned=1, raw_possible=2), Score(raw_earned=1, raw_possible=2)]
- self.assertEqual(result, expected_result) # noqa: PT009
-
- def test_calculate_score_list_empty(self):
- """
- Test that the `calculate_score_list` method returns an
- empty list when the `correct_map_history` is empty.
-
- The `calculate_score` method should not be called.
- """
- block = CapaFactory.create(correct=True)
- block.lcp.correct_map_history = []
-
- with patch.object(block.lcp, "calculate_score", return_value=Mock()):
- result = block.calculate_score_list()
- self.assertEqual(result, []) # noqa: PT009
- block.lcp.calculate_score.assert_not_called()
-
- def test_update_correctness_list_updates_attempt(self):
- """
- Test that the `update_correctness_list` method updates the attempt number.
- """
- block = CapaFactory.create(correct=True, attempts=0)
-
- block.update_correctness_list()
-
- self.assertEqual(block.lcp.context["attempt"], 1) # noqa: PT009
-
- def test_update_correctness_list_with_history(self):
- """
- Test that the `update_correctness_list` method updates the correct map history.
- """
- block = CapaFactory.create(correct=True, attempts=2)
- correct_map = CorrectMap(answer_id="1_2_1", correctness="correct", npoints=1)
- student_answers = {"1_2_1": "abcd"}
- block.correct_map_history = [correct_map]
- block.student_answers_history = [student_answers]
-
- with patch.object(block.lcp, "get_grade_from_current_answers", return_value=correct_map):
- block.update_correctness_list()
- self.assertEqual(block.lcp.context["attempt"], 2) # noqa: PT009
- block.lcp.get_grade_from_current_answers.assert_called_once_with(student_answers, correct_map)
- self.assertEqual(block.lcp.correct_map_history, [correct_map]) # noqa: PT009
- self.assertEqual(block.lcp.correct_map.get_dict(), correct_map.get_dict()) # noqa: PT009
-
- def test_update_correctness_list_without_history(self):
- """
- Test that the `update_correctness_list` method does not
- update the correct map history because the history is empty.
-
- The `get_grade_from_current_answers` method should not be called.
- """
- block = CapaFactory.create(correct=True, attempts=1)
- block.correct_map_history = []
- block.student_answers_history = []
-
- with patch.object(block.lcp, "get_grade_from_current_answers", return_value=Mock()):
- block.update_correctness_list()
- self.assertEqual(block.lcp.context["attempt"], 1) # noqa: PT009
- block.lcp.get_grade_from_current_answers.assert_not_called()
-
- def test_get_rescore_with_grading_method(self):
- """
- Test that the `get_rescore_with_grading_method` method returns the correct score.
- """
- block = CapaFactory.create(done=True, attempts=0, max_attempts=2)
- get_request_dict = {CapaFactory.input_key(): "3.21"}
- block.submit_problem(get_request_dict)
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- result = block.get_rescore_with_grading_method()
-
- self.assertEqual(result, Score(raw_earned=1, raw_possible=1)) # noqa: PT009
-
- def test_get_score_with_grading_method(self):
- """
- Test that the `get_score_with_grading_method` method
- returns the correct score based on the grading method.
- """
- block = CapaFactory.create(done=True, attempts=0, max_attempts=2)
- get_request_dict = {CapaFactory.input_key(): "3.21"}
- block.submit_problem(get_request_dict)
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
- expected_score = Score(raw_earned=1, raw_possible=1)
-
- score = block.get_score_with_grading_method(block.score_from_lcp(block.lcp))
-
- self.assertEqual(score, expected_score) # noqa: PT009
- self.assertEqual(block.score, expected_score) # noqa: PT009
-
- @patch("xmodule.capa_block._BuiltInProblemBlock.score_from_lcp")
- def test_get_score_with_grading_method_updates_score(self, mock_score_from_lcp: Mock):
- """
- Test that the `get_score_with_grading_method` method returns the correct score.
-
- Check that the score is returned with the correct score and the score
- history is updated including that score.
- """
- block = CapaFactory.create(attempts=1)
- current_score = Score(raw_earned=1, raw_possible=1)
- mock_score_from_lcp.return_value = current_score
-
- score = block.get_score_with_grading_method(current_score)
-
- self.assertEqual(score, current_score) # noqa: PT009
- self.assertEqual(block.score_history, [current_score]) # noqa: PT009
-
- def test_get_score_with_grading_method_calls_grading_method_handler(self):
- """
- Test that the `get_score_with_grading_method` method calls
- the grading method handler with the appropriate arguments.
- """
- block = CapaFactory.create(attempts=1)
- current_score = Score(raw_earned=0, raw_possible=1)
-
- with patch("xmodule.capa_block.GradingMethodHandler") as mock_handler:
- mock_handler.return_value.get_score.return_value = current_score
- block.get_score_with_grading_method(current_score)
- mock_handler.assert_called_once_with(
- Score(raw_earned=0, raw_possible=1),
- "last_score",
- block.score_history,
- current_score.raw_possible,
- )
-
- def capa_factory_for_problem_xml(self, xml):
- """Return a custom CapaFactory configured with the given problem XML."""
-
- class CustomCapaFactory(CapaFactory):
- """
- A factory for creating a Capa problem with arbitrary xml.
- """
-
- sample_problem_xml = textwrap.dedent(xml)
-
- return CustomCapaFactory
-
- def test_codejail_error_upon_problem_creation(self):
- """Verify codejail execution errors during problem creation raise LoncapaProblemError."""
- # Simulate a codejail safe_exec failure upon problem creation.
- # Create a problem with some script attached.
- xml_str = textwrap.dedent(
- """
-
-
-
- """
- )
- factory = self.capa_factory_for_problem_xml(xml_str)
-
- # When codejail safe_exec fails upon problem creation, a LoncapaProblemError should be raised.
- with pytest.raises(LoncapaProblemError): # noqa: PT012
- with patch("xblocks_contrib.problem.capa.capa_problem.safe_exec") as mock_safe_exec:
- mock_safe_exec.side_effect = SafeExecException()
- factory.create()
-
- def _rescore_problem_error_helper(self, exception_class):
- """Helper to allow testing all errors that rescoring might return."""
- # Create the block
- block = CapaFactory.create(attempts=0)
- CapaFactory.answer_key()
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "1"}
- block.submit_problem(get_request_dict)
-
- # Simulate answering a problem that raises the exception
- with patch(
- "xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.get_grade_from_current_answers"
- ) as mock_rescore:
- mock_rescore.side_effect = exception_class("test error \u03a9")
- with pytest.raises(exception_class):
- block.rescore(only_if_higher=False)
-
- # Expect that the number of attempts is NOT incremented
- assert block.attempts == 1
- # and that this was considered the first attempt for grading purposes
- assert block.lcp.context["attempt"] == 1
-
- def test_rescore_problem_student_input_error(self):
- """Ensure StudentInputError during rescore is handled correctly."""
- self._rescore_problem_error_helper(StudentInputError)
-
- def test_rescore_problem_problem_error(self):
- """Ensure LoncapaProblemError during rescore is handled correctly."""
- self._rescore_problem_error_helper(LoncapaProblemError)
-
- def test_rescore_problem_response_error(self):
- """Ensure ResponseError during rescore is handled correctly."""
- self._rescore_problem_error_helper(ResponseError)
-
- def test_save_problem(self):
- """Verify saving a problem persists answers and returns success."""
- block = CapaFactory.create(done=False)
-
- # Save the problem
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.save_problem(get_request_dict)
-
- # Expect that answers are saved to the problem
- expected_answers = {CapaFactory.answer_key(): "3.14"}
- assert block.lcp.student_answers == expected_answers
-
- # Expect that the result is success
- assert ("success" in result) and result["success"] # noqa: PT018
-
- def test_save_problem_closed(self):
- """Ensure saving a closed problem fails."""
- block = CapaFactory.create(done=False)
-
- # Simulate that the problem is closed
- with patch("xmodule.capa_block._BuiltInProblemBlock.closed") as mock_closed:
- mock_closed.return_value = True
-
- # Try to save the problem
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.save_problem(get_request_dict)
-
- # Expect that the result is failure
- assert ("success" in result) and (not result["success"]) # noqa: PT018
-
- @ddt.data(RANDOMIZATION.ALWAYS, "true")
- def test_save_problem_submitted_with_randomize(self, rerandomize):
- """Verify saving fails when problem is submitted and rerandomization is enabled."""
- # Capa XModule treats 'always' and 'true' equivalently
- block = CapaFactory.create(rerandomize=rerandomize, done=True)
-
- # Try to save
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.save_problem(get_request_dict)
-
- # Expect that we cannot save
- assert ("success" in result) and (not result["success"]) # noqa: PT018
-
- @ddt.data(RANDOMIZATION.NEVER, "false", RANDOMIZATION.PER_STUDENT)
- def test_save_problem_submitted_no_randomize(self, rerandomize):
- """Verify saving succeeds when problem is submitted without rerandomization."""
- # Capa XBlock treats 'false' and 'per_student' equivalently
- block = CapaFactory.create(rerandomize=rerandomize, done=True)
-
- # Try to save
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- result = block.save_problem(get_request_dict)
-
- # Expect that we succeed
- assert ("success" in result) and result["success"] # noqa: PT018
-
- def test_submit_button_name(self):
- """Verify the submit button label is correct."""
- block = CapaFactory.create(attempts=0)
- assert block.submit_button_name() == "Submit"
-
- def test_submit_button_submitting_name(self):
- """Verify the submitting button label is correct."""
- block = CapaFactory.create(attempts=1, max_attempts=10)
- assert block.submit_button_submitting_name() == "Submitting"
-
- def test_should_enable_submit_button(self):
- """Verify submit button enablement logic across deadlines, attempts, and states."""
-
- attempts = random.randint(1, 10)
-
- # If we're after the deadline, disable the submit button
- block = CapaFactory.create(due=self.yesterday_str)
- assert not block.should_enable_submit_button()
-
- # If user is out of attempts, disable the submit button
- block = CapaFactory.create(attempts=attempts, max_attempts=attempts)
- assert not block.should_enable_submit_button()
-
- # If survey question (max_attempts = 0), disable the submit button
- block = CapaFactory.create(max_attempts=0)
- assert not block.should_enable_submit_button()
-
- # If user submitted a problem but hasn't reset,
- # disable the submit button
- # Note: we can only reset when rerandomize="always" or "true"
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
- assert not block.should_enable_submit_button()
-
- block = CapaFactory.create(rerandomize="true", done=True)
- assert not block.should_enable_submit_button()
-
- # Otherwise, enable the submit button
- block = CapaFactory.create()
- assert block.should_enable_submit_button()
-
- # If the user has submitted the problem
- # and we do NOT have a reset button, then we can enable the submit button
- # Setting rerandomize to "never" or "false" ensures that the reset button
- # is not shown
- block = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, done=True)
- assert block.should_enable_submit_button()
-
- block = CapaFactory.create(rerandomize="false", done=True)
- assert block.should_enable_submit_button()
-
- block = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, done=True)
- assert block.should_enable_submit_button()
-
- def test_should_show_reset_button(self):
- """Verify reset button visibility logic across problem states and settings."""
-
- attempts = random.randint(1, 10)
-
- # If we're after the deadline, do NOT show the reset button
- block = CapaFactory.create(due=self.yesterday_str, done=True)
- assert not block.should_show_reset_button()
-
- # If the user is out of attempts, do NOT show the reset button
- block = CapaFactory.create(attempts=attempts, max_attempts=attempts, done=True)
- assert not block.should_show_reset_button()
-
- # pre studio default value, DO show the reset button
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
- assert block.should_show_reset_button()
-
- # If survey question for capa (max_attempts = 0),
- # DO show the reset button
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True)
- assert block.should_show_reset_button()
-
- # If the question is not correct
- # DO show the reset button
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=False)
- assert block.should_show_reset_button()
-
- # If the question is correct and randomization is never
- # DO not show the reset button
- block = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=0, done=True, correct=True)
- assert not block.should_show_reset_button()
-
- # If the question is correct and randomization is always
- # Show the reset button
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, max_attempts=0, done=True, correct=True)
- assert block.should_show_reset_button()
-
- # Don't show reset button if randomization is turned on and the question is not done
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=False)
- assert not block.should_show_reset_button()
-
- # Show reset button if randomization is turned on and the problem is done
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, show_reset_button=False, done=True)
- assert block.should_show_reset_button()
-
- def test_should_show_save_button(self):
- """Verify save button visibility logic across attempts, deadlines, and randomization."""
-
- attempts = random.randint(1, 10)
-
- # If we're after the deadline, do NOT show the save button
- block = CapaFactory.create(due=self.yesterday_str, done=True)
- assert not block.should_show_save_button()
-
- # If the user is out of attempts, do NOT show the save button
- block = CapaFactory.create(attempts=attempts, max_attempts=attempts, done=True)
- assert not block.should_show_save_button()
-
- # If user submitted a problem but hasn't reset, do NOT show the save button
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=True)
- assert not block.should_show_save_button()
-
- block = CapaFactory.create(rerandomize="true", done=True)
- assert not block.should_show_save_button()
-
- # If the user has unlimited attempts and we are not randomizing,
- # then do NOT show a save button
- # because they can keep using "Check"
- block = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.NEVER, done=False)
- assert not block.should_show_save_button()
-
- block = CapaFactory.create(max_attempts=None, rerandomize="false", done=True)
- assert not block.should_show_save_button()
-
- block = CapaFactory.create(max_attempts=None, rerandomize=RANDOMIZATION.PER_STUDENT, done=True)
- assert not block.should_show_save_button()
-
- # pre-studio default, DO show the save button
- block = CapaFactory.create(rerandomize=RANDOMIZATION.ALWAYS, done=False)
- assert block.should_show_save_button()
-
- # If we're not randomizing and we have limited attempts, then we can save
- block = CapaFactory.create(rerandomize=RANDOMIZATION.NEVER, max_attempts=2, done=True)
- assert block.should_show_save_button()
-
- block = CapaFactory.create(rerandomize="false", max_attempts=2, done=True)
- assert block.should_show_save_button()
-
- block = CapaFactory.create(rerandomize=RANDOMIZATION.PER_STUDENT, max_attempts=2, done=True)
- assert block.should_show_save_button()
-
- # If survey question for capa (max_attempts = 0),
- # DO show the save button
- block = CapaFactory.create(max_attempts=0, done=False)
- assert block.should_show_save_button()
-
- def test_should_show_save_button_force_save_button(self):
- """Verify force_save_button overrides normal save button visibility rules."""
- # If we're after the deadline, do NOT show the save button
- # even though we're forcing a save
- block = CapaFactory.create(due=self.yesterday_str, force_save_button="true", done=True)
- assert not block.should_show_save_button()
-
- # If the user is out of attempts, do NOT show the save button
- attempts = random.randint(1, 10)
- block = CapaFactory.create(attempts=attempts, max_attempts=attempts, force_save_button="true", done=True)
- assert not block.should_show_save_button()
-
- # Otherwise, if we force the save button,
- # then show it even if we would ordinarily
- # require a reset first
- block = CapaFactory.create(force_save_button="true", rerandomize=RANDOMIZATION.ALWAYS, done=True)
- assert block.should_show_save_button()
-
- block = CapaFactory.create(force_save_button="true", rerandomize="true", done=True)
- assert block.should_show_save_button()
-
- def test_no_max_attempts(self):
- """Ensure problems with empty max_attempts render without errors."""
- block = CapaFactory.create(max_attempts="")
- html = block.get_problem_html()
- assert html is not None
- # assert that we got here without exploding
-
- @patch("xmodule.capa_block.render_to_string")
- def test_get_problem_html(self, render_template):
- """Verify problem HTML rendering uses correct template context and encapsulation."""
- render_template.return_value = "Test Template HTML
"
- block = CapaFactory.create()
-
- # We've tested the show/hide button logic in other tests,
- # so here we hard-wire the values
- enable_submit_button = bool(random.randint(0, 1) % 2)
- show_reset_button = bool(random.randint(0, 1) % 2)
- show_save_button = bool(random.randint(0, 1) % 2)
-
- block.should_enable_submit_button = Mock(return_value=enable_submit_button)
- block.should_show_reset_button = Mock(return_value=show_reset_button)
- block.should_show_save_button = Mock(return_value=show_save_button)
-
- # Patch the capa problem's HTML rendering
- with patch("xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.get_html") as mock_html:
- mock_html.return_value = "Test Problem HTML
"
-
- # Render the problem HTML
- html = block.get_problem_html(encapsulate=False)
-
- # Also render the problem encapsulated in a
- html_encapsulated = block.get_problem_html(encapsulate=True)
-
- # Expect that we get the rendered template back
- assert html == "
Test Template HTML
"
-
- # Check the rendering context
- render_args, _ = render_template.call_args
- assert len(render_args) == 2
-
- template_name = render_args[0]
- assert template_name == "problem.html"
-
- context = render_args[1]
- assert context["problem"]["html"] == "
Test Problem HTML
"
- assert bool(context["should_enable_submit_button"]) == enable_submit_button
- assert bool(context["reset_button"]) == show_reset_button
- assert bool(context["save_button"]) == show_save_button
- assert not context["demand_hint_possible"]
-
- # Assert that the encapsulated html contains the original html
- assert html in html_encapsulated
-
- demand_xml = """
-
- That is the question
-
-
- Alpha A hint
-
- Beta
-
-
-
- Demand 1
- Demand 2
-
- """
-
- @patch("xmodule.capa_block.render_to_string")
- def test_demand_hint(self, render_template):
- """Verify image-based demand hints render correctly without static URL issues."""
- # HTML generation is mocked out to be meaningless here, so instead we check
- # the context dict passed into HTML generation.
- render_template.return_value = "
Test Template HTML
"
- block = CapaFactory.create(xml=self.demand_xml)
- block.get_problem_html() # ignoring html result
- context = render_template.call_args[0][1]
- assert context["demand_hint_possible"]
- assert context["should_enable_next_hint"]
-
- # Check the AJAX call that gets the hint by index
- result = block.get_demand_hint(0)
- assert result["hint_index"] == 0
- assert result["should_enable_next_hint"]
-
- result = block.get_demand_hint(1)
- assert result["hint_index"] == 1
- assert not result["should_enable_next_hint"]
-
- result = block.get_demand_hint(2) # here the server wraps around to index 0
- assert result["hint_index"] == 0
- assert result["should_enable_next_hint"]
-
- @patch("xmodule.capa_block.render_to_string")
- def test_single_demand_hint(self, render_template):
- """
- Test the hint button enabled state when there is just a single hint.
- """
- test_xml = """
-
- That is the question
-
-
- Alpha A hint
-
- Beta
-
-
-
- Only demand hint
-
- """
- render_template.return_value = "
Test Template HTML
"
- block = CapaFactory.create(xml=test_xml)
- block.get_problem_html() # ignoring html result
- context = render_template.call_args[0][1]
- assert context["demand_hint_possible"]
- assert context["should_enable_next_hint"]
-
- # Check the AJAX call that gets the hint by index
- result = block.get_demand_hint(0)
- assert result["hint_index"] == 0
- assert not result["should_enable_next_hint"]
-
- @patch("xmodule.capa_block.render_to_string")
- def test_image_hint(self, render_template):
- """
- Test the hint button shows an image without the static url.
- """
- test_xml = """
-
- That is the question
-
-
- Alpha A hint
-
- Beta
-
-
-
-
-
- You can add an optional hint like this. Problems that have a hint include a hint button, and this text appears the first time learners select the button.
-
- """
- render_template.return_value = "
Test Template HTML
"
- block = CapaFactory.create(xml=test_xml)
- block.get_problem_html() # ignoring html result
- context = render_template.call_args[0][1]
- assert context["demand_hint_possible"]
- assert context["should_enable_next_hint"]
-
- # Check the AJAX call that gets the hint by index
- result = block.get_demand_hint(0)
- assert result["hint_index"] == 0
- assert not result["should_enable_next_hint"]
-
- def test_demand_hint_logging(self):
- """
- Test calling get_demand_hunt() results in an event being published.
- """
- block = CapaFactory.create(xml=self.demand_xml)
- with patch.object(block.runtime, "publish") as mock_publish:
- block.get_problem_html()
- block.get_demand_hint(0)
- mock_publish.assert_called_with(
- block,
- "edx.problem.hint.demandhint_displayed",
- {"hint_index": 0, "module_id": str(block.location), "hint_text": "Demand 1", "hint_len": 2},
- )
-
- def test_input_state_consistency(self):
- """Verify input_state keys remain consistent and isolated across block instances."""
- block1 = CapaFactory.create()
- block2 = CapaFactory.create()
-
- # check to make sure that the input_state and the keys have the same values
- block1.set_state_from_lcp()
- assert list(block1.lcp.inputs.keys()) == list(block1.input_state.keys())
-
- block2.set_state_from_lcp()
-
- intersection = set(block2.input_state.keys()).intersection(set(block1.input_state.keys()))
- assert len(intersection) == 0
-
- @patch("xmodule.capa_block.render_to_string")
- def test_get_problem_html_error(self, render_template):
- """
- In production, when an error occurs with the problem HTML
- rendering, a "dummy" problem is created with an error
- message to display to the user.
- """
- render_template.return_value = "
Test Template HTML
"
- block = CapaFactory.create()
-
- # Save the original problem so we can compare it later
- original_problem = block.lcp
-
- # Simulate throwing an exception when the capa problem
- # is asked to render itself as HTML
- block.lcp.get_html = Mock(side_effect=Exception("Test"))
-
- # Try to render the block with DEBUG turned off
- html = block.get_problem_html()
-
- assert html is not None
-
- # Check the rendering context
- render_args, _ = render_template.call_args
- context = render_args[1]
- assert "error" in context["problem"]["html"]
-
- # Expect that the block has created a new dummy problem with the error
- assert original_problem != block.lcp
-
- @patch("xmodule.capa_block.render_to_string")
- def test_get_problem_html_error_preview(self, render_template):
- """
- Test the html response when an error occurs with DEBUG off in Studio.
- """
- render_template.return_value = "
Test Template HTML
"
- block = CapaFactory.create()
-
- # Simulate throwing an exception when the capa problem
- # is asked to render itself as HTML
- error_msg = "Superterrible error happened: ☠"
- block.lcp.get_html = Mock(side_effect=Exception(error_msg))
-
- block.runtime.is_author_mode = True
-
- # Try to render the block with the author mode turned on
- html = block.get_problem_html()
-
- assert html is not None
-
- # Check the rendering context
- render_args, _ = render_template.call_args
- context = render_args[1]
- assert error_msg in context["problem"]["html"]
-
- @override_settings(DEBUG=True)
- @patch("xmodule.capa_block.render_to_string")
- def test_get_problem_html_error_w_debug(self, render_template):
- """
- Test the html response when an error occurs with DEBUG on
- """
- render_template.return_value = "
Test Template HTML
"
- block = CapaFactory.create()
- block.runtime.is_author_mode = True
-
- # Simulate throwing an exception when the capa problem
- # is asked to render itself as HTML
- error_msg = "Superterrible error happened: ☠"
- block.lcp.get_html = Mock(side_effect=Exception(error_msg))
-
- # Try to render the block with DEBUG turned on
- html = block.get_problem_html()
-
- assert html is not None
-
- # Check the rendering context
- render_args, _ = render_template.call_args
- context = render_args[1]
- assert error_msg in context["problem"]["html"]
-
- @ddt.data(
- "false", "true", RANDOMIZATION.NEVER, RANDOMIZATION.PER_STUDENT, RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET
- )
- def test_random_seed_no_change(self, rerandomize):
- """Verify problem seed remains stable when rerandomization does not apply."""
-
- # Run the test for each possible rerandomize value
-
- block = CapaFactory.create(rerandomize=rerandomize)
-
- # Get the seed
- # By this point, the block should have persisted the seed
- seed = block.seed
- assert seed is not None
-
- # If we're not rerandomizing, the seed is always set
- # to the same value (1)
- if rerandomize == RANDOMIZATION.NEVER:
- assert seed == 1, f"Seed should always be 1 when rerandomize='{rerandomize}'"
-
- # Check the problem
- get_request_dict = {CapaFactory.input_key(): "3.14"}
- block.submit_problem(get_request_dict)
-
- # Expect that the seed is the same
- assert seed == block.seed
-
- # Save the problem
- block.save_problem(get_request_dict)
-
- # Expect that the seed is the same
- assert seed == block.seed
-
- @ddt.data(
- "false", "true", RANDOMIZATION.NEVER, RANDOMIZATION.PER_STUDENT, RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET
- )
- def test_random_seed_with_reset(self, rerandomize):
- """
- Run the test for each possible rerandomize value
- """
-
- def _reset_and_get_seed(block):
- """
- Reset the XBlock and return the block's seed
- """
-
- # Simulate submitting an attempt
- # We need to do this, or reset_problem() will
- # fail because it won't re-randomize until the problem has been submitted
- # the problem yet.
- block.done = True
-
- # Reset the problem
- block.reset_problem({})
-
- # Return the seed
- return block.seed
-
- def _retry_and_check(num_tries, test_func):
- """
- Returns True if *test_func* was successful
- (returned True) within *num_tries* attempts
-
- *test_func* must be a function
- of the form test_func() -> bool
- """
- success = False
- for __ in range(num_tries):
- if test_func() is True:
- success = True
- break
- return success
-
- block = CapaFactory.create(rerandomize=rerandomize, done=True)
-
- # Get the seed
- # By this point, the block should have persisted the seed
- seed = block.seed
- assert seed is not None
-
- # We do NOT want the seed to reset if rerandomize
- # is set to 'never' -- it should still be 1
- # The seed also stays the same if we're randomizing
- # 'per_student': the same student should see the same problem
- if rerandomize in [RANDOMIZATION.NEVER, "false", RANDOMIZATION.PER_STUDENT]:
- assert seed == _reset_and_get_seed(block)
-
- # Otherwise, we expect the seed to change
- # to another valid seed
- else:
-
- # Since there's a small chance (expected) we might get the
- # same seed again, give it 60 chances
- # to generate a different seed
- success = _retry_and_check(60, lambda: _reset_and_get_seed(block) != seed)
-
- assert block.seed is not None
- msg = "Could not get a new seed from reset after 60 tries"
- assert success, msg
-
- @ddt.data(
- "false", "true", RANDOMIZATION.NEVER, RANDOMIZATION.PER_STUDENT, RANDOMIZATION.ALWAYS, RANDOMIZATION.ONRESET
- )
- def test_random_seed_with_reset_question_unsubmitted(self, rerandomize):
- """
- Run the test for each possible rerandomize value
- """
-
- def _reset_and_get_seed(block):
- """
- Reset the XBlock and return the block's seed
- """
-
- # Reset the problem
- # By default, the problem is instantiated as unsubmitted
- block.reset_problem({})
-
- # Return the seed
- return block.seed
-
- block = CapaFactory.create(rerandomize=rerandomize, done=False)
-
- # Get the seed
- # By this point, the block should have persisted the seed
- seed = block.seed
- assert seed is not None
-
- # the seed should never change because the student hasn't finished the problem
- assert seed == _reset_and_get_seed(block)
-
- @ddt.data(RANDOMIZATION.ALWAYS, RANDOMIZATION.PER_STUDENT, "true", RANDOMIZATION.ONRESET)
- def test_random_seed_bins(self, rerandomize):
- """Ensure generated random seeds fall within the expected numeric range."""
- # Assert that we are limiting the number of possible seeds.
- # Get a bunch of seeds, they should all be in 0-999.
- i = 200
- while i > 0:
- block = CapaFactory.create(rerandomize=rerandomize)
- assert 0 <= block.seed < 1000
- i -= 1
-
- @patch("xmodule.capa_block.log")
- @patch("xmodule.capa_block.Progress")
- def test_get_progress_error(self, mock_progress, mock_log):
- """
- Check that an exception given in `Progress` produces a `log.exception` call.
- """
- error_types = [TypeError, ValueError]
- for error_type in error_types:
- mock_progress.side_effect = error_type
- block = CapaFactory.create()
- assert block.get_progress() is None
- mock_log.exception.assert_called_once_with("Got bad progress")
- mock_log.reset_mock()
-
- @patch("xmodule.capa_block.Progress")
- def test_get_progress_no_error_if_weight_zero(self, mock_progress):
- """
- Check that if the weight is 0 get_progress does not try to create a Progress object.
- """
- mock_progress.return_value = True
- block = CapaFactory.create()
- block.weight = 0
- progress = block.get_progress()
- assert progress is None
- assert not mock_progress.called
-
- @patch("xmodule.capa_block.Progress")
- def test_get_progress_calculate_progress_fraction(self, mock_progress):
- """
- Check that score and total are calculated correctly for the progress fraction.
- """
- block = CapaFactory.create()
- block.weight = 1
- block.get_progress()
- mock_progress.assert_called_with(0, 1)
-
- other_block = CapaFactory.create(correct=True)
- other_block.weight = 1
- other_block.get_progress()
- mock_progress.assert_called_with(1, 1)
-
- @ddt.data(
- ("never", True, None),
- ("never", False, None),
- ("past_due", True, None),
- ("past_due", False, None),
- ("always", True, 1),
- ("always", False, 0),
- )
- @ddt.unpack
- def test_get_display_progress_show_correctness(self, show_correctness, is_correct, expected_score):
- """
- Check that score and total are calculated correctly for the progress fraction.
- """
- block = CapaFactory.create(correct=is_correct, show_correctness=show_correctness, due=self.tomorrow_str)
- block.weight = 1
- score, total = block.get_display_progress()
- assert score == expected_score
- assert total == 1
-
- def test_get_html(self):
- """
- Check that get_html() calls get_progress() with no arguments.
- """
- block = CapaFactory.create()
- block.get_progress = Mock(wraps=block.get_progress)
- block.get_html()
- block.get_progress.assert_called_with()
-
- def test_get_problem(self):
- """
- Check that get_problem() returns the expected dictionary.
- """
- block = CapaFactory.create()
- assert block.get_problem("data") == {"html": block.get_problem_html(encapsulate=False)}
-
- # Standard question with shuffle="true" used by a few tests
- common_shuffle_xml = textwrap.dedent(
- """
-
-
-
- Apple
- Banana
- Chocolate
- Donut
-
-
-
- """
- )
-
- def test_check_unmask(self):
- """
- Check that shuffle unmasking is plumbed through: when submit_problem is called,
- unmasked names should appear in the publish event_info.
- """
- block = CapaFactory.create(xml=self.common_shuffle_xml)
- with patch.object(block.runtime, "publish") as mock_publish:
- get_request_dict = {CapaFactory.input_key(): "choice_3"} # the correct choice
- block.submit_problem(get_request_dict)
- mock_call = mock_publish.mock_calls[1]
- event_info = mock_call[1][2]
- assert event_info["answers"][CapaFactory.answer_key()] == "choice_3"
- # 'permutation' key added to record how problem was shown
- assert event_info["permutation"][CapaFactory.answer_key()] == (
- "shuffle",
- ["choice_3", "choice_1", "choice_2", "choice_0"],
- )
- assert event_info["success"] == "correct"
-
- def test_check_unmask_answerpool(self):
- """Check answer-pool question publish uses unmasked names"""
- xml = textwrap.dedent(
- """
-
-
-
- Apple
- Banana
- Chocolate
- Donut
-
-
-
- """
- )
- block = CapaFactory.create(xml=xml)
- with patch.object(block.runtime, "publish") as mock_publish:
- get_request_dict = {CapaFactory.input_key(): "choice_2"} # mask_X form when masking enabled
- block.submit_problem(get_request_dict)
- mock_call = mock_publish.mock_calls[1]
- event_info = mock_call[1][2]
- assert event_info["answers"][CapaFactory.answer_key()] == "choice_2"
- # 'permutation' key added to record how problem was shown
- assert event_info["permutation"][CapaFactory.answer_key()] == (
- "answerpool",
- ["choice_1", "choice_3", "choice_2", "choice_0"],
- )
- assert event_info["success"] == "incorrect"
-
- @ddt.unpack
- @ddt.data(
- {"display_name": None, "expected_display_name": "problem"},
- {"display_name": "", "expected_display_name": "problem"},
- {"display_name": " ", "expected_display_name": "problem"},
- {"display_name": "CAPA 101", "expected_display_name": "CAPA 101"},
- )
- def test_problem_display_name_with_default(self, display_name, expected_display_name):
- """
- Verify that display_name_with_default works as expected.
- """
- block = CapaFactory.create(display_name=display_name)
- assert block.display_name_with_default == expected_display_name
-
- @ddt.data(
- "",
- " ",
- )
- @patch("xmodule.capa_block.render_to_string")
- def test_problem_no_display_name(self, display_name, render_template):
- """
- Verify that if problem display name is not provided then a default name is used.
- """
- render_template.return_value = "
Test Template HTML
"
- block = CapaFactory.create(display_name=display_name)
- block.get_problem_html()
- render_args, _ = render_template.call_args
- context = render_args[1]
- assert context["problem"]["name"] == block.location.block_type
-
-
-@ddt.ddt
-@pytest.mark.django_db
-class ProblemBlockXMLTest(unittest.TestCase):
- """Tests XML strings for various problem types in XBlocks."""
-
- sample_checkbox_problem_xml = textwrap.dedent(
- """
-
- Title
-
- Description
-
- Example
-
- The following languages are in the Indo-European family:
-
-
- Urdu
- Finnish
- Marathi
- French
- Hungarian
-
-
-
- Note: Make sure you select all of the correct options—there may be more than one!
-
-
-
-
Explanation
-
-
Solution for CAPA problem
-
-
-
-
-
- """
- )
-
- sample_dropdown_problem_xml = textwrap.dedent(
- """
-
- Dropdown problems allow learners to select only one option from a list of options.
-
- Description
-
- You can use the following example problem as a model.
-
- Which of the following countries celebrates its independence on August 15?
-
-
-
-
-
-
-
-
-
Explanation
-
-
India became an independent nation on August 15, 1947.
-
-
-
-
-
- """
- )
-
- sample_multichoice_problem_xml = textwrap.dedent(
- """
-
- Multiple choice problems allow learners to select only one option.
-
- When you add the problem, be sure to select Settings to specify a Display Name and other values.
-
- You can use the following example problem as a model.
-
- Which of the following countries has the largest population?
-
-
- Brazil
- timely feedback -- explain why an almost correct answer is wrong
-
- Germany
- Indonesia
- Russia
-
-
-
-
-
-
Explanation
-
-
According to September 2014 estimates:
-
The population of Indonesia is approximately 250 million.
-
The population of Brazil is approximately 200 million.
-
The population of Russia is approximately 146 million.
-
The population of Germany is approximately 81 million.
-
-
-
-
-
- """
- )
-
- sample_numerical_input_problem_xml = textwrap.dedent(
- """
-
- In a numerical input problem, learners enter numbers or a specific and relatively simple mathematical
- expression. Learners enter the response in plain text, and the system then converts the text to a symbolic
- expression that learners can see below the response field.
-
- The system can handle several types of characters, including basic operators, fractions, exponents, and
- common constants such as "i". You can refer learners to "Entering Mathematical and Scientific Expressions"
- in the edX Guide for Students for more information.
-
- When you add the problem, be sure to select Settings to specify a Display Name and other values that
- apply.
-
- You can use the following example problems as models.
-
- How many miles away from Earth is the sun? Use scientific notation to answer.
-
-
-
-
-
- The square of what number is -100?
-
-
-
-
-
-
-
-
Explanation
-
-
The sun is 93,000,000, or 9.3*10^7, miles away from Earth.
-
-100 is the square of 10 times the imaginary number, i.
-
-
-
-
-
- """
- )
-
- sample_text_input_problem_xml = textwrap.dedent(
- """
-
- In text input problems, also known as "fill-in-the-blank" problems, learners enter text into a response
- field. The text can include letters and characters such as punctuation marks. The text that the learner
- enters must match your specified answer text exactly. You can specify more than one correct answer.
- Learners must enter a response that matches one of the correct answers exactly.
-
- When you add the problem, be sure to select Settings to specify a Display Name and other values that
- apply.
-
- You can use the following example problem as a model.
-
- What was the first post-secondary school in China to allow both male and female students?
-
-
-
-
-
-
-
-
-
-
Explanation
-
-
Nanjing Higher Normal Institute first admitted female students in 1920.
-
-
-
-
-
- """
- )
-
- sample_checkboxes_with_hints_and_feedback_problem_xml = textwrap.dedent(
- """
-
- You can provide feedback for each option in a checkbox problem, with distinct feedback depending on
- whether or not the learner selects that option.
-
- You can also provide compound feedback for a specific combination of answers. For example, if you have
- three possible answers in the problem, you can configure specific feedback for when a learner selects each
- combination of possible answers.
-
- You can also add hints for learners.
-
- Be sure to select Settings to specify a Display Name and other values that apply.
-
- Use the following example problem as a model.
-
- Which of the following is a fruit? Check all that apply.
-
-
- apple
- You are correct that an apple is a fruit because it is the fertilized
- ovary that comes from an apple tree and contains seeds.
- Remember that an apple is also a fruit.
- pumpkin
- You are correct that a pumpkin is a fruit because it is the fertilized
- ovary of a squash plant and contains seeds.
- Remember that a pumpkin is also a fruit.
- potato
- A potato is a vegetable, not a fruit, because it does not come from a
- flower and does not contain seeds.
- You are correct that a potato is a vegetable because it is an edible
- part of a plant in tuber form.
- tomato
- You are correct that a tomato is a fruit because it is the fertilized
- ovary of a tomato plant and contains seeds.
- Many people mistakenly think a tomato is a vegetable. However, because
- a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.
-
- An apple, pumpkin, and tomato are all fruits as they all are fertilized
- ovaries of a plant and contain seeds.
- You are correct that an apple, pumpkin, and tomato are all fruits as they
- all are fertilized ovaries of a plant and contain seeds. However, a potato is not a fruit as it is an
- edible part of a plant in tuber form and is a vegetable.
-
-
-
-
-
- A fruit is the fertilized ovary from a flower.
- A fruit contains seeds of the plant.
-
-
- """
- )
-
- sample_dropdown_with_hints_and_feedback_problem_xml = textwrap.dedent(
- """
-
- You can provide feedback for each available option in a dropdown problem.
-
- You can also add hints for learners.
-
- Be sure to select Settings to specify a Display Name and other values that apply.
-
- Use the following example problem as a model.
-
- A/an ________ is a vegetable.
-
-
- apple An apple is the fertilized ovary that comes from an apple
- tree and contains seeds, meaning it is a fruit.
- pumpkin A pumpkin is the fertilized ovary of a squash plant and
- contains seeds, meaning it is a fruit.
- potato A potato is an edible part of a plant in tuber form and is a
- vegetable.
- tomato Many people mistakenly think a tomato is a vegetable.
- However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.
-
-
-
-
-
- A fruit is the fertilized ovary from a flower.
- A fruit contains seeds of the plant.
-
-
- """
- )
-
- sample_multichoice_with_hints_and_feedback_problem_xml = textwrap.dedent(
- """
-
- You can provide feedback for each option in a multiple choice problem.
-
- You can also add hints for learners.
-
- Be sure to select Settings to specify a Display Name and other values that apply.
-
- Use the following example problem as a model.
-
- Which of the following is a vegetable?
-
-
- apple An apple is the fertilized ovary that comes from an apple
- tree and contains seeds, meaning it is a fruit.
- pumpkin A pumpkin is the fertilized ovary of a squash plant and
- contains seeds, meaning it is a fruit.
- potato A potato is an edible part of a plant in tuber form and is a
- vegetable.
- tomato Many people mistakenly think a tomato is a vegetable.
- However, because a tomato is the fertilized ovary of a tomato plant and contains seeds, it is a fruit.
-
-
-
-
-
-
- A fruit is the fertilized ovary from a flower.
- A fruit contains seeds of the plant.
-
-
- """
- )
-
- sample_numerical_input_with_hints_and_feedback_problem_xml = textwrap.dedent(
- """
-
- You can provide feedback for correct answers in numerical input problems. You cannot provide feedback
- for incorrect answers.
-
- Use feedback for the correct answer to reinforce the process for arriving at the numerical value.
-
- You can also add hints for learners.
-
- Be sure to select Settings to specify a Display Name and other values that apply.
-
- Use the following example problem as a model.
-
- What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)
-
-
-
- The mean for this set of numbers is 20 / 5, which equals 4.
-
-
-
-
Explanation
-
-
The mean is calculated by summing the set of numbers and dividing by n. In this case:
- (1 + 5 + 6 + 3 + 5) / 5 = 20 / 5 = 4.
-
-
-
-
-
- The mean is calculated by summing the set of numbers and dividing by n.
- n is the count of items in the set.
-
-
- """
- )
-
- sample_text_input_with_hints_and_feedback_problem_xml = textwrap.dedent(
- """
-
- You can provide feedback for the correct answer in text input problems, as well as for specific
- incorrect answers.
-
- Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on
- how to arrive at the correct answer.
-
- Be sure to select Settings to specify a Display Name and other values that apply.
-
- Use the following example problem as a model.
-
- Which U.S. state has the largest land area?
-
-
- Alaska is 576,400 square miles, more than double the land area of the second largest state,
- Texas.
- While many people think Texas is the largest state, it is actually the
- second largest, with 261,797 square miles.
- California is the third largest state, with 155,959 square miles.
-
-
-
-
-
- Consider the square miles, not population.
- Consider all 50 states, not just the continental United States.
-
-
- """
- )
-
- def _create_block(self, xml, name=None):
- """Creates a ProblemBlock to run test against"""
- block = CapaFactory.create()
- block.data = xml
- if name:
- block.display_name = name
- return block
-
- @ddt.data(*sorted(responsetypes.registry.registered_tags()))
- def test_all_response_types(self, response_tag):
- """Tests that every registered response tag is correctly returned"""
- xml = "
<{response_tag}>{response_tag}> ".format(response_tag=response_tag) # noqa: UP032
- name = "Some Capa Problem"
- block = self._create_block(xml, name=name)
- assert block.problem_types == {response_tag}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": [response_tag],
- "content": {"display_name": name, "capa_content": ""},
- }
-
- def test_response_types_ignores_non_response_tags(self):
- """Ensure non-response XML tags are ignored when determining problem response types."""
- xml = textwrap.dedent(
- """
-
- Label
- Some comment
-
-
- Apple
- Banana
- Chocolate
- Donut
-
-
-
- """
- )
- name = "Test Capa Problem"
- block = self._create_block(xml, name=name)
- assert block.problem_types == {"multiplechoiceresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["multiplechoiceresponse"],
- "content": {"display_name": name, "capa_content": "Label Some comment Apple Banana Chocolate Donut"},
- }
-
- def test_response_types_multiple_tags(self):
- """Verify indexing behavior when multiple response types are present in a single problem."""
- xml = textwrap.dedent(
- """
-
- Label
- Some comment
-
-
- Donut
-
-
-
-
- Buggy
-
-
-
-
-
-
- """
- )
- name = "Other Test Capa Problem"
- block = self._create_block(xml, name=name)
- assert block.problem_types == {"multiplechoiceresponse", "optionresponse"}
-
- # We are converting problem_types to a set to compare it later without taking into account the order
- # the reasoning behind is that the problem_types (property) is represented by dict and when it is converted
- # to list its ordering is different everytime.
-
- indexing_result = block.index_dictionary()
- indexing_result["problem_types"] = set(indexing_result["problem_types"])
- self.assertDictEqual( # noqa: PT009
- indexing_result,
- {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": {"optionresponse", "multiplechoiceresponse"},
- "content": {"display_name": name, "capa_content": "Label Some comment Donut Buggy '1','2'"},
- },
- )
-
- def test_solutions_not_indexed(self):
- """Confirm that solutions, scripts, styles, answers, and hints are excluded from indexing."""
- xml = textwrap.dedent(
- """
-
- Test solution.
- Test solution with attribute.
-
- Test solutionset.
- Test solution within solutionset.
-
-
- Test feedback.
- Test feedback with attribute.
-
- Test FeedbackSet.
- Test feedback within feedbackset.
-
-
- Test answer.
- Test answer with attribute.
-
-
-
-
-
-
-
- Test choicehint.
- Test hint.
- Test hintpart.
-
- """
- )
- name = "Blank Common Capa Problem"
- block = self._create_block(xml, name=name)
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": [],
- "content": {"display_name": name, "capa_content": ""},
- }
-
- def test_indexing_checkboxes(self):
- """Verify correct indexing of checkbox-based problems and extracted content."""
- name = "Checkboxes"
- block = self._create_block(self.sample_checkbox_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- Title
- Description
- Example
- The following languages are in the Indo-European family:
- Urdu
- Finnish
- Marathi
- French
- Hungarian
- Note: Make sure you select all of the correct options—there may be more than one!
- """
- )
- assert block.problem_types == {"choiceresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["choiceresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_dropdown(self):
- """Verify correct indexing of dropdown-based problems and extracted content."""
- name = "Dropdown"
- block = self._create_block(self.sample_dropdown_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- Dropdown problems allow learners to select only one option from a list of options.
- Description
- You can use the following example problem as a model.
- Which of the following countries celebrates its independence on August 15? 'India','Spain','China','Bermuda'
- """
- )
- assert block.problem_types == {"optionresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["optionresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_multiple_choice(self):
- """Verify correct indexing of multiple-choice problems and extracted content."""
- name = "Multiple Choice"
- block = self._create_block(self.sample_multichoice_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- Multiple choice problems allow learners to select only one option.
- When you add the problem, be sure to select Settings to specify a Display Name and other values.
- You can use the following example problem as a model.
- Which of the following countries has the largest population?
- Brazil
- Germany
- Indonesia
- Russia
- """
- )
- assert block.problem_types == {"multiplechoiceresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["multiplechoiceresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_numerical_input(self):
- """Verify correct indexing of numerical input problems and extracted content."""
- name = "Numerical Input"
- block = self._create_block(self.sample_numerical_input_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- In a numerical input problem, learners enter numbers or a specific and relatively simple mathematical
- expression. Learners enter the response in plain text, and the system then converts the text to a symbolic
- expression that learners can see below the response field.
- The system can handle several types of characters, including basic operators, fractions, exponents, and
- common constants such as "i". You can refer learners to "Entering Mathematical and Scientific Expressions"
- in the edX Guide for Students for more information.
- When you add the problem, be sure to select Settings to specify a Display Name and other values that
- apply.
- You can use the following example problems as models.
- How many miles away from Earth is the sun? Use scientific notation to answer.
- The square of what number is -100?
- """
- )
- assert block.problem_types == {"numericalresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["numericalresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_text_input(self):
- """Verify correct indexing of text input problems and extracted content."""
- name = "Text Input"
- block = self._create_block(self.sample_text_input_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- In text input problems, also known as "fill-in-the-blank" problems, learners enter text into a response
- field. The text can include letters and characters such as punctuation marks. The text that the learner
- enters must match your specified answer text exactly. You can specify more than one correct answer.
- Learners must enter a response that matches one of the correct answers exactly.
- When you add the problem, be sure to select Settings to specify a Display Name and other values that
- apply.
- You can use the following example problem as a model.
- What was the first post-secondary school in China to allow both male and female students?
- """
- )
- assert block.problem_types == {"stringresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["stringresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_non_latin_problem(self):
- """Ensure non-Latin characters are preserved correctly in indexed problem content."""
- sample_text_input_problem_xml = textwrap.dedent(
- """
-
-
- Δοκιμή με μεταβλητές με Ελληνικούς χαρακτήρες μέσα σε python: $FX1_VAL
-
- """
- )
- name = "Non latin Input"
- block = self._create_block(sample_text_input_problem_xml, name=name)
- capa_content = "Δοκιμή με μεταβλητές με Ελληνικούς χαρακτήρες μέσα σε python: $FX1_VAL"
-
- block_dict = block.index_dictionary()
- assert block_dict["content"]["capa_content"] == smart_str(capa_content)
-
- def test_indexing_checkboxes_with_hints_and_feedback(self):
- """Verify indexing of checkbox problems containing hints and feedback."""
- name = "Checkboxes with Hints and Feedback"
- block = self._create_block(self.sample_checkboxes_with_hints_and_feedback_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- You can provide feedback for each option in a checkbox problem, with distinct feedback depending on
- whether or not the learner selects that option.
- You can also provide compound feedback for a specific combination of answers. For example, if you have
- three possible answers in the problem, you can configure specific feedback for when a learner selects each
- combination of possible answers.
- You can also add hints for learners.
- Be sure to select Settings to specify a Display Name and other values that apply.
- Use the following example problem as a model.
- Which of the following is a fruit? Check all that apply.
- apple
- pumpkin
- potato
- tomato
- """
- )
- assert block.problem_types == {"choiceresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["choiceresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_dropdown_with_hints_and_feedback(self):
- """Verify indexing of dropdown problems containing hints and feedback."""
- name = "Dropdown with Hints and Feedback"
- block = self._create_block(self.sample_dropdown_with_hints_and_feedback_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- You can provide feedback for each available option in a dropdown problem.
- You can also add hints for learners.
- Be sure to select Settings to specify a Display Name and other values that apply.
- Use the following example problem as a model.
- A/an ________ is a vegetable.
- apple
- pumpkin
- potato
- tomato
- """
- )
- assert block.problem_types == {"optionresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["optionresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_multiple_choice_with_hints_and_feedback(self):
- """Verify indexing of multiple-choice problems containing hints and feedback."""
- name = "Multiple Choice with Hints and Feedback"
- block = self._create_block(self.sample_multichoice_with_hints_and_feedback_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- You can provide feedback for each option in a multiple choice problem.
- You can also add hints for learners.
- Be sure to select Settings to specify a Display Name and other values that apply.
- Use the following example problem as a model.
- Which of the following is a vegetable?
- apple
- pumpkin
- potato
- tomato
- """
- )
- assert block.problem_types == {"multiplechoiceresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["multiplechoiceresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_numerical_input_with_hints_and_feedback(self):
- """Verify indexing of numerical input problems containing hints and feedback."""
- name = "Numerical Input with Hints and Feedback"
- block = self._create_block(self.sample_numerical_input_with_hints_and_feedback_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- You can provide feedback for correct answers in numerical input problems. You cannot provide feedback
- for incorrect answers.
- Use feedback for the correct answer to reinforce the process for arriving at the numerical value.
- You can also add hints for learners.
- Be sure to select Settings to specify a Display Name and other values that apply.
- Use the following example problem as a model.
- What is the arithmetic mean for the following set of numbers? (1, 5, 6, 3, 5)
- """
- )
- assert block.problem_types == {"numericalresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["numericalresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_text_input_with_hints_and_feedback(self):
- """Verify indexing of text input problems containing hints and feedback."""
- name = "Text Input with Hints and Feedback"
- block = self._create_block(self.sample_text_input_with_hints_and_feedback_problem_xml, name=name)
- capa_content = textwrap.dedent(
- """
- You can provide feedback for the correct answer in text input problems, as well as for specific
- incorrect answers.
- Use feedback on expected incorrect answers to address common misconceptions and to provide guidance on
- how to arrive at the correct answer.
- Be sure to select Settings to specify a Display Name and other values that apply.
- Use the following example problem as a model.
- Which U.S. state has the largest land area?
- """
- )
- assert block.problem_types == {"stringresponse"}
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["stringresponse"],
- "content": {"display_name": name, "capa_content": capa_content.replace("\n", " ").strip()},
- }
-
- def test_indexing_problem_with_html_tags(self):
- """Ensure HTML tags, comments, scripts, and styles are safely ignored during indexing."""
- sample_problem_xml = textwrap.dedent(
- """
-
-
-
- This has HTML comment in it.
-
-
- HTML end.
-
-
-
- """
- )
- name = "Mixed business"
- block = self._create_block(sample_problem_xml, name=name)
- capa_content = "This has HTML comment in it. HTML end."
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": [],
- "content": {"display_name": name, "capa_content": capa_content},
- }
-
- def test_indexing_problem_with_no_whitespace_between_tags(self):
- """
- The new (MFE) visual editor for capa problems renders the OLX without spaces between the tags.
- We want to make sure the index description is still readable and has whitespace.
- """
- sample_problem_xml = (
- '
'
- "Question text here.
"
- 'Option A
'
- 'Option B
'
- " "
- " "
- )
- name = "No spaces"
- block = self._create_block(sample_problem_xml, name=name)
- capa_content = "Question text here. Option A Option B"
- assert block.index_dictionary() == {
- "content_type": ProblemBlock.INDEX_CONTENT_TYPE,
- "problem_types": ["choiceresponse"],
- "content": {"display_name": name, "capa_content": capa_content},
- }
-
- def test_invalid_xml_handling(self):
- """
- Tests to confirm that invalid XML throws errors during xblock creation,
- so as not to allow bad data into modulestore.
- """
- sample_invalid_xml = textwrap.dedent(
- """
-
-
-
- You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown
- problems. Edit this component to replace this template with your own assessment.
- Add the question text, or prompt, here. This text is required.
- You can add an optional tip or note related to the prompt like this.
-
- an incorrect answer
- the correct answer
- an incorrect answer
-
-
-
- """
- )
- with pytest.raises(Exception): # noqa: B017, PT011
- CapaFactory.create(xml=problem_xml)
-
-
-class ComplexEncoderTest(unittest.TestCase):
- """Tests JSON encoding of complex numbers."""
-
- def test_default(self):
- """
- Check that complex numbers can be encoded into JSON.
- """
- complex_num = 1 - 1j
- expected_str = "1-1*j"
- json_str = json.dumps(complex_num, cls=ComplexEncoder)
- assert expected_str == json_str[1:(-1)]
- # ignore quotes
-
-
-@skip_unless_lms
-@UseUnsafeCodejail()
-@pytest.mark.django_db
-class ProblemCheckTrackingTest(unittest.TestCase):
- """
- Ensure correct tracking information is included in events emitted during problem checks.
- """
-
- def setUp(self):
- super().setUp()
- self.maxDiff = None # pylint: disable=invalid-name
-
- def test_choice_answer_text(self):
- """Verify tracked submission data for multiple choice, option, and checkbox responses."""
- xml = """\
-
-
- What color is the open ocean on a sunny day?
-
-
-
-
- Which piece of furniture is built for sitting?
-
- a table
- a desk
- a chair
- a bookshelf
-
-
-
-
- Which of the following are musical instruments?
-
- a piano
- a tree
- a guitar
- a window
-
-
-
- """
-
- # Whitespace screws up comparisons
- xml = "".join(line.strip() for line in xml.split("\n"))
- factory = self.capa_factory_for_problem_xml(xml)
- block = factory.create()
-
- answer_input_dict = {
- factory.input_key(2): "blue",
- factory.input_key(3): "choice_0",
- factory.input_key(4): ["choice_0", "choice_1"],
- }
- event = self.get_event_for_answers(block, answer_input_dict)
-
- assert event["submission"] == {
- factory.answer_key(2): {
- "question": "What color is the open ocean on a sunny day?",
- "answer": "blue",
- "response_type": "optionresponse",
- "input_type": "optioninput",
- "correct": True,
- "group_label": "",
- "variant": "",
- },
- factory.answer_key(3): {
- "question": "Which piece of furniture is built for sitting?",
- "answer": "
a table ",
- "response_type": "multiplechoiceresponse",
- "input_type": "choicegroup",
- "correct": False,
- "group_label": "",
- "variant": "",
- },
- factory.answer_key(4): {
- "question": "Which of the following are musical instruments?",
- "answer": ["a piano", "a tree"],
- "response_type": "choiceresponse",
- "input_type": "checkboxgroup",
- "correct": False,
- "group_label": "",
- "variant": "",
- },
- }
-
- def capa_factory_for_problem_xml(self, xml):
- """Create a custom CapaFactory for a given problem XML string."""
-
- class CustomCapaFactory(CapaFactory):
- """
- A factory for creating a Capa problem with arbitrary xml.
- """
-
- sample_problem_xml = textwrap.dedent(xml)
-
- return CustomCapaFactory
-
- def get_event_for_answers(self, block, answer_input_dict):
- """Submit answers and return the emitted tracking event payload."""
- with patch.object(block.runtime, "publish") as mock_publish:
- block.submit_problem(answer_input_dict)
-
- assert len(mock_publish.mock_calls) >= 2
- # There are potentially 2 track logs: answers and hint. [-1]=answers.
- mock_call = mock_publish.mock_calls[-1]
- event = mock_call[1][2]
-
- return event
-
- def test_numerical_textline(self):
- """Verify tracking data for numerical textline responses."""
- factory = CapaFactory
- block = factory.create()
-
- answer_input_dict = {factory.input_key(2): "3.14"}
-
- event = self.get_event_for_answers(block, answer_input_dict)
- assert event["submission"] == {
- factory.answer_key(2): {
- "question": "",
- "answer": "3.14",
- "response_type": "numericalresponse",
- "input_type": "textline",
- "correct": True,
- "group_label": "",
- "variant": "",
- }
- }
-
- def test_multiple_inputs(self):
- """Verify tracking data for multiple inputs within a single response group."""
- group_label = "Choose the correct color"
- input1_label = "What color is the sky?"
- input2_label = "What color are pine needles?"
- factory = self.capa_factory_for_problem_xml(
- f"""\
-
-
- {group_label}
-
-
-
-
- """
- )
- block = factory.create()
- answer_input_dict = {
- factory.input_key(2, 1): "blue",
- factory.input_key(2, 2): "yellow",
- }
-
- event = self.get_event_for_answers(block, answer_input_dict)
- assert event["submission"] == {
- factory.answer_key(2, 1): {
- "group_label": group_label,
- "question": input1_label,
- "answer": "blue",
- "response_type": "optionresponse",
- "input_type": "optioninput",
- "correct": True,
- "variant": "",
- },
- factory.answer_key(2, 2): {
- "group_label": group_label,
- "question": input2_label,
- "answer": "yellow",
- "response_type": "optionresponse",
- "input_type": "optioninput",
- "correct": False,
- "variant": "",
- },
- }
-
- def test_optioninput_extended_xml(self):
- """Test the new XML form of writing with
tag instead of options= attribute."""
- group_label = "Are you the Gatekeeper?"
- input1_label = "input 1 label"
- input2_label = "input 2 label"
- factory = self.capa_factory_for_problem_xml(
- f"""\
-
-
- {group_label}
-
-
- apple
-
- banana
-
-
-
- cucumber
-
- donut
-
-
-
-
-
-
- apple
-
- banana
-
-
-
- cucumber
-
- donut
-
-
-
-
-
- """
- )
- block = factory.create()
-
- answer_input_dict = {
- factory.input_key(2, 1): "apple",
- factory.input_key(2, 2): "cucumber",
- }
-
- event = self.get_event_for_answers(block, answer_input_dict)
- assert event["submission"] == {
- factory.answer_key(2, 1): {
- "group_label": group_label,
- "question": input1_label,
- "answer": "apple",
- "response_type": "optionresponse",
- "input_type": "optioninput",
- "correct": True,
- "variant": "",
- },
- factory.answer_key(2, 2): {
- "group_label": group_label,
- "question": input2_label,
- "answer": "cucumber",
- "response_type": "optionresponse",
- "input_type": "optioninput",
- "correct": False,
- "variant": "",
- },
- }
-
- def test_rerandomized_inputs(self):
- """Ensure variant seed is included in tracking data for rerandomized problems."""
- factory = CapaFactory
- block = factory.create(rerandomize=RANDOMIZATION.ALWAYS)
-
- answer_input_dict = {factory.input_key(2): "3.14"}
-
- event = self.get_event_for_answers(block, answer_input_dict)
- assert event["submission"] == {
- factory.answer_key(2): {
- "question": "",
- "answer": "3.14",
- "response_type": "numericalresponse",
- "input_type": "textline",
- "correct": True,
- "group_label": "",
- "variant": block.seed,
- }
- }
-
- @pytest.mark.django_db
- @patch.object(XQueueInterface, "_http_post")
- def test_file_inputs(self, mock_xqueue_post):
- """Verify tracking data for file submission and custom response inputs."""
- fnames = ["prog1.py", "prog2.py", "prog3.py"]
- fpaths = [os.path.join(DATA_DIR, "capa", fname) for fname in fnames]
- fileobjs = []
- for fpath in fpaths:
- with open(fpath, encoding="utf-8") as f:
- fileobjs.append(f.read())
-
- factory = CapaFactoryWithFiles
- block = factory.create()
-
- # Mock the XQueueInterface post method
- mock_xqueue_post.return_value = (0, "ok")
-
- answer_input_dict = {
- CapaFactoryWithFiles.input_key(response_num=2): fileobjs,
- CapaFactoryWithFiles.input_key(response_num=3): "None",
- }
-
- event = self.get_event_for_answers(block, answer_input_dict)
- assert event["submission"] == {
- factory.answer_key(2): {
- "question": "",
- "answer": fileobjs,
- "response_type": "coderesponse",
- "input_type": "filesubmission",
- "correct": False,
- "group_label": "",
- "variant": "",
- },
- factory.answer_key(3): {
- "answer": "None",
- "correct": True,
- "group_label": "",
- "question": "",
- "response_type": "customresponse",
- "input_type": "textline",
- "variant": "",
- },
- }
-
- def test_get_answer_with_jump_to_id_urls(self):
- """
- Make sure replace_jump_to_id_urls() is called in get_answer.
- """
- problem_xml = textwrap.dedent(
- """
-
- What is 1+4?
-
-
-
-
-
-
-
-
- """
- )
-
- data = {}
- problem = CapaFactory.create(showanswer="always", xml=problem_xml)
- problem.runtime.service(problem, "replace_urls").replace_urls = Mock()
-
- problem.get_answer(data)
- assert problem.runtime.service(problem, "replace_urls").replace_urls.called
-
-
-class ProblemBlockReportGenerationTest(unittest.TestCase):
- """
- Ensure that Capa report generation works correctly
- """
-
- def setUp(self):
- self.find_question_label_patcher = patch(
- "xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.find_question_label",
- lambda self, answer_id: answer_id,
- )
- self.find_answer_text_patcher = patch(
- "xblocks_contrib.problem.capa.capa_problem.LoncapaProblem.find_answer_text",
- lambda self, answer_id, current_answer: current_answer,
- )
- self.find_question_label_patcher.start()
- self.find_answer_text_patcher.start()
- self.addCleanup(self.find_question_label_patcher.stop)
- self.addCleanup(self.find_answer_text_patcher.stop)
-
- def _mock_user_state_generator(self, user_count=1, response_count=10):
- for uid in range(user_count):
- yield self._user_state(username=f"user{uid}", response_count=response_count)
-
- def _user_state(self, username="testuser", response_count=10, suffix=""):
- return XBlockUserState(
- username=username,
- state={
- "student_answers": {
- f"{username}_answerid_{aid}{suffix}": f"{username}_answer_{aid}" for aid in range(response_count)
- },
- "seed": 1,
- "correct_map": {},
- },
- block_key=None,
- updated=None,
- scope=None,
- )
-
- def _get_block(self):
- """Create and return a mock ProblemBlock with default test data."""
- scope_ids = Mock(block_type="problem")
- block = ProblemBlock(get_test_system(), scope_ids=scope_ids)
- block.runtime = Mock()
- block.data = " "
- return block
-
- def test_generate_report_data_not_implemented(self):
- """Verify report generation is not supported for non-problem blocks."""
- scope_ids = Mock(block_type="noproblem")
- block = ProblemBlock(get_test_system(), scope_ids=scope_ids)
- with pytest.raises(NotImplementedError):
- next(block.generate_report_data(iter([])))
-
- def test_generate_report_data_limit_responses(self):
- """Ensure report generation respects the response limit."""
- block = self._get_block()
- report_data = list(block.generate_report_data(self._mock_user_state_generator(), 2))
- assert 2 == len(report_data)
-
- def test_generate_report_data_dont_limit_responses(self):
- """Verify all responses are included when no limit is provided."""
- block = self._get_block()
- user_count = 5
- response_count = 10
- report_data = list(
- block.generate_report_data(
- self._mock_user_state_generator(
- user_count=user_count,
- response_count=response_count,
- )
- )
- )
- assert (user_count * response_count) == len(report_data)
-
- def test_generate_report_data_skip_dynamath(self):
- """Ensure Dynamath responses are excluded from reports."""
- block = self._get_block()
- iterator = iter([self._user_state(suffix="_dynamath")])
- report_data = list(block.generate_report_data(iterator))
- assert 0 == len(report_data)
-
- def test_generate_report_data_report_loncapa_error(self):
- """Verify LonCapa errors are captured and reported instead of aborting."""
- # Test to make sure reports continue despite loncappa errors, and write them into the report.
- block = self._get_block()
- with patch("xmodule.capa_block.LoncapaProblem") as mock_loncapa_problem:
- mock_loncapa_problem.side_effect = LoncapaProblemError
- report_data = list(
- block.generate_report_data(
- self._mock_user_state_generator(
- user_count=1,
- response_count=5,
- )
- )
- )
- assert "Python Error: No Answer Retrieved" in list(report_data[0][1].values())
diff --git a/xmodule/tests/test_delay_between_attempts.py b/xmodule/tests/test_delay_between_attempts.py
index bc2d074df91b..6e4a0c23e486 100644
--- a/xmodule/tests/test_delay_between_attempts.py
+++ b/xmodule/tests/test_delay_between_attempts.py
@@ -20,8 +20,7 @@
from xblock.field_data import DictFieldData
from xblock.fields import ScopeIds
from xblock.scorable import Score
-
-from xmodule.capa_block import ProblemBlock
+from xblocks_contrib.problem import ProblemBlock
from . import get_test_system
diff --git a/xmodule/tests/test_item_bank.py b/xmodule/tests/test_item_bank.py
index 2063381670d7..9e778fc6a781 100644
--- a/xmodule/tests/test_item_bank.py
+++ b/xmodule/tests/test_item_bank.py
@@ -10,10 +10,10 @@
from rest_framework import status
from web_fragments.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime
+from xblocks_contrib.problem import ProblemBlock
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangolib.testing.utils import skip_unless_cms, skip_unless_lms
-from xmodule.capa_block import ProblemBlock
from xmodule.modulestore.tests.factories import CourseFactory
from xmodule.modulestore.tests.utils import MixedSplitTestCase
from xmodule.tests import prepare_block_runtime
@@ -171,7 +171,7 @@ def test_max_count_validation(self):
'xmodule.modulestore.split_mongo.runtime.SplitModuleStoreRuntime.render',
VanillaRuntime.render,
)
- @patch('xmodule.capa_block.ProblemBlock.author_view', dummy_render, create=True)
+ @patch('xblocks_contrib.problem.ProblemBlock.author_view', dummy_render, create=True)
@patch('xmodule.x_module.ModuleStoreRuntime.applicable_aside_types', lambda self, block: [])
def test_preview_view(self):
""" Test preview view rendering """
@@ -186,7 +186,7 @@ def test_preview_view(self):
'xmodule.modulestore.split_mongo.runtime.SplitModuleStoreRuntime.render',
VanillaRuntime.render,
)
- @patch('xmodule.capa_block.ProblemBlock.author_view', dummy_render, create=True)
+ @patch('xblocks_contrib.problem.ProblemBlock.author_view', dummy_render, create=True)
@patch('xmodule.x_module.ModuleStoreRuntime.applicable_aside_types', lambda self, block: [])
def test_author_view(self):
""" Test author view rendering """
diff --git a/xmodule/tests/test_library_content.py b/xmodule/tests/test_library_content.py
index d5a7fcbc1cc5..b22a824f53c8 100644
--- a/xmodule/tests/test_library_content.py
+++ b/xmodule/tests/test_library_content.py
@@ -13,11 +13,11 @@
from search.search_engine_base import SearchEngine
from web_fragments.fragment import Fragment
from xblock.runtime import Runtime as VanillaRuntime
+from xblocks_contrib.problem import ProblemBlock
from common.djangoapps.student.tests.factories import UserFactory
from openedx.core.djangoapps.content_libraries import api as lib_api
from openedx.core.djangolib.testing.utils import skip_unless_cms
-from xmodule.capa_block import ProblemBlock
from xmodule.library_content_block import ANY_CAPA_TYPE_VALUE, LegacyLibraryContentBlock
from xmodule.library_tools import LegacyLibraryToolsService
from xmodule.modulestore import ModuleStoreEnum