From d03d12484b8003389399dab3d9b6085e9899b769 Mon Sep 17 00:00:00 2001 From: Jeff Casimir Date: Sun, 13 Sep 2026 15:11:18 -0600 Subject: [PATCH 1/3] chore: enable minitest and pin Ruby via mise The repo had no test infrastructure at all: rails/test_unit/railtie was commented out and test/test_helper.rb didn't exist, even though CLAUDE.md documents `bin/rails test` as the dev workflow. Enable the railtie, add the standard test_helper, and pin the .ruby-version via mise.toml so `mise use ruby@3.4.4` works out of the box. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011EqZeVdG5q6KDS1Q8sbcxu --- config/application.rb | 2 +- mise.toml | 2 ++ test/test_helper.rb | 13 +++++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 mise.toml create mode 100644 test/test_helper.rb diff --git a/config/application.rb b/config/application.rb index 8ec185d..7141610 100644 --- a/config/application.rb +++ b/config/application.rb @@ -12,7 +12,7 @@ require "action_text/engine" require "action_view/railtie" require "action_cable/engine" -# require "rails/test_unit/railtie" +require "rails/test_unit/railtie" # Require the gems listed in Gemfile, including any gems # you've limited to :test, :development, or :production. diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..cd4135e --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +ruby = "3.4.4" diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..7c60862 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,13 @@ +ENV["RAILS_ENV"] ||= "test" +require_relative "../config/environment" +require "rails/test_help" + +module ActiveSupport + class TestCase + # Run tests in parallel with specified workers + parallelize(workers: :number_of_processors) + + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + end +end From a7276df8a44ffb41f3998010cc3730abf87f5f59 Mon Sep 17 00:00:00 2001 From: Jeff Casimir Date: Sun, 13 Sep 2026 15:11:24 -0600 Subject: [PATCH 2/3] feat: load CCSS grades 7-8 into the standards import Extend fetch_ccss_standards.rb to pull grades 4-8 (was 4-6) from the same SirFizX/standards-data source, regenerate the committed JSON as data/ccss-math-grades-4-8.json (180 standards total, up from 110), and point ccss:import at the new file. This unblocks focus-standard tagging for grade 7-8 lesson HTML, whose CCSS codes previously had no matching Standard row. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011EqZeVdG5q6KDS1Q8sbcxu --- ...des-4-6.json => ccss-math-grades-4-8.json} | 562 +++++++++++++++++- lib/tasks/ccss.rake | 2 +- scripts/fetch_ccss_standards.rb | 15 +- 3 files changed, 570 insertions(+), 9 deletions(-) rename data/{ccss-math-grades-4-6.json => ccss-math-grades-4-8.json} (61%) diff --git a/data/ccss-math-grades-4-6.json b/data/ccss-math-grades-4-8.json similarity index 61% rename from data/ccss-math-grades-4-6.json rename to data/ccss-math-grades-4-8.json index 177dc4f..af291d6 100644 --- a/data/ccss-math-grades-4-6.json +++ b/data/ccss-math-grades-4-8.json @@ -1,7 +1,7 @@ { "source": "Common Core State Standards via SirFizX/standards-data (GitHub)", "source_url": "https://raw.githubusercontent.com/SirFizX/standards-data/master/clean-data/CC/math/CC-math-0.8.0.json", - "fetched_at": "2026-03-25T15:34:29Z", + "fetched_at": "2026-09-13T21:08:45Z", "standards": [ { "code": "4.G.1", @@ -882,6 +882,566 @@ "cluster": "Summarize numerical data sets in relation to their context, such as by:", "description": "Relating the choice of measures of center and variability to the shape of the data distribution and the context in which the data were gathered.", "grade_level": 6 + }, + { + "code": "7.EE.1", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Use properties of operations to generate equivalent expressions.", + "description": "Apply properties of operations as strategies to add, subtract, factor, and expand linear expressions with rational coefficients.", + "grade_level": 7 + }, + { + "code": "7.EE.2", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Use properties of operations to generate equivalent expressions.", + "description": "Understand that rewriting an expression in different forms in a problem context can shed light on the problem and how the quantities in it are related. For example, a + 0.05a = 1.05a means that "increase by 5%" is the same as "multiply by 1.05."", + "grade_level": 7 + }, + { + "code": "7.EE.3", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Solve real-life and mathematical problems using numerical and algebraic expressions and equations.", + "description": "Solve multi-step real-life and mathematical problems posed with positive and negative rational numbers in any form (whole numbers, fractions, and decimals), using tools strategically. Apply properties of operations to calculate with numbers in any form; convert between forms as appropriate; and assess the reasonableness of answers using mental computation and estimation strategies. For example: If a woman making $25 an hour gets a 10% raise, she will make an additional 1/10 of her salary an hour, or $2.50, for a new salary of $27.50. If you want to place a towel bar 9 3/4 inches long in the center of a door that is 27 1/2 inches wide, you will need to place the bar about 9 inches from each edge; this estimate can be used as a check on the exact computation.", + "grade_level": 7 + }, + { + "code": "7.EE.4a", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Use variables to represent quantities in a real-world or mathematical problem, and construct simple equations and inequalities to solve problems by reasoning about the quantities.", + "description": "Solve word problems leading to equations of the form px + q = r and p(x + q) = r, where p, q, and r are specific rational numbers. Solve equations of these forms fluently. Compare an algebraic solution to an arithmetic solution, identifying the sequence of the operations used in each approach. For example, the perimeter of a rectangle is 54 cm. Its length is 6 cm. What is its width?", + "grade_level": 7 + }, + { + "code": "7.EE.4b", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Use variables to represent quantities in a real-world or mathematical problem, and construct simple equations and inequalities to solve problems by reasoning about the quantities.", + "description": "Solve word problems leading to inequalities of the form px + q > r or px + q < r, where p, q, and r are specific rational numbers. Graph the solution set of the inequality and interpret it in the context of the problem. For example: As a salesperson, you are paid $50 per week plus $3 per sale. This week you want your pay to be at least $100. Write an inequality for the number of sales you need to make, and describe the solutions.", + "grade_level": 7 + }, + { + "code": "7.G.1", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Draw, construct, and describe geometrical figures and describe the relationships between them.", + "description": "Solve problems involving scale drawings of geometric figures, including computing actual lengths and areas from a scale drawing and reproducing a scale drawing at a different scale.", + "grade_level": 7 + }, + { + "code": "7.G.2", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Draw, construct, and describe geometrical figures and describe the relationships between them.", + "description": "Draw (freehand, with ruler and protractor, and with technology) geometric shapes with given conditions. Focus on constructing triangles from three measures of angles or sides, noticing when the conditions determine a unique triangle, more than one triangle, or no triangle.", + "grade_level": 7 + }, + { + "code": "7.G.3", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Draw, construct, and describe geometrical figures and describe the relationships between them.", + "description": "Describe the two-dimensional figures that result from slicing three-dimensional figures, as in plane sections of right rectangular prisms and right rectangular pyramids.", + "grade_level": 7 + }, + { + "code": "7.G.4", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Solve real-life and mathematical problems involving angle measure, area, surface area, and volume.", + "description": "Know the formulas for the area and circumference of a circle and use them to solve problems; give an informal derivation of the relationship between the circumference and area of a circle.", + "grade_level": 7 + }, + { + "code": "7.G.5", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Solve real-life and mathematical problems involving angle measure, area, surface area, and volume.", + "description": "Use facts about supplementary, complementary, vertical, and adjacent angles in a multi-step problem to write and solve simple equations for an unknown angle in a figure.", + "grade_level": 7 + }, + { + "code": "7.G.6", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Solve real-life and mathematical problems involving angle measure, area, surface area, and volume.", + "description": "Solve real-world and mathematical problems involving area, volume and surface area of two- and three-dimensional objects composed of triangles, quadrilaterals, polygons, cubes, and right prisms.", + "grade_level": 7 + }, + { + "code": "7.NS.1a", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of addition and subtraction to add and subtract rational numbers; represent addition and subtraction on a horizontal or vertical number line diagram.", + "description": "Describe situations in which opposite quantities combine to make 0. For example, a hydrogen atom has 0 charge because its two constituents are oppositely charged.", + "grade_level": 7 + }, + { + "code": "7.NS.1b", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of addition and subtraction to add and subtract rational numbers; represent addition and subtraction on a horizontal or vertical number line diagram.", + "description": "Understand p + q as the number located a distance |q| from p, in the positive or negative direction depending on whether q is positive or negative. Show that a number and its opposite have a sum of 0 (are additive inverses). Interpret sums of rational numbers by describing real-world contexts.", + "grade_level": 7 + }, + { + "code": "7.NS.1c", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of addition and subtraction to add and subtract rational numbers; represent addition and subtraction on a horizontal or vertical number line diagram.", + "description": "Understand subtraction of rational numbers as adding the additive inverse, p — q = p + (—q). Show that the distance between two rational numbers on the number line is the absolute value of their difference, and apply this principle in real-world contexts.", + "grade_level": 7 + }, + { + "code": "7.NS.1d", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of addition and subtraction to add and subtract rational numbers; represent addition and subtraction on a horizontal or vertical number line diagram.", + "description": "Apply properties of operations as strategies to add and subtract rational numbers.", + "grade_level": 7 + }, + { + "code": "7.NS.2a", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of multiplication and division and of fractions to multiply and divide rational numbers.", + "description": "Understand that multiplication is extended from fractions to rational numbers by requiring that operations continue to satisfy the properties of operations, particularly the distributive property, leading to products such as (—1)(—1) = 1 and the rules for multiplying signed numbers. Interpret products of rational numbers by describing real-world contexts.", + "grade_level": 7 + }, + { + "code": "7.NS.2b", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of multiplication and division and of fractions to multiply and divide rational numbers.", + "description": "Understand that integers can be divided, provided that the divisor is not zero, and every quotient of integers (with non-zero divisor) is a rational number. If p and q are integers, then —(p/q) = (—p)/q = p/(—q). Interpret quotients of rational numbers by describing real-world contexts.", + "grade_level": 7 + }, + { + "code": "7.NS.2c", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of multiplication and division and of fractions to multiply and divide rational numbers.", + "description": "Apply properties of operations as strategies to multiply and divide rational numbers.", + "grade_level": 7 + }, + { + "code": "7.NS.2d", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of multiplication and division and of fractions to multiply and divide rational numbers.", + "description": "Convert a rational number to a decimal using long division; know that the decimal form of a rational number terminates in 0s or eventually repeats.", + "grade_level": 7 + }, + { + "code": "7.NS.3", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Apply and extend previous understandings of operations with fractions to add, subtract, multiply, and divide rational numbers.", + "description": "Solve real-world and mathematical problems involving the four operations with rational numbers. Computations with rational numbers extend the rules for manipulating fractions to complex fractions.", + "grade_level": 7 + }, + { + "code": "7.RP.1", + "domain": "Ratios and Proportional Relationships", + "domain_code": "RP", + "cluster": "Analyze proportional relationships and use them to solve real-world and mathematical problems.", + "description": "Compute unit rates associated with ratios of fractions, including ratios of lengths, areas and other quantities measured in like or different units. For example, if a person walks 1/2 mile in each 1/4 hour, compute the unit rate as the complex fraction <sup>1/2</sup>/<sub>1/4</sub> miles per hour, equivalently 2 miles per hour.", + "grade_level": 7 + }, + { + "code": "7.RP.2a", + "domain": "Ratios and Proportional Relationships", + "domain_code": "RP", + "cluster": "Recognize and represent proportional relationships between quantities.", + "description": "Decide whether two quantities are in a proportional relationship, e.g., by testing for equivalent ratios in a table or graphing on a coordinate plane and observing whether the graph is a straight line through the origin.", + "grade_level": 7 + }, + { + "code": "7.RP.2b", + "domain": "Ratios and Proportional Relationships", + "domain_code": "RP", + "cluster": "Recognize and represent proportional relationships between quantities.", + "description": "Identify the constant of proportionality (unit rate) in tables, graphs, equations, diagrams, and verbal descriptions of proportional relationships.", + "grade_level": 7 + }, + { + "code": "7.RP.2c", + "domain": "Ratios and Proportional Relationships", + "domain_code": "RP", + "cluster": "Recognize and represent proportional relationships between quantities.", + "description": "Represent proportional relationships by equations. For example, if total cost t is proportional to the number n of items purchased at a constant price p, the relationship between the total cost and the number of items can be expressed as t = pn.", + "grade_level": 7 + }, + { + "code": "7.RP.2d", + "domain": "Ratios and Proportional Relationships", + "domain_code": "RP", + "cluster": "Recognize and represent proportional relationships between quantities.", + "description": "Explain what a point (x, y) on the graph of a proportional relationship means in terms of the situation, with special attention to the points (0, 0) and (1, r) where r is the unit rate.", + "grade_level": 7 + }, + { + "code": "7.RP.3", + "domain": "Ratios and Proportional Relationships", + "domain_code": "RP", + "cluster": "Analyze proportional relationships and use them to solve real-world and mathematical problems.", + "description": "Use proportional relationships to solve multistep ratio and percent problems. Examples: simple interest, tax, markups and markdowns, gratuities and commissions, fees, percent increase and decrease, percent error.", + "grade_level": 7 + }, + { + "code": "7.SP.1", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Use random sampling to draw inferences about a population.", + "description": "Understand that statistics can be used to gain information about a population by examining a sample of the population; generalizations about a population from a sample are valid only if the sample is representative of that population. Understand that random sampling tends to produce representative samples and support valid inferences.", + "grade_level": 7 + }, + { + "code": "7.SP.2", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Use random sampling to draw inferences about a population.", + "description": "Use data from a random sample to draw inferences about a population with an unknown characteristic of interest. Generate multiple samples (or simulated samples) of the same size to gauge the variation in estimates or predictions. For example, estimate the mean word length in a book by randomly sampling words from the book; predict the winner of a school election based on randomly sampled survey data. Gauge how far off the estimate or prediction might be.", + "grade_level": 7 + }, + { + "code": "7.SP.3", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Draw informal comparative inferences about two populations.", + "description": "Informally assess the degree of visual overlap of two numerical data distributions with similar variabilities, measuring the difference between the centers by expressing it as a multiple of a measure of variability. For example, the mean height of players on the basketball team is 10 cm greater than the mean height of players on the soccer team, about twice the variability (mean absolute deviation) on either team; on a dot plot, the separation between the two distributions of heights is noticeable.", + "grade_level": 7 + }, + { + "code": "7.SP.4", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Draw informal comparative inferences about two populations.", + "description": "Use measures of center and measures of variability for numerical data from random samples to draw informal comparative inferences about two populations. For example, decide whether the words in a chapter of a seventh-grade science book are generally longer than the words in a chapter of a fourth-grade science book.", + "grade_level": 7 + }, + { + "code": "7.SP.5", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Investigate chance processes and develop, use, and evaluate probability models.", + "description": "Understand that the probability of a chance event is a number between 0 and 1 that expresses the likelihood of the event occurring. Larger numbers indicate greater likelihood. A probability near 0 indicates an unlikely event, a probability around 1/2 indicates an event that is neither unlikely nor likely, and a probability near 1 indicates a likely event.", + "grade_level": 7 + }, + { + "code": "7.SP.6", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Investigate chance processes and develop, use, and evaluate probability models.", + "description": "Approximate the probability of a chance event by collecting data on the chance process that produces it and observing its long-run relative frequency, and predict the approximate relative frequency given the probability. For example, when rolling a number cube 600 times, predict that a 3 or 6 would be rolled roughly 200 times, but probably not exactly 200 times.", + "grade_level": 7 + }, + { + "code": "7.SP.7a", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Develop a probability model and use it to find probabilities of events. Compare probabilities from a model to observed frequencies; if the agreement is not good, explain possible sources of the discrepancy.", + "description": "Develop a uniform probability model by assigning equal probability to all outcomes, and use the model to determine probabilities of events. For example, if a student is selected at random from a class, find the probability that Jane will be selected and the probability that a girl will be selected.", + "grade_level": 7 + }, + { + "code": "7.SP.7b", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Develop a probability model and use it to find probabilities of events. Compare probabilities from a model to observed frequencies; if the agreement is not good, explain possible sources of the discrepancy.", + "description": "Develop a probability model (which may not be uniform) by observing frequencies in data generated from a chance process. For example, find the approximate probability that a spinning penny will land heads up or that a tossed paper cup will land open-end down. Do the outcomes for the spinning penny appear to be equally likely based on the observed frequencies?", + "grade_level": 7 + }, + { + "code": "7.SP.8a", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Find probabilities of compound events using organized lists, tables, tree diagrams, and simulation.", + "description": "Understand that, just as with simple events, the probability of a compound event is the fraction of outcomes in the sample space for which the compound event occurs.", + "grade_level": 7 + }, + { + "code": "7.SP.8b", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Find probabilities of compound events using organized lists, tables, tree diagrams, and simulation.", + "description": "Represent sample spaces for compound events using methods such as organized lists, tables and tree diagrams. For an event described in everyday language (e.g., "rolling double sixes"), identify the outcomes in the sample space which compose the event. ", + "grade_level": 7 + }, + { + "code": "7.SP.8c", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Find probabilities of compound events using organized lists, tables, tree diagrams, and simulation.", + "description": "Design and use a simulation to generate frequencies for compound events. For example, use random digits as a simulation tool to approximate the answer to the question: If 40% of donors have type A blood, what is the probability that it will take at least 4 donors to find one with type A blood?", + "grade_level": 7 + }, + { + "code": "8.EE.1", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Work with radicals and integer exponents.", + "description": "Know and apply the properties of integer exponents to generate equivalent numerical expressions. For example, 3² × 3<sup>-5</sup> = 3<sup>-3</sup> = 1/3³ = 1/27.", + "grade_level": 8 + }, + { + "code": "8.EE.2", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Work with radicals and integer exponents.", + "description": "Use square root and cube root symbols to represent solutions to equations of the form x² = p and x³ = p, where p is a positive rational number. Evaluate square roots of small perfect squares and cube roots of small perfect cubes. Know that √2 is irrational.", + "grade_level": 8 + }, + { + "code": "8.EE.3", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Work with radicals and integer exponents.", + "description": "Use numbers expressed in the form of a single digit times an integer power of 10 to estimate very large or very small quantities, and to express how many times as much one is than the other. For example, estimate the population of the United States as 3 × 10<sup>8</sup> and the population of the world as 7 × 10<sup>9</sup>, and determine that the world population is more than 20 times larger.", + "grade_level": 8 + }, + { + "code": "8.EE.4", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Work with radicals and integer exponents.", + "description": "Perform operations with numbers expressed in scientific notation, including problems where both decimal and scientific notation are used. Use scientific notation and choose units of appropriate size for measurements of very large or very small quantities (e.g., use millimeters per year for seafloor spreading). Interpret scientific notation that has been generated by technology.", + "grade_level": 8 + }, + { + "code": "8.EE.5", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Understand the connections between proportional relationships, lines, and linear equations.", + "description": "Graph proportional relationships, interpreting the unit rate as the slope of the graph. Compare two different proportional relationships represented in different ways. For example, compare a distance-time graph to a distance-time equation to determine which of two moving objects has greater speed.", + "grade_level": 8 + }, + { + "code": "8.EE.6", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Understand the connections between proportional relationships, lines, and linear equations.", + "description": "Use similar triangles to explain why the slope m is the same between any two distinct points on a non-vertical line in the coordinate plane; derive the equation y = mx for a line through the origin and the equation y = mx + b for a line intercepting the vertical axis at b.", + "grade_level": 8 + }, + { + "code": "8.EE.7a", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Solve linear equations in one variable.", + "description": "Give examples of linear equations in one variable with one solution, infinitely many solutions, or no solutions. Show which of these possibilities is the case by successively transforming the given equation into simpler forms, until an equivalent equation of the form x = a, a = a, or a = b results (where a and b are different numbers).", + "grade_level": 8 + }, + { + "code": "8.EE.7b", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Solve linear equations in one variable.", + "description": "Solve linear equations with rational number coefficients, including equations whose solutions require expanding expressions using the distributive property and collecting like terms.", + "grade_level": 8 + }, + { + "code": "8.EE.8a", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Analyze and solve pairs of simultaneous linear equations.", + "description": "Understand that solutions to a system of two linear equations in two variables correspond to points of intersection of their graphs, because points of intersection satisfy both equations simultaneously.", + "grade_level": 8 + }, + { + "code": "8.EE.8b", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Analyze and solve pairs of simultaneous linear equations.", + "description": "Solve systems of two linear equations in two variables algebraically, and estimate solutions by graphing the equations. Solve simple cases by inspection. For example, 3x + 2y = 5 and 3x + 2y = 6 have no solution because 3x + 2y cannot simultaneously be 5 and 6.", + "grade_level": 8 + }, + { + "code": "8.EE.8c", + "domain": "Expressions and Equations", + "domain_code": "EE", + "cluster": "Analyze and solve pairs of simultaneous linear equations.", + "description": "Solve real-world and mathematical problems leading to two linear equations in two variables. For example, given coordinates for two pairs of points, determine whether the line through the first pair of points intersects the line through the second pair.", + "grade_level": 8 + }, + { + "code": "8.F.1", + "domain": "Functions", + "domain_code": "F", + "cluster": "Define, evaluate, and compare functions.", + "description": "Understand that a function is a rule that assigns to each input exactly one output. The graph of a function is the set of ordered pairs consisting of an input and the corresponding output. Function notation is not required in Grade 8.", + "grade_level": 8 + }, + { + "code": "8.F.2", + "domain": "Functions", + "domain_code": "F", + "cluster": "Define, evaluate, and compare functions.", + "description": "Compare properties of two functions each represented in a different way (algebraically, graphically, numerically in tables, or by verbal descriptions). For example, given a linear function represented by a table of values and a linear function represented by an algebraic expression, determine which function has the greater rate of change.", + "grade_level": 8 + }, + { + "code": "8.F.3", + "domain": "Functions", + "domain_code": "F", + "cluster": "Define, evaluate, and compare functions.", + "description": "Interpret the equation y = mx + b as defining a linear function, whose graph is a straight line; give examples of functions that are not linear. For example, the function A = s² giving the area of a square as a function of its side length is not linear because its graph contains the points (1,1), (2,4) and (3,9), which are not on a straight line.", + "grade_level": 8 + }, + { + "code": "8.F.4", + "domain": "Functions", + "domain_code": "F", + "cluster": "Use functions to model relationships between quantities.", + "description": "Construct a function to model a linear relationship between two quantities. Determine the rate of change and initial value of the function from a description of a relationship or from two (x, y) values, including reading these from a table or from a graph. Interpret the rate of change and initial value of a linear function in terms of the situation it models, and in terms of its graph or a table of values.", + "grade_level": 8 + }, + { + "code": "8.F.5", + "domain": "Functions", + "domain_code": "F", + "cluster": "Use functions to model relationships between quantities.", + "description": "Describe qualitatively the functional relationship between two quantities by analyzing a graph (e.g., where the function is increasing or decreasing, linear or nonlinear). Sketch a graph that exhibits the qualitative features of a function that has been described verbally.", + "grade_level": 8 + }, + { + "code": "8.G.1a", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Verify experimentally the properties of rotations, reflections, and translations:", + "description": "Lines are taken to lines, and line segments to line segments of the same length.", + "grade_level": 8 + }, + { + "code": "8.G.1b", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Verify experimentally the properties of rotations, reflections, and translations:", + "description": "Angles are taken to angles of the same measure.", + "grade_level": 8 + }, + { + "code": "8.G.1c", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Verify experimentally the properties of rotations, reflections, and translations:", + "description": "Parallel lines are taken to parallel lines.", + "grade_level": 8 + }, + { + "code": "8.G.2", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Understand congruence and similarity using physical models, transparencies, or geometry software.", + "description": "Understand that a two-dimensional figure is congruent to another if the second can be obtained from the first by a sequence of rotations, reflections, and translations; given two congruent figures, describe a sequence that exhibits the congruence between them.", + "grade_level": 8 + }, + { + "code": "8.G.3", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Understand congruence and similarity using physical models, transparencies, or geometry software.", + "description": "Describe the effect of dilations, translations, rotations, and reflections on two-dimensional figures using coordinates.", + "grade_level": 8 + }, + { + "code": "8.G.4", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Understand congruence and similarity using physical models, transparencies, or geometry software.", + "description": "Understand that a two-dimensional figure is similar to another if the second can be obtained from the first by a sequence of rotations, reflections, translations, and dilations; given two similar two-dimensional figures, describe a sequence that exhibits the similarity between them.", + "grade_level": 8 + }, + { + "code": "8.G.5", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Understand congruence and similarity using physical models, transparencies, or geometry software.", + "description": "Use informal arguments to establish facts about the angle sum and exterior angle of triangles, about the angles created when parallel lines are cut by a transversal, and the angle-angle criterion for similarity of triangles. For example, arrange three copies of the same triangle so that the sum of the three angles appears to form a line, and give an argument in terms of transversals why this is so.", + "grade_level": 8 + }, + { + "code": "8.G.6", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Understand and apply the Pythagorean Theorem.", + "description": "Explain a proof of the Pythagorean Theorem and its converse.", + "grade_level": 8 + }, + { + "code": "8.G.7", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Understand and apply the Pythagorean Theorem.", + "description": "Apply the Pythagorean Theorem to determine unknown side lengths in right triangles in real-world and mathematical problems in two and three dimensions.", + "grade_level": 8 + }, + { + "code": "8.G.8", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Understand and apply the Pythagorean Theorem.", + "description": "Apply the Pythagorean Theorem to find the distance between two points in a coordinate system.", + "grade_level": 8 + }, + { + "code": "8.G.9", + "domain": "Geometry", + "domain_code": "G", + "cluster": "Solve real-world and mathematical problems involving volume of cylinders, cones, and spheres.", + "description": "Know the formulas for the volumes of cones, cylinders, and spheres and use them to solve real-world and mathematical problems.", + "grade_level": 8 + }, + { + "code": "8.NS.1", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Know that there are numbers that are not rational, and approximate them by rational numbers.", + "description": "Know that numbers that are not rational are called irrational. Understand informally that every number has a decimal expansion; for rational numbers show that the decimal expansion repeats eventually, and convert a decimal expansion which repeats eventually into a rational number.", + "grade_level": 8 + }, + { + "code": "8.NS.2", + "domain": "The Number System", + "domain_code": "NS", + "cluster": "Know that there are numbers that are not rational, and approximate them by rational numbers.", + "description": "Use rational approximations of irrational numbers to compare the size of irrational numbers, locate them approximately on a number line diagram, and estimate the value of expressions (e.g., π²). For example, by truncating the decimal expansion of √2, show that √2 is between 1 and 2, then between 1.4 and 1.5, and explain how to continue on to get better approximations.", + "grade_level": 8 + }, + { + "code": "8.SP.1", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Investigate patterns of association in bivariate data.", + "description": "Construct and interpret scatter plots for bivariate measurement data to investigate patterns of association between two quantities. Describe patterns such as clustering, outliers, positive or negative association, linear association, and nonlinear association.", + "grade_level": 8 + }, + { + "code": "8.SP.2", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Investigate patterns of association in bivariate data.", + "description": "Know that straight lines are widely used to model relationships between two quantitative variables. For scatter plots that suggest a linear association, informally fit a straight line, and informally assess the model fit by judging the closeness of the data points to the line.", + "grade_level": 8 + }, + { + "code": "8.SP.3", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Investigate patterns of association in bivariate data.", + "description": "Use the equation of a linear model to solve problems in the context of bivariate measurement data, interpreting the slope and intercept. For example, in a linear model for a biology experiment, interpret a slope of 1.5 cm/hr as meaning that an additional hour of sunlight each day is associated with an additional 1.5 cm in mature plant height.", + "grade_level": 8 + }, + { + "code": "8.SP.4", + "domain": "Statistics and Probability", + "domain_code": "SP", + "cluster": "Investigate patterns of association in bivariate data.", + "description": "Understand that patterns of association can also be seen in bivariate categorical data by displaying frequencies and relative frequencies in a two-way table. Construct and interpret a two-way table summarizing data on two categorical variables collected from the same subjects. Use relative frequencies calculated for rows or columns to describe possible association between the two variables. For example, collect data from students in your class on whether or not they have a curfew on school nights and whether or not they have assigned chores at home. Is there evidence that those who have a curfew also tend to have chores?", + "grade_level": 8 } ] } \ No newline at end of file diff --git a/lib/tasks/ccss.rake b/lib/tasks/ccss.rake index 37655ac..abf81f0 100644 --- a/lib/tasks/ccss.rake +++ b/lib/tasks/ccss.rake @@ -1,7 +1,7 @@ namespace :ccss do desc "Import Common Core State Standards from JSON" task import: :environment do - path = Rails.root.join("data/ccss-math-grades-4-6.json") + path = Rails.root.join("data/ccss-math-grades-4-8.json") data = JSON.parse(File.read(path)) created = 0 diff --git a/scripts/fetch_ccss_standards.rb b/scripts/fetch_ccss_standards.rb index a3cb7f9..9cb3345 100644 --- a/scripts/fetch_ccss_standards.rb +++ b/scripts/fetch_ccss_standards.rb @@ -1,18 +1,18 @@ #!/usr/bin/env ruby # frozen_string_literal: true -# Fetches Common Core State Standards for Mathematics (Grades 4-6) from the +# Fetches Common Core State Standards for Mathematics (Grades 4-8) from the # SirFizX/standards-data GitHub repo and structures them for CommonMath. # # Usage: # ruby scripts/fetch_ccss_standards.rb # -# Output: data/ccss-math-grades-4-6.json +# Output: data/ccss-math-grades-4-8.json require "open-uri" require "json" -OUTPUT_PATH = File.expand_path("../data/ccss-math-grades-4-6.json", __dir__) +OUTPUT_PATH = File.expand_path("../data/ccss-math-grades-4-8.json", __dir__) SOURCE_URL = "https://raw.githubusercontent.com/SirFizX/standards-data/master/clean-data/CC/math/CC-math-0.8.0.json" DOMAIN_NAMES = { @@ -24,11 +24,12 @@ "RP" => "Ratios and Proportional Relationships", "NS" => "The Number System", "EE" => "Expressions and Equations", - "SP" => "Statistics and Probability" + "SP" => "Statistics and Probability", + "F" => "Functions" }.freeze -TARGET_GRADES = %w[04 05 06].freeze -TARGET_GRADE_INTS = [ 4, 5, 6 ].freeze +TARGET_GRADES = %w[04 05 06 07 08].freeze +TARGET_GRADE_INTS = [ 4, 5, 6, 7, 8 ].freeze $stderr.puts "Fetching CCSS data from #{SOURCE_URL}..." raw = URI.open(SOURCE_URL).read @@ -49,7 +50,7 @@ clusters[entry["code"]] = entry["statement"] end -$stderr.puts "Found #{clusters.length} clusters for grades 4-6" +$stderr.puts "Found #{clusters.length} clusters for grades #{TARGET_GRADE_INTS.first}-#{TARGET_GRADE_INTS.last}" # Second pass: extract leaf standards standards = [] From eac57abf990358777b0d74ad0f791b47d46edf52 Mon Sep 17 00:00:00 2001 From: Jeff Casimir Date: Sun, 13 Sep 2026 15:11:32 -0600 Subject: [PATCH 3/3] feat: tag topics and lessons to CCSS focus standards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Engageny::FocusStandardTagger, which parses the ny-list-focusstandards markup EngageNY topic overviews carry (the same Aspose list-style class convention as ny-h1 headings, per Engageny::Importer) into CCSS codes, normalizes them to the dotted form standards.code uses (stripping any embedded cluster letter — our imported Standard rows never carry one), and creates polymorphic StandardTagging rows. Focus Standards are listed once per Topic overview in the source curriculum, not per Lesson, so the Topic is the taggable and every Lesson beneath it inherits the union of its Topic's standards. Tagging is idempotent (existing rows are never recreated), and codes with no matching Standard row (grade 7-8 codes not yet imported, cluster-only references like 5.NBT.A) are reported rather than dropped. Add standards:tag_focus, a rake task that runs the tagger over every Topic with overview HTML and prints topic/lesson tagging counts plus an unresolved-code frequency report (also written to tmp/standards_tag_focus_unresolved.txt). The real EngageNY topic-overview HTML lives only in the production database (data/engageny/grade-*-html/ is gitignored and empty in this checkout), so tests build fixtures under test/fixtures/files/ that mirror the ny-list-focusstandards markup as described in the importer. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011EqZeVdG5q6KDS1Q8sbcxu --- lib/engageny/focus_standard_tagger.rb | 124 ++++++++++++++++++ lib/tasks/standards.rake | 36 +++++ .../files/topic_overview_focus_standards.html | 10 ++ ...ic_overview_unresolved_focus_standard.html | 9 ++ .../engageny/focus_standard_tagger_test.rb | 112 ++++++++++++++++ test/tasks/standards_tag_focus_test.rb | 46 +++++++ 6 files changed, 337 insertions(+) create mode 100644 lib/engageny/focus_standard_tagger.rb create mode 100644 lib/tasks/standards.rake create mode 100644 test/fixtures/files/topic_overview_focus_standards.html create mode 100644 test/fixtures/files/topic_overview_unresolved_focus_standard.html create mode 100644 test/lib/engageny/focus_standard_tagger_test.rb create mode 100644 test/tasks/standards_tag_focus_test.rb diff --git a/lib/engageny/focus_standard_tagger.rb b/lib/engageny/focus_standard_tagger.rb new file mode 100644 index 0000000..6afaf26 --- /dev/null +++ b/lib/engageny/focus_standard_tagger.rb @@ -0,0 +1,124 @@ +require "nokogiri" + +module Engageny + # Extracts CCSS codes from the "Focus Standards" markup EngageNY topic + # overviews carry (a `ny-list-focusstandards` CSS class applied to the + # paragraphs/list items Aspose emits for that Word list style, the same + # convention as `ny-h1` for headings — see Engageny::Importer#extract_topic_title) + # and turns them into StandardTagging rows. + # + # Focus Standards are listed once per Topic overview in the source + # curriculum, not per Lesson, so the Topic is the canonical taggable and + # every Lesson under it inherits the union of its Topic's standards. + class FocusStandardTagger + FOCUS_STANDARDS_CLASS = "ny-list-focusstandards" + + # Matches a full standard code, optionally with an embedded cluster + # letter (e.g. "5.NBT.1" or "5.NBT.A.1"), or a bare cluster reference + # with no leaf number (e.g. "5.NBT.A"). The longer, number-bearing form + # is tried first so it wins over the shorter cluster-only alternative. + CODE_PATTERN = %r{ + \d{1,2}\.[A-Z]{1,4}\.(?:[A-Z]\.)?\d+[a-z]? # e.g. 5.NBT.1, 5.NBT.A.1, 5.NF.4a + | \d{1,2}\.[A-Z]{1,4}\.[A-Z]\b # cluster only, e.g. 5.NBT.A + }x + + Report = Struct.new(:topic_taggings, :lesson_taggings, :unresolved, keyword_init: true) do + def initialize(**kwargs) + super(topic_taggings: 0, lesson_taggings: 0, unresolved: Hash.new(0), **kwargs) + end + + def merge!(other) + self.topic_taggings += other.topic_taggings + self.lesson_taggings += other.lesson_taggings + other.unresolved.each { |code, count| unresolved[code] += count } + self + end + end + + # Extracts the raw CCSS codes present in the given HTML's focus-standards + # markup. Returns [] for blank, nil, or malformed HTML, or HTML with no + # focus-standards markup at all. Never raises. + def self.extract_codes(html) + return [] if html.blank? + + doc = Nokogiri::HTML::DocumentFragment.parse(html) + nodes = doc.css(".#{FOCUS_STANDARDS_CLASS}") + return [] if nodes.empty? + + text = nodes.map(&:text).join(" ") + text = text.scrub("") + text.scan(CODE_PATTERN).uniq + rescue StandardError + [] + end + + # Normalizes a raw extracted code to the dotted form Standard#code uses: + # grade.domain.number[subletter], with any embedded cluster letter + # stripped (our imported Standard rows never carry the cluster letter, + # e.g. "5.NBT.1" not "5.NBT.A.1"). A bare cluster reference like + # "5.NBT.A" has no leaf number to normalize to and is returned as-is — + # it will not resolve to a Standard and is reported unresolved. + def self.normalize(raw_code) + parts = raw_code.split(".") + return raw_code unless parts.last.match?(/\A\d+[a-z]?\z/) + return raw_code unless parts.length == 4 + + [ parts[0], parts[1], parts[3] ].join(".") + end + + # Tags a single Topic (from its overview_html) and every Lesson beneath + # it (with the same, unioned set of standards). Idempotent: existing + # StandardTagging rows are left alone and not recreated. + def self.tag_topic!(topic) + report = Report.new + codes = extract_codes(topic.overview_html) + return report if codes.empty? + + standards = [] + codes.each do |raw| + normalized = normalize(raw) + standard = Standard.find_by(code: normalized) + if standard + standards << standard + else + report.unresolved[normalized] += 1 + end + end + standards.uniq! + return report if standards.empty? + + report.topic_taggings += create_missing_taggings(topic, standards) + + topic.lessons.each do |lesson| + report.lesson_taggings += create_missing_taggings(lesson, standards) + end + + report + end + + def self.create_missing_taggings(taggable, standards) + existing_ids = StandardTagging.where( + taggable_type: taggable.class.polymorphic_name, + taggable_id: taggable.id + ).pluck(:standard_id) + + to_create = standards.reject { |s| existing_ids.include?(s.id) } + return 0 if to_create.empty? + + now = Time.current + StandardTagging.insert_all( + to_create.map do |standard| + { + standard_id: standard.id, + taggable_type: taggable.class.polymorphic_name, + taggable_id: taggable.id, + created_at: now, + updated_at: now + } + end + ) + to_create.size + end + private_class_method :create_missing_taggings + end +end diff --git a/lib/tasks/standards.rake b/lib/tasks/standards.rake new file mode 100644 index 0000000..b0a5045 --- /dev/null +++ b/lib/tasks/standards.rake @@ -0,0 +1,36 @@ +namespace :standards do + desc "Tag topics (and their lessons) with CCSS focus standards parsed from EngageNY overview HTML" + task tag_focus: :environment do + report = Engageny::FocusStandardTagger::Report.new + topics_with_html = 0 + topics_without_html = 0 + + Topic.find_each do |topic| + if topic.overview_html.blank? + topics_without_html += 1 + next + end + + topics_with_html += 1 + report.merge!(Engageny::FocusStandardTagger.tag_topic!(topic)) + end + + puts "Topics with overview HTML: #{topics_with_html}" + puts "Topics without overview HTML (skipped): #{topics_without_html}" + puts "Topic taggings created: #{report.topic_taggings}" + puts "Lesson taggings created: #{report.lesson_taggings}" + + if report.unresolved.any? + puts "\nUnresolved codes (no matching Standard row):" + report.unresolved.sort_by { |_code, count| -count }.each do |code, count| + puts " #{code} (#{count})" + end + + report_path = Rails.root.join("tmp/standards_tag_focus_unresolved.txt") + File.write(report_path, report.unresolved.sort_by { |_code, count| -count }.map { |code, count| "#{code}\t#{count}" }.join("\n") + "\n") + puts "\nWrote unresolved-code report to #{report_path}" + else + puts "\nNo unresolved codes." + end + end +end diff --git a/test/fixtures/files/topic_overview_focus_standards.html b/test/fixtures/files/topic_overview_focus_standards.html new file mode 100644 index 0000000..bf76c6b --- /dev/null +++ b/test/fixtures/files/topic_overview_focus_standards.html @@ -0,0 +1,10 @@ + + + +

Topic A Place Value and Decimal Fractions

+

Focus Standard:

+

5.NBT.1 Recognize that in a multi-digit number, a digit in one place represents 10 times as much as it represents in the place to its right and 1/10 of what it represents in the place to its left.

+

5.NBT.2 Explain patterns in the number of zeros of the product when multiplying a number by powers of 10, and explain patterns in the placement of the decimal point when a decimal is multiplied or divided by a power of 10.

+

Instructional Days: 5

+ + diff --git a/test/fixtures/files/topic_overview_unresolved_focus_standard.html b/test/fixtures/files/topic_overview_unresolved_focus_standard.html new file mode 100644 index 0000000..707daec --- /dev/null +++ b/test/fixtures/files/topic_overview_unresolved_focus_standard.html @@ -0,0 +1,9 @@ + + + +

Topic B Cluster and Out-of-Scope Codes

+

Focus Standard:

+

5.NBT.A Understand the place value system.

+

9.ZZ.1 Not a real grade-4-8 standard.

+ + diff --git a/test/lib/engageny/focus_standard_tagger_test.rb b/test/lib/engageny/focus_standard_tagger_test.rb new file mode 100644 index 0000000..a12d553 --- /dev/null +++ b/test/lib/engageny/focus_standard_tagger_test.rb @@ -0,0 +1,112 @@ +require "test_helper" + +module Engageny + class FocusStandardTaggerTest < ActiveSupport::TestCase + def focus_standards_html + file_fixture("topic_overview_focus_standards.html").read + end + + def unresolved_focus_standard_html + file_fixture("topic_overview_unresolved_focus_standard.html").read + end + + def build_topic(overview_html:, lesson_count: 2) + grade = Grade.create!(number: 5, title: "Grade 5") + content_module = grade.content_modules.create!(number: 1, title: "Module 1", position: 1) + topic = content_module.topics.create!(letter: "A", title: "Topic A", position: 1, overview_html: overview_html) + lesson_count.times do |i| + topic.lessons.create!(number: i + 1, position: i + 1) + end + topic + end + + def create_standard(code:, grade_level: 5, domain: "Number and Operations in Base Ten") + Standard.create!( + code: code, + domain: domain, + description: "Description for #{code}", + grade_level: grade_level + ) + end + + test "extract_codes finds CCSS codes inside ny-list-focusstandards markup" do + codes = FocusStandardTagger.extract_codes(focus_standards_html) + assert_equal %w[5.NBT.1 5.NBT.2], codes.sort + end + + test "extract_codes returns empty array for blank html" do + assert_equal [], FocusStandardTagger.extract_codes(nil) + assert_equal [], FocusStandardTagger.extract_codes("") + end + + test "extract_codes returns empty array when the focus-standards class is absent" do + html = "

5.NBT.1 mentioned but not in the focus standards list

" + assert_equal [], FocusStandardTagger.extract_codes(html) + end + + test "extract_codes yields nothing and does not raise on malformed markup" do + malformed = "

unterminated

" + assert_nothing_raised { FocusStandardTagger.extract_codes(malformed) } + + binary_garbage = "

\xFF\xFE not valid utf-8

".dup.force_encoding("ASCII-8BIT") + assert_nothing_raised { FocusStandardTagger.extract_codes(binary_garbage) } + end + + test "normalize strips an embedded cluster letter but leaves bare cluster codes alone" do + assert_equal "5.NBT.1", FocusStandardTagger.normalize("5.NBT.1") + assert_equal "5.NBT.1", FocusStandardTagger.normalize("5.NBT.A.1") + assert_equal "5.NF.4a", FocusStandardTagger.normalize("5.NF.4a") + assert_equal "5.NBT.A", FocusStandardTagger.normalize("5.NBT.A") + end + + test "tag_topic! creates a tagging on the topic and on every lesson beneath it" do + create_standard(code: "5.NBT.1") + create_standard(code: "5.NBT.2") + topic = build_topic(overview_html: focus_standards_html, lesson_count: 2) + + report = FocusStandardTagger.tag_topic!(topic) + + assert_equal 2, report.topic_taggings + assert_equal 4, report.lesson_taggings # 2 standards x 2 lessons + assert_empty report.unresolved + + assert_equal %w[5.NBT.1 5.NBT.2], topic.standards.reload.pluck(:code).sort + topic.lessons.each do |lesson| + assert_equal %w[5.NBT.1 5.NBT.2], lesson.standards.reload.pluck(:code).sort + end + end + + test "tag_topic! is idempotent on a second run" do + create_standard(code: "5.NBT.1") + create_standard(code: "5.NBT.2") + topic = build_topic(overview_html: focus_standards_html, lesson_count: 2) + + FocusStandardTagger.tag_topic!(topic) + second_report = FocusStandardTagger.tag_topic!(topic) + + assert_equal 0, second_report.topic_taggings + assert_equal 0, second_report.lesson_taggings + assert_equal 2, topic.standard_taggings.count + end + + test "tag_topic! reports codes with no matching Standard row instead of dropping them" do + topic = build_topic(overview_html: unresolved_focus_standard_html, lesson_count: 1) + + report = FocusStandardTagger.tag_topic!(topic) + + assert_equal 0, report.topic_taggings + assert_equal 0, report.lesson_taggings + assert_equal({ "5.NBT.A" => 1, "9.ZZ.1" => 1 }, report.unresolved) + end + + test "tag_topic! returns an empty report for a topic with no overview html" do + topic = build_topic(overview_html: nil, lesson_count: 1) + + report = FocusStandardTagger.tag_topic!(topic) + + assert_equal 0, report.topic_taggings + assert_equal 0, report.lesson_taggings + assert_empty report.unresolved + end + end +end diff --git a/test/tasks/standards_tag_focus_test.rb b/test/tasks/standards_tag_focus_test.rb new file mode 100644 index 0000000..d0674dd --- /dev/null +++ b/test/tasks/standards_tag_focus_test.rb @@ -0,0 +1,46 @@ +require "test_helper" +require "rake" + +class StandardsTagFocusTaskTest < ActiveSupport::TestCase + setup do + Rails.application.load_tasks unless Rake::Task.task_defined?("standards:tag_focus") + Rake::Task["standards:tag_focus"].reenable + end + + def html_fixture(name) + file_fixture(name).read + end + + test "tags topics/lessons with resolvable standards and reports unresolved ones" do + Standard.create!(code: "5.NBT.1", domain: "Number and Operations in Base Ten", description: "d", grade_level: 5) + Standard.create!(code: "5.NBT.2", domain: "Number and Operations in Base Ten", description: "d", grade_level: 5) + + grade = Grade.create!(number: 5, title: "Grade 5") + content_module = grade.content_modules.create!(number: 1, title: "Module 1", position: 1) + + tagged_topic = content_module.topics.create!( + letter: "A", title: "Topic A", position: 1, + overview_html: html_fixture("topic_overview_focus_standards.html") + ) + tagged_topic.lessons.create!(number: 1, position: 1) + + unresolved_topic = content_module.topics.create!( + letter: "B", title: "Topic B", position: 2, + overview_html: html_fixture("topic_overview_unresolved_focus_standard.html") + ) + + content_module.topics.create!(letter: "C", title: "Topic C (no overview)", position: 3, overview_html: nil) + + out, = capture_io { Rake::Task["standards:tag_focus"].invoke } + + assert_equal 2, tagged_topic.standard_taggings.count + assert_equal 0, unresolved_topic.standard_taggings.count + + assert_match(/Topics with overview HTML: 2/, out) + assert_match(/Topics without overview HTML \(skipped\): 1/, out) + assert_match(/Topic taggings created: 2/, out) + assert_match(/Lesson taggings created: 2/, out) + assert_match(/5\.NBT\.A \(1\)/, out) + assert_match(/9\.ZZ\.1 \(1\)/, out) + end +end