Quantcast
Channel: NoRedInk
Viewing all 193 articles
Browse latest View live

New! Writing Content

$
0
0

Premium Features

Writing Independent Practice

Testing season is quickly approaching! Check out our new pathways, including Embedding Evidence, Introductions, and Conclusions. Or filter our content by state and national standards to find material to fit your students’ needs.

Not sure how to assign writing-specific practice? Click here.


New! Assignment Form Updates

$
0
0

New Premium Features

Preview Writing Cycle Descriptions & Criteria

Looking to assign a Writing Cycle, but not sure which topics fit into your unit? Check out the topic descriptions next to each rubric item and preview the topic if you need more information.

This feature is available as part of NoRedInk’s Writing Cycle. Click here to learn more.

New Free Features

Previously Assigned Passage Quiz Warning

Worried about reassigning a passage-based quiz? We’ve got you covered. With our new warning directly on the assignment form, you can check to see if you’ve already assigned that quiz to your students.

Learn how to assign a passage-based quiz here.

New! Printable Student Login Strips

$
0
0

New Free Features

Printable Student Login Strips

Worried that students won’t remember their username and password? Create their credentials so you can print them out and give them to students on the first day of class (or the many other times they’ll inevitably forget)!

Learn how to add students to your class here.

Backfilling More Faster

$
0
0

Not so long ago, I wrote about how we swapped out our old mastery engine for a shiny, new one without any downtime. At the time, I glossed over the problem of backfilling what was, at the time, about 250,000,000 rows. This post is a deep dive into what I learned, and mistakes I made, when writing that backfill.

The Backfill

So backfilling is relatively simple. For each record I needed two queries: calculate the current score, then create a new record for the new engine with the same score. I built the simplest possible script, told the team we could start doing QA on our staging machine tomorrow, and went home to run the script on staging.

We have a Rails app, and I wrote the script as a rake task. Here is basically what I had:


task :sync_mastery_scores => :environment do
  StudentTopic.find_each do |student_topic|
    score = student_topic.calculate_mastery_score_the_old_way
  
    student_topic.update_attribute(:new_mastery_score, score)  
  end
end


A script like this produces noticeable load against the production server, so we can’t run it during peek hours. There are about 8 hours every evening when usage is low enough to run it. I should have started by doing a little math: what would it take to get this done in ~8 hours?

( 8 hours / 250,000,000 rows ) / 2 queries/row = 0.0576 ms/query

That means I would need 18 queries to run in 1ms. With round trip in a datacenter at 0.5ms, one approach might be to have each query run in 1ms, and 18 processes working in parallel. That, however, was not what I had built. Here’s what I saw:

New Mastery Records: |=          | 0.00% (15)  ETA: > 4 days

Did you catch it? Here it is again: ETA: > 4 days. That’s what the ProgressBar gem says when it’s going to take so long it doesn’t bother telling you just how long it thinks. But, no problem, cause that was actually just the first half of the script. There are really two datasets, and the second half is the larger one.

We had a goal of releasing the new mastery system in 2 weeks. There was just no way I could complete running this script on both staging and production in less than 2 weeks. I needed to speed it up.

The thing is, the old system stored mastery scores in a complex relational structure requiring several joins. At best I could get a score out in 80ms.

This may have been the point I started freaking out.

I tinkered a little that night, trying to speed up the calculate_mastery_score part by fetching all the records for a student at once. Students have 0-500 scores in the system, so this had the potential to help significantly. It did, in that the progress bar went from “so much time I’m not gonna bother” to “70 hours.” Here’s what I had after that change:


Student.find_each do |student|
  scores = student.calculate_all_mastery_scores_the_old_way
  
  scores.each do |topic_id, score|
    StudentTopic.
      where(student_id: student.id, topic_id: topic_id).  # a unique constraint
      update_all(new_mastery_score: score)
  end
end


N = 250,000,000

I was tired and getting nowhere, so I went to sleep and started the next day fresh. One thing I’m very grateful for is having such a talented and experienced team. First thing the next day, I reached out to @dgtized for advice.

I was thinking parallelization, but he pointed out my real problem was how big N was. I needed to fetch multiple students at the same time, and update multiple rows at the same time, and he had just the thing. Here’s an example stolen from this stackoverflow answer:


UPDATE table_name
   SET a = CASE WHEN id = 1
                THEN 'something'
                WHEN id = 2
                THEN 'something else'
                WHEN id = 3
                THEN 'another'
           END
 WHERE id IN (1,2,3)
;

Bulk Queries

Armed with a way to do bulk updates, and determined to do more with each query, I rejiggered the SQL queries to fetch data in batches of students, and write all the updates for those students at once.

In order to select batches of students more efficiently, I skipped fetching Student, and instead iterated through a range of integers. The only reason this approach might be a problem would be if a large percentage of the ids in that range had no associated users. If that were the case, we would be constructing expensive bulk queries that did very little. Fortunately, that is not the case for us.


BATCH_SIZE = 50
max_id = Student.order("id desc").limit(1).pluck(:id)
ids = ( 1..max_id )

ids.step(BATCH_SIZE) do |first_id|
  last_id = student_id + BATCH_SIZE - 1
  student_ids = ( first_id..last_id )

  # Fetch {primary_key_id => old_mastery_score} for every topic for each student_id
  score_by_primary_key = OldMasteryCalculator.data_for_student_ids(student_ids)
  StudentTopic.batch_update_new_mastery(score_by_primary_key)
end

In order to make the bulk update with CASE statements work, we need to fetch the primary key id for each record that we are updating, along with the mastery score. We use that data to construct the following update query:


class StudentTopic
  def batch_update_new_mastery(score_by_primary_key)
    return if score_by_primary_key.empty?
    case_statement = "CASE id "
    score_by_primary_key.each do |id, score|
      case_statement.concat "WHEN #{id} THEN #{ActiveRecord::Base.sanitize(score)} "
    end

    # If the case statement misses a record, don't do anything (ELSE: no-op)
    case_statement.concat "ELSE new_mastery_score END" 

    update_query.update_all("new_mastery_score = #{case_statement}")
  end
end

If we revisit that math from before, it looks a whole lot different now:

(8 hours / ~144,000 batches) / 2 queries/batch = 100 ms/batch

Now that is a much more attainable goal! Thanks, @dgtized!

Batch Size

There’s definitely a sweet spot for the batch size. Make it too small, and you’re missing out on performance. Make it too large, and it can actually slow you down. You could improve the script to adapt while it’s running – measuring performance and adjusting the batch size over time – but I decided that was overkill in this case. I ran the script for 2 minutes at values between 2 and 100 and compared how many students it was able to update. At the time, this led me to find that 50 was pretty close to optimal. I did later discover, however, that the newest student accounts (highest id values) had much more data, and 20 was a better value for the final id range of the script.


Parallelize

I was feeling pretty good after the batching changes, and was ready to run the scripts again – see where we were. But first, I took the one script and split it into two, one for each table we needed to update, and then tried running each of them. We were at right about 20 hours for each script. Not bad, but not good enough.

From experience at this point, I knew the scripts slowed down as they reached students with higher ids. So, to add a little buffer, I figured a 3-4x speedup would get us to the point where each script was safely under 8 hours.

I still had at my disposal two parallelization strategies: multi-threaded, and multi-process.

Multi-Process

When it comes to multiple threads vs multiple processes, at least in this case, multiple processes is the much easier approach. Running a rake task starts up a brand new, shiny rails process. Running the same rake task in two terminals (or two tmux panes) starts up two, totally separate, rails processes. Now, it doesn’t do us any good to have two processes running at the same time if they’re doing the same work, so the key change here is to be able to pass in to the rake task which section of the work to do. Which looks like this:


task :sync_mastery_scores, [:start_id, :max_id] => :environment do |_, args|
  start_id = args[:start_id]
  max_id   = args[:max_id]
  
  ids = ( start_id..max_id )
  ids.step(BATCH_SIZE) do |first_id|
    last_id = student_id + BATCH_SIZE - 1
    student_ids = ( first_id..last_id )

    score_by_primary_key = OldMasteryCalculator.data_for_student_ids(student_ids)
    StudentTopic.batch_update_new_mastery(score_by_primary_key)
  end
end

Now, I can jump into tmux, open a couple panes, and start two processes:


|--------------------------------------------------------
|
| $ rake sync_mastery_scores[1,50000]
| 
|--------------------------------------------------------
| 
| $ rake sync_mastery_scores[50001,100000] 
|
|--------------------------------------------------------

The EC2 instance I needed to run the script from had two cores, so two processes seemed like a reasonable number, but there’s nothing stopping us from starting three, or a dozen.

Note: There are a number of other ways I could have used to get two processes running (parallel, IO::popen, fork, etc), but I wanted two independent panels for printing out the current progress, and this was the simplest way I knew of doing that.

Multi-Threaded

The multi-threaded approach is undeniably more complex. In fact, it led to its own blog post! I’m reprinting the final code below, and if you’re curious feel free to dive into the earlier post.


task :sync_mastery_scores, [:start_id, :max_id] => :environment do |_, args|
  start_id = args[:start_id]
  max_id   = args[:max_id]

  ids = ( start_id..max_id )

  thread_each(n_threads: 2, ids: ids, batch_size:BATCH_SIZE) do |first_id, last_id|
    student_ids = ( first_id..last_id )
    score_by_primary_key = OldMasteryCalculator.data_for_student_ids(student_ids)
    StudentTopic.batch_update_new_mastery(score_by_primary_key)
  end
end

# Executes a block of code for all ids in a range, in batches of a
# specified size, and distributed across a specified number of threads
#
# @param n_threads, Fixnum, number of threads to create
# @param ids, Range, ids to be distributed between the threads
# @param batch_size, Fixnum, how many ids to pass into the block at a time
# @param block, Block, execute this block passing in a start_id and last_id for
# each batch of ids
def thread_each(n_threads:, ids:, batch_size:, &block)
  _preload = [
    StudentTopic
    OldMasteryCalculator
  ]
  
  ActiveRecord::Base.connection_pool.release_connection

  threads = []
  
  ids_per_thread = (ids.size / n_threads.to_f).ceil
  
  (0…n_threads).each do |thread_idx|
    thread_first_id = ids.first + (thread_idx * ids_per_thread)
    thread_last_id = thread_first_id + ids_per_thread
    thread_ids = (thread_first_id...thread_last_id)

    threads.append Thread.new {
      puts "Thread #{thread_idx} | Starting!"

      thread_ids.step(batch_size) do |id|
        block.call(id, id + batch_size - 1)
      end

      puts "Thread #{thread_idx} | Complete!"
    }
  end

  threads.each { |t| t.abort_on_exception = true }  # make sure the whole task fails if one thread fails
  threads.each { |t| t.join }  # wait for all the Threads to complete
end

One Last Thing About Logging

In the end, I was able to run the script successfully on our staging environment so our work could be tested. I could run two processes, each with two threads, and get the job done in about 8 hours.

Between running it on staging and on production, Marica noticed something on the staging server: About 2 GB of logs in an 8 hour time period. The whole time the script had been running, it had been pumping out ream after ream of logging data to the filesystem. So before running the script on production, I added one little function and called it at the start of the script:


def turn_off_logging!
  nil_logger = Logger.new(nil)
  Rails.logger = nil_logger
  ActiveRecord::Base.logger = nil_logger
end

Some Final Considerations

I wish I could tell you exactly how long it finally took to run on production, but in the end I ran into a couple hiccups that prevented me from getting a good measurement.

For one, I discovered that running more than 4 tasks at a time (2 processes x 2 threads) was enough to affect performance of the production site, even at the very low usage we experience in the late evening.

In addition, I didn’t have a good way to resume from where I left off when something went wrong (e.g. slowing down production), so when I did hit a hiccup I had to kill the tasks and start them over. To minimize the pain of “starting over”, I ran those 4 tasks over smaller chunks of the user space (user_ids 0-200,000, then 200,001-400,000, etc) over the course of two nights.

Using shorter running tasks also helped me adjust when I discovered that the backfill performance was slowing down as the student_ids were getting larger. Because of how NoRedInk has changed over the years, early users produced much less mastery data than newer users. Lowering the batch size for newer users helped to level out the backfill performance. At the start of each chunk, I was able to do a trial run for a minute or two, judge the performance, and adjust the batch size to speed things up.

Fortunately, we designed a backfilling procedure which was idempotent, so it was harmless to kill and restart to my hearts content!

Thanks

And that’s everything I learned. Thanks for coming along on this journey. Hopefully this will help you avoid some of the pitfalls I ran into along the way. If you notice anything I missed or got wrong, I’d love to hear about it and keep learning - please write to me and let me know. Thanks!


Josh Leven
@thejosh
Engineer at NoRedInk

New! Student Mastery Review

$
0
0

New Free Features

Student Self-Guided Mastery Review

Do you have students who finished their assignment early?

If a student has no assignment from his or her teacher, NoRedInk shows students a recommended topic to review. Topics recommended are topics that the student has mastered at least two weeks prior. Students will see their oldest mastered topic first to help promote retention!

Learn about other ways to use NoRedInk for student self-review here.

Artisans at Ikea: why we built (and threw away) our highest fidelity prototype

$
0
0

In Q4 of 2017, one of our technical leads shared a tension she noticed her team was experiencing: “It feels like we’re asking artisans to assemble Ikea furniture.”

Our artisans were a team of talented, motivated, and process-driven engineers. The Ikea furniture was a prototype we were planning to work on for a quarter and then immediately tear down. Why, you may wonder, would we choose to devote five full-time engineers (a significant portion of our team of 24) to work on something for months that we knew we’d throw away? Let’s start from the beginning.

The backstory

NoRedInk was founded with the vision of being a comprehensive writing product, aiming to help all students to better express themselves in writing. Since launching in 2012, we’ve focused more on grammar and conventions than composition and craft. Last year, we finally were ready to turn some of our focus toward building a new writing platform.

We started by creating a new type of assignment called a “Writing Cycle” ™, which leveraged anonymous peer review and our adaptive practice engine to help students write essays. The cycle consisted of four steps. Students would:

  • draft their essays (the “Write” step)
  • practice relevant skills, like building a controversial thesis statement or removing wordy phrases (the “Practice” step)
  • rate their peers’ writing along criteria they had mastered during Practice (the “Rate” step)
  • set goals and revise their own writing based on feedback from their peers (the “Revise” step)

Writing Cycles were our first collaborative assignments, whereby students were relying on the feedback of their peers to complete their work. This model was rooted both in personal experience (our founder had prototyped something similar in 2011 when he was a teacher) as well as research into pedagogy and instruction.

Before launch, we tested the assignment in several local classrooms. Our initial users praised the quality of the practice and the depth of reflection the steps encouraged. We leveraged user feedback to define an MVP and ship our first version of Writing Cycles just in time for the start of last school year.

The problem

Once we built and released Writing Cycles, however, we saw that our MVP wasn’t able to fully support the wide variety of states and edge cases users could enter. Students were getting stuck at various steps in the cycle because they were dependent on their peers’ submissions and feedback to move forward. The assignment took longer than many teachers had planned for, but they didn’t have sufficient controls to move things along.

Upon reflection, it became clear that our user testing model hadn’t drawn sufficient attention to the impact complicated logistics would have on Writing Cycles. Here’s how testing had looked:

  • We visited multiple classrooms to test prototypes and early versions of the Writing Cycle. In some tests, we took over for 2 days to run the full four-step cycle with students.
  • Team members—PMs, designers, engineers, and curriculum designers—interacted with 60+ students and collected survey and usage data along the way.
  • Between visits, we iterated, implementing changes like making the revision process more structured and making peer commenting anonymous.

So many parts of this process had felt right—we had crafted a set of hypotheses and a controlled experiment to test them. The problem, though, was just that: control.

In real classrooms, not only do students move at different paces, but there are absences, fire drills, assemblies, and countless other events that make it difficult to predict how long students will take to do their work.

None of these delays or edge cases were news to us—our team is composed of former teachers and long-time education advocates well acquainted with the challenge of supporting diverse learners—but our testing process had made the outcomes of delays seem less detrimental than they ultimately turned out to be. With ample quantitative and qualitative feedback on our MVP, we knew it was time to revisit how Writing Cycles would work.

The design sprint

The question at hand felt pretty clear: How might we build a more flexible version of the Writing Cycle? To find an answer, we chose to conduct a design sprint, a process we’d used before to tackle complex problems.

We composed a cross-functional team of product managers, designers, engineers, and a user researcher and hunkered down in a conference room. We took our first day to refine our core question, listen to “lightning talks” from various members of the company, and summarize the feedback and insights we’d heard.

We ended up going through two rounds of design review and voting, ultimately arriving at a set of new ideas that we wanted to prototype. Some highlights:

  • Give teachers a “start” button and an “end” button so they can have maximum control over when the assignment is active
  • Give teachers the ability to “skip” students forward if students ever get stuck, overriding the site’s algorithm for calculating when a student can move to the next step
  • Offer students more clarity into what’s expected of them, whom they may be preventing from moving forward, and how their feedback is being received

The prototype

A core element of the design sprint process is building a lightweight prototype that allows for quick learning. This is something we value and use in our standard product development process. That said, we also knew that, this time around, we needed a prototype that would hold up in real, unsupervised classroom environments.

Enter the Ikea furniture. We needed to build a functional, free-standing product with only two weeks of engineering time, so we opted for a cheap, lightweight build that took advantage of existing functionality wherever possible. To do so, we had to quickly iterate on designs, skimp on testing and code reviews, cut scope, seek “hacky” ways to mimic ideal functionality (e.g. leaning more on modals and tool tips than ideal flows), and invest some good, old-fashioned elbow grease.

Meanwhile, our user researcher was working to recruit a group of 70 teachers to test the new prototype in classrooms all over the country. The group was composed of a mix of NoRedInk power users, casual visitors, and teachers who had never used the website. They committed to trying out the new Writing Cycle in their classrooms twice between Thanksgiving and winter break.

Here’s how our next five weeks looked:

  • Our engineers and designer worked at breakneck speed to release and then tweak the prototype, ultimately deploying over 50,000 lines of code and redesigning 54 screens.
  • We tracked usage data to visualize user flows, dropoff funnels, and behaviors.
  • Our user researcher conducted 1-on-1 interviews with over a third of the pilot teachers and led one classroom observation. She also fielded questions and feedback from teachers via email, surveys, and diary-style check-ins throughout the process. This resulted in over 500 tagged “insights” in our qualitative data tracking system, ProductBoard.
  • Our team met almost every day to discuss preliminary feedback and make quick decisions on how we wanted to adjust designs and functionality.

Along the way, our prototype stood on the web development version of wobbly screws and cheap cardboard.

This approach, of course, wasn’t without some serious pitfalls. Teachers and students encountered more bugs and error screens than we’d ever feel comfortable with in our real product. Our engineers had to manually reset two teachers’ accounts because we hadn’t anticipated certain edge cases. That meant they had to delay their lesson plans and make adjustments on the fly. Knowing how valuable teachers’ instructional time is, we didn’t take that lightly.

Our immediate strategy when bugs came up was to provide the best possible customer service to make sure the teachers knew how much we valued and appreciated their participation in the pilot. The whole product team monitored emails from the teachers and prioritized responding as fast as possible. Engineers were there to fix high-pain bugs within hours of issues being reported.

From a longer-term strategic perspective, we weighed the tradeoffs of an imperfect product against the wealth of knowledge we were gaining about how Writing Cycles worked in the real world. We were incredibly grateful to have recruited a group of teachers who were excited about contributing to our product, flexible in the face of interruptions, and honest in sharing both positive and constructive feedback.

The results

Positive feedback started to roll in within days of releasing the prototype, but it wasn’t until we compiled and analyzed all of the data that we understood the impact of our changes.

  • Of the teachers who had used the original Writing Cycle, 100% of them (26/26) said that they preferred the new version.
  • We asked teachers their likelihood of recommending the tool, and calculated an NPS of 62, a statistically significant 158% improvement from our baseline score of 24.
  • We analyzed our usage funnel, finding a significant improvement in the percentage of Writing Cycle assignments that classes were able to fully complete, suggesting that notable blockers had been removed in the new version.

Qualitatively, the excitement pouring in from classrooms was palpable. Several teachers asked us if they could keep using the tool after the pilot period was over, sharing that their students were excited to work on the assignments and showed marked improvement in their writing. Here are some quotes from our teachers:

  • “This is pretty amazing! I keep talking it up to my administrators. Students loved it, and when I told them we’d do one more Writing Cycle this month they were actually excited!”-Jervaise Pileggi

  • “A lot of the [students] have much better body paragraphs as a result. When teenagers tell you that something that’s work is actually helping them, that’s a pretty big win.” -Michael MacLeod

We also learned of various areas of the product that still needed improvement. For example, teachers needed the ability to preview the assignment in advance; students still weren’t fully clear on the status of their assignments; our grading UX made it cumbersome for teachers to grade many students in a single sitting.

Next steps

At the start of 2018, we turned the prototype off and tore out the code. We organized and summarized our learnings and developed an action plan, with the ultimate goal of releasing the new Writing Cycle in time for the 2018-19 school year.

With a wealth of learnings and much greater clarity on our path forward, we felt confident that our investment had paid off. But that’s not to suggest that this rapid build/rapid teardown system will become everyday practice. We’d worked our team hard. Our engineers relished the opportunity to start the new quarter with time to refactor code and lay a stable foundation for the impending new Writing Cycle. Our designer was able to take the time to deeply explore designs, rather than needing to produce new solutions daily. Our user researcher happily watched her unread message count decline, and was able to turn her focus toward summarizing learnings. Rebuilding morale and energy was critical.

The decision to conduct real-world, unsupervised prototype testing should not be taken lightly; it requires a careful weighing of the potential costs and benefits. In the end, even the teachers who experienced the biggest glitches with the prototype shared that the benefits to their students’ writing still outweighed the frustration.

Our product team came away with an important lesson on the why and how of field testing: while not every feature should be tested as rigorously as we did for this prototype, there’s value in turning a skeptical eye toward testing plans and questioning whether a controlled testing environment is always the best one.

Christa Simone is a User Researcher at NoRedInk, and Jocey Karlan is the Director of Product.

My Polymorphic Relationships Adventure How did I get here? Fix an awkward relationship. That was...

$
0
0

My Polymorphic Relationships Adventure

How did I get here?

Fix an awkward relationship. That was the name of the story that I took on as the NoRedInk Writing team embarked on creating our new Peer Review writing product. As we perfected how a teacher can assign and move their students through a cycle of writing steps, it became increasingly obvious that the functionality which we needed to add was not elegantly handled by our current data model. At the end of the day, we needed a new table.

This is a common problem many people working on large, evolving codebases face. What do we do when we need to add a new table but want to use many of the same relationships and components of another table? How do we elegantly handle jumping back and forth between tables that have similar relationships to the same table? The following is a breakdown of my discovery and implementation of polymorphic relationships. It’s not always the right decision, but when it is, it can make working within your tables simpler, prettier, and more efficient.

The Problem

Our current table which handled writing steps, WritingCycles, needed to stick around while we made the gradual transition to our new table, PeerReviews. A single row in the WritingCycle table held information about an assigned series of writing step assignments (writing, practicing topics, rating peers, and revising). It did not point to the corresponding assignment rows in their respective tables. However, the assignments did point to their writing cycle. In other words, the assignments knew about their umbrella writing cycle, but a writing cycle could not reach its assignments. This could have been fixed with a ‘belongs_to’, but it would then have to be duplicated for the new PeerReviews table. This would mean in each assignment table there would be a peer_review_id column and a writing_cycle_id column. These ids should never exist at the same time—yet they could be. If you are thinking “wow, this is really awkward”, that is exactly what we were thinking too.

Our initial solution was to change the direction of the relationship. A writing cycle could point to its assignments, but an assignment could not point to its writing cycle. It wasn’t ideal, but this would allow us to create rows in the PeerReviews table with corresponding assignments, without any hiccups in the WritingCycle→ Assignments relationship.

A First Stab

This initial solution would, of course, solve our problem, but it would involve changing anywhere in the codebase where we referenced assignment.writing_cycle. We would have to change the logic in more than just a few locations. It felt dangerous but in the end I accepted my fate and moved forward with the solution.

About a half hour into writing migrations, I approached my manager, Josh Leven, my go-to Ruby expert, with a random syntax question. For a few minutes he stared at my code, his expression turning more and more confused. Finally, he asked “Why aren’t you using a polymorphic relationship?”.

“Huh?”, I responded. He proceeded to explain.

Polymorphic Relationships!

A polymorphic relationship is a created relationship between at least three tables. It is used when a table can have the same relationship with at least two different tables. Take for example, a Meal table. A meal can be eaten by a customer, in which case the meal row will have a field for the customer_id. But what if a meal can also be eaten by an employee? We cannot just put the employee_id in the customer_id field as there is no protection against the same id being in both tables. In this case, we would want one column, consumer_id, which can hold either the customer_id or the employee_id, as well as another column, consumer_type, which can hold a string indicating which type of consumer it is, “Employee” or “Customer”. Now, when we want to see who ate the last piece of pie, our database will check the consumer_type, go to the respective table, and grab the row where id equals consumer_id. The great part about this solution is that it works both ways—a query can also return all of the meals any given consumer or employee has eaten.

Let’s think about another way to do this before we dive into how to implement a polymorphic relationship. Many have tried to solve this dilemma by creating two id columns, one for each parent table. So, in our example above, we would have a customer_id and an employee_id column. Both would be a foreign key to their respective tables. If you wanted to check who ate the pie, you would check which of the two was not NULL, and query for the row with that id. Technically, this would work. But where this fails is that there is no guarantee that the one of the two columns is always NULL. There could be a situation where there is an id in both columns. Unless the customer and an employee shared a piece of pie in some scandalous breach of professionalism, this situation should not occur in our database. Adding a consumer_type and consumer_id pattern will make sure we can always have non-null values in both columns so it will always point to just one row.

I imagined this is what it felt like when the early pioneers found a short cut through the Rocky Mountains…or something like it. A polymorphic relationship would both allow me to create parallel relationships between WritingCycles and Assignments and PeerReviews and Assignments, as well as prevent me from having to do risky logic changes throughout the codebase. I was ecstatic. I went to work.

How To

Luckily, Rails makes creating polymorphic relationships pretty straightforward.

First, I ran a migration to create the parent_assignment_id and parent_assignment_type columns on our assignment tables.

    class AddPolymorphicParentAssignmentOnRatingAssignments  :environment do |t, args|
         RatingAssignment.update_all(parent_assignment_type: "WritingCycle")

         RatingAssignment.update_all("parent_assignment_id=writing_cycle_id")
      end

We could have also renamed writing_cycle_id , but this would mean that the migration and updating the code with the new names and relationships would all have to happen in one pull request. I opted to have both the writing_cycle_id and the parent_assignment_id exist at the same time as I made the switch to make it a wee bit safer.

Great! I now had two new columns in my assignment tables, parent_assignment_id and parent_assignment_type, which were filled in with the correct data from the corresponding parent WritingCycle! When we were ready to create assignments for PeerReviews it would be as easy as adding the PeerReview id as the parent_assignment_id and “PeerReview” as the peer_assignment_type!

Getting everything to work

The next part got a little chewy. In order to delete the redundant writing_cycle_id column from the assignment tables, I had to swap any reference to it with parent_assignment_id as well as specify the parent_assignment_type where applicable. My team lead, Tessa Kelly, and I, took our time with this one. Jumping into code you haven’t written to change a tiny thing and moving on is deceptively easy. In many files, though tests were passing, I found I had made the code ambiguous and prime for false positives. Three of us reviewed this pull request before we felt okay to merge it into master.

The final bolts we had to screw in were our test alterations. We use a form of FactoryBot which creates rows in our test database to test against. We altered the assignment factories to create a writing cycle and assigned it to the parent assignment:

parent_assignment { create :writing_cycle }

Again, we had to change any reference to the assignments’ writing cycle and replace it with a parent assignment. We took our time with this one too. Subtleties of tests can sometimes take a moment to catch on to and we tried our best to preserve the intentions. If I were to do this over again, I would update the factories before changing any tests. Know what you’re testing against!

It works!

Finally, after all the dust had settled, we deleted the writing_cycle_id column. I was so happy when it was gone, I had to stand up and do a little jig. Gleefully, I created a peer review in the console and jumped between it and its assignments and back again. I had dedicated countless hours to create such a simple and elegant relationship, and I had no regrets.

If you’re struggling in the future with tying a bunch of tables together with tenuous associations, do yourself a favor and look into polymorphic relationships. Hopefully, this blogpost will shed some light on a pretty cool tool to help elevate your database.


Ally McKnight
@allykmcknight
Engineer at NoRedInk

New! Updated Assignment Form, New Pre-made Diagnostics, and Easier Class Management

$
0
0

Welcome to the 2017-2018 school year! We’ve made some big updates this summer.

New Free Features

Assignment Form

We’ve streamlined the assignment creation process into 3 core steps: pick the type of assignment, select the content, and handle the logistics. Our simplified form makes it faster to get work to your students!

Pre-made Diagnostics

Not sure where to start? Try one of our premade planning diagnostics! The diagnostics select standards-aligned, grade-level appropriate content to get your students started. Once you have student data, you can decide what to teach next.

Here are sample diagnostics for grades 4-6, grades 7-9, and grades 10-12. You can browse our full library of pre-made diagnsotics, including diagnostics specifically aligned to state assessments, at this link.

Class Management

Our new class management page is your central hub for controlling your courses and rosters.


New! Interactive lessons, view data as you assign, and reuse past assignments

$
0
0

We’re excited to announce more back-to-school updates to help support you and your students this year!

New Free Features

Interactive Lessons

We’ve rolled out our first batch of interactive lessons, which will introduce students to concepts prior to the start of practice. These lessons include friendly visuals, guided instruction, and targeted tips to set students up for success!

To try out an interactive lesson, go to your Curriculum page, scroll to “Who and Whom,” and then click “Practice this!”

View Data in the Assignment Form

Have student performance data at your fingertips as you create assignments. In the assignment form, expand your student roster to see up-to-date mastery and assessment data. Leverage this data to differentiate assignments for individual students or groups of students.

Reuse Past Assignments

Have assignments from last school year that you loved? On your assignments page, select to view “My archived classes.” You’ll then have the option to share or reuse work from prior classes. Learn more.

New! Curriculum and updates to gradebook, assignments page, and site colors!

$
0
0

We’ve done some cleanup and adjustments to make NoRedInk even easier to use!

New Premium Features

New Exercises on Transitions and Embedding Evidence

We’ve released new pathways focused on “Transition Words and Phrases” and “Avoiding Plagiarism and Using Citations.” Students can develop skills around producing a logical flow of ideas, as well as skills related to paraphrasing, citation, and plagiarism detection.

All topics are available as part of NoRedInk Writing! Free teachers can also try out a topic in each pathway.

New Free Features

Updated Gradebook

Our new gradebook is easier to scan, sort, and export! Learn about the full update here.

Updated Assignments Page

Quickly scan your in-progress, past-due, and upcoming assignments. Take advantage of our prompts to create growth quizzes or other new assignments for your students.

Updated Colors

Our colors got a facelift! We heard from teachers and students that our use of purple during level 1 of mastery could be discouraging or confusing – we’ve updated the colors to be brighter, friendlier, and clearer for your students.

New! “Create a unit” and improved search

$
0
0

Create a Unit

Quickly and easily build a unit of assignments! Start with a Unit Diagnostic and then add on a Practice and a Growth Quiz with a single click. This is a great way to track student growth and ensure skill development.

You’ll see the “create unit” button on your assignments page. You can also check out this Help Center article article for more information!

Improved Search

We’ve improved the searchability of our assignment form to make it easier for teachers to find what they’re looking for!

Accidental abstractions

$
0
0

Sometimes we create abstractions in our code without even realizing it. These might turn out to be very useful but more often will come back to haunt us. In this post we’re going to look at one example of such an abstraction in Elm and how we can improve upon it.

Introduction

Suppose we’re working on a brand new social network called MyNemesis. MyNemesis grew out of frustration with existing social networks eating so much of our time. We’re following so many people sharing so many stories that it’s simply too much. MyNemesis addresses this with its central premise: you can have many friends but only a single nemesis.

Abstracting by accident

We’re going to implement a core functionality of MyNemesis: the profile card. These are its requirements:

  1. Anonymous users looking at a profile card should be able to see a name, avatar, and short bio.
  2. Logged in users looking at a profile card should additionally see a button allowing them to send the profile card’s owner a nemesis request.
  3. Logged in users looking at their own profile card should see a button to break up with their current nemesis, if they have one.
  4. Logged in users looking at the profile card of their nemesis should see a button labeled ‘schedule a show down’. When clicked, it should open up a date picker.

Got it? Let’s get to work! In the grand Elm tradition lets start by designing a model for this profile card.

type alias Profile =
    { user : User
    , loggedInAs : Maybe User
    , showDownDate : Maybe DatePicker
    }


type alias User =
    { id : UserId
    , name : String
    , avatar : Url
    , nemesis : Maybe UserId
    }

Voilà, that was easy! It looks like this solution can do everything we want. We have the data both of the profile card owner and the currently logged in user, if one exists. That takes care of requirements 1 through 3. Then we have some state for the date picker necessary to implement requirement 4. We’re done here, let’s go play some Fussball.

But, wait, before we do that, let’s quickly check in with our future selves. In the future, just after the site’s 1.000.000th show down ended in a cliffhanger, we revisit the profile card to add a new feature. Anonymous users should see a banner on a profile card saying “Create an account to make {name} your nemesis!”. We haven’t touched this code for a while so we start by going to the site as an anonymous user and looking at the current profile card. Then we look at the Model and get confused. The user field on it makes sense, but what are those loggedInAs and showDownDate fields about? For present us it’s clear that this is data used by other functionality. For future us it’s extra context that needs to be absorbed, only to find out afterwards it’s not actually relevant for the task at hand.

Intermezzo: do we actually have a problem?

I think it’s fair to ask at this point if we actually have a problem. Sure, those two fields on the model aren’t relevant for anonymous users, but it’s easy to learn what they’re for. We can come up with some better field names or write a little bit of documentation. I’ll still argue that although this is so far a relatively small problem it is one we could have avoided.

It turns out we unconsciously created an abstraction: that of 'The Profile Card’, where in reality there’s many different profile cards. Unconsciously we’ve made the decision to create a type to support all features, instead of creating separate types for the individual profile cards that exist.

Secondly, it’s not a very good abstraction. Good abstractions lighten our mental load, by using them we hide some details allowing us to focus on the bigger picture. This abstraction does the opposite, using it requires us to know extra details about use cases we’re not interested in. This is a problem that will grow because we have now set the expectation that new profile cards should make use of this generic profile card implementation. Hence every new feature added in any profile card will make all other profile cards more complex.

Let’s be explicit

Let’s make our different uses cases explicit by changing our types! That should serve both as excellent documentation for everyone new to code and will allow the compiler to prevent us from making mistakes.

type Profile
    = AsLoggedOut User
    | AsLoggedIn User
    | OfOwn User
    | OfNemesis
        { user : User
        , showDownDate : Maybe DatePicker
        }


view : Profile -> Html Msg
view profile =
    case profile of
        AsLoggedOut user ->
            ...

That’s so much better! We’ve now explicitly drawn attention to the fact that these different profiles exist, allowing others to zoom in on the case relevant to them and ignore the rest.

There is one further improvement we can make. On most pages of the site we know exactly which profile should be shown, but we still go through this dance of first wrapping our data into this generic Profile type, passing it to a generic view function and then immediately unwrapping it again in a case statement. We’re making the same decision twice!

Making the same decision twice

It turns out that the union type combining all the different profiles is unnecessary. By removing the union type, we can get rid of that extra layer of conditionals. It looks something like:

type LoggedOutProfile
    = LoggedOutProfile User


type LoggedInProfile
    = LoggedInProfile User


type OwnProfile
    = OwnProfile User


type NemesisProfile
    = NemesisProfile
        { user : User
        , showDownDate : Maybe DatePicker
        }



-- These views are used in the views of the different pages.


viewLoggedOutProfile : LoggedOutProfile -> Html msg
viewLoggedOutProfile profile =
    ...


viewLoggedInProfile : LoggedInProfile -> Html Msg
viewLoggedInProfile profile =
    ...


viewOwnProfile : OwnProfile -> Html Msg
viewOwnProfile profile =
    ...

The smaller types extracted from the sum type are actually more powerful on their own! When we use one of these narrower profile types in our functions we make clear to human and compiler alike that particular function is meant to be used for a particular profile only. Of course these top level functions can share a lot of the logic related to rendering common parts of the profile card.

At some point these models and views will end up a part of a single top level model and view, that describe the entire program, but there’s no benefit in rushing the process to this single model by wrapping similar looking things into union types or extensible records. The more code we can write using the smaller types the better, because it’s functions taking and returning these smaller types which are easier to understand, easier to reuse, and offer more type safety.


Jasper Woudenberg
@jasperwoudnberg
Engineer at NoRedInk

New! Curriculum page refresh

$
0
0

We’ve redesigned our Curriculum page to make browsing a breeze!

New Free Features

Updated Curriculum page

Our new Curriculum page is easier to search! Click on a Pathway to see objectives, topics, interactive tutorials, lessons and more!

Check out the full update here.

New! Preview features & student activity

$
0
0

Find long-form passages, tutorials, and student activity in seconds.

Updated Curriculum page

When creating assignments, teachers often want to preview the content first. Head to our updated Curriculum page, find the concept you’d like to teach, and check out a few key features:

Click on the footprints to see what interactive tutorial goes with your topic! 

Want to prompt students to evaluate and correct a 3-5 paragraph passage that covers content from an entire pathway? Preview our long-form passage quizzes. Just look for the purple icon.

Check out how to use these resources for whole-class modeling here.

Updated “Last Active” on “Manage Students” page

Ever wonder when your students last accessed NoRedInk? Was it during class? Rushing to get an assignment in at 11:59pm? Now you can check! Navigate to the “Manage classes” section, click on the “Students” tab, and see an hourly update of when students were last active.

New! Mastery Tab, Sentence Stems, and Mobile Sign-Up

$
0
0

We’ve updated a few features to give teachers what they’ve been asking for!

New Premium Features

Sentence Stems

Want students to give quality feedback during the Step 3 of the Writing Cycle?

Have them keep an eye out for these sentence steps to get them started:

This feature is available as part of NoRedInk’s Writing Cycle. Click here to learn more.

New Free Features

Updated Mastery Tracker

The Mastery tab provides teachers with an overall picture of her class’s current mastery!

This new update takes into account both work that has been assigned and work that has been done by students on their own!

Learn about the full update here.

Updated Mobile Signup

We’ve changed the interface students see when signup using mobile devices, so students can create accounts on the go!


Win a $1000 DonorsChoose.org gift card for collaborating with your colleagues

$
0
0

Building common assessments or final exams in NoRedInk? Have an assignment that really worked for your students? From now until December 15th, share assignments with other teachers for a chance to win!

How does it work?

  • STEP 1: Create or choose an assignment on NoRedInk to share with other teachers
  • STEP 2: Click the Share icon and select “Share with other teachers”
  • STEP 3: Copy the link and share! Anywhere works: Facebook, email, Pinterest, you name it!
  • From now until Dec. 15th: Each time your assignment is reused, you’ll be entered to win the $1000 DonorsChoose.org gift card!

No special signup required; we’ll track everything automatically! Questions? Let us know here.

FAQs

1. What is NoRedInk?

NoRedInk builds stronger writers through interest-based curriculum, adaptive exercises, and actionable data. We teach over 500 skills, covering composition, grammar, mechanics, usage, and style. Sign up for free today!

2. Why would I want to share an assignment?

When you share an assignment, other teachers can quickly assess their students on the same content you assigned. This is a great way to build common assessments across your department or to help a teacher new to NoRedInk get started.

3. What is DonorsChoose.org?

DonorsChoose.org helps connect educators with potential donors able to contribute to the purchase of classroom supplies or experiences. They state their mission as, “We make it easy for anyone to help a classroom in need, moving us closer to a nation where students in every community have the tools and experiences they need for a great education.”

With a $1000 DonorsChoose.org gift card, you’ll be able to create a project on DonorsChoose.org that NoRedInk will help fund. Learn more at https://www.donorschoose.org/

4. What happens when I share an assignment link?

Any teacher who clicks your link will be able to create an assignment that covers the same content you originally assigned. Teachers will be able to adjust and customize the assignment, but their starting point will match the assignment you created.

5. How can I track how many teachers have used my link?

Unfortunately, this isn’t possible at this time. NoRedInk will be tracking all share counts internally.

6. How will I know if I won?

Our team will email the winner after the competition concludes on December 15. We’ll also announce the winners in our blog!

New! Passage Preview in Assignment Form

$
0
0

New Free Features

Long-Form Passage Preview

Do you want to assign a passage quiz, but first you want to make sure it aligns with what students are learning?

When you go to select a passage to assign, you can now preview it on the assignment form before you assign it to students!

Click here to learn more about how to use passages in your instruction!

New! Updated Student Interests

$
0
0

New Student Interests

Re-energize students in the new year with our updated interests!

We listened to students’ top requests and made our content more engaging and relevant than ever!

Students can now practice with sentences featuring…

  • Hit musicians like Ed Sheeran and Solange
  • Popular superheroes like Supergirl and The Flash
  • The Broadway musical Hamilton
  • Coco, Pixar’s latest hit
  • And much more!

New! Additional State Alignment Filters & Student Invite

$
0
0

New Premium Features

State Alignment Filters

Want to know if content on NoRedInk aligns to your state’s standards? We’ve updated our filters to now include ACCRS (AZ), SCCCR (SC), and OLS (OH).

Keep an eye out to see if your state will be next!

Click here to learn more about filters and how to utilize them on the site.

New Free Features

Student Invitation Link

Teachers can now invite students to join their courses via a link!

Learn about the different ways to enroll students in your NoRedInk class here.

New! Class Activity Feature

$
0
0

New Free Features

Class Activity

Need a quick overview of what’s going on in each of your classes? Click “Show Activity” on a class to view late submissions, student data, and assignment activity.

Learn more about the Class Activity feature here.

Viewing all 193 articles
Browse latest View live