Bicker Progress - MLK Day
As previously discussed, I am currently in the process of remaking Bicker with modern technologies and architecture, hopefully along with lessons learned over the last decade about what makes a good community system. In the interest of transparency, Iâm using the blog as a kind of developer journal while I work, posting occasional updates as to where things stand.
If you want to read the other posts in the series, you can get a full list of posts under the bicker tag.
Plan A
After last weekâs easy wins, I started out this past week thinking I would nibble at Bicker around the edges. Create some quick controllers and views and dump out a working CRUD foundation to build the rest of the user interface on.
So, I rigged up a quick global menu to allow for basic account management, based on that same Nopio article. That worked well, so I plunged into creating CategoriesâŚonly to realize that I stupidly made a whole bunch of fields in my database models required (non-null
) and failed to name some of the foreign-key reference columns. This created a minor disaster, where references couldnât be empty, but also needed to map to a row in an imaginary table.
If youâre not a database person, imagine if the government passed a law stating that every person was required to own a vehicle, but due to an offhanded joke and a transcription error, the only permissible vehicle to own was a flying carpet. The requirement is there, but itâs impossible to fill it, so the system chokes itself on red tape.
Itâs a shame that Rails doesnât have an easy way of validating things like this before they happen. I understand why, of course: Itâs the database management systemâs responsibility. But since Rails, out of the box, works with SQLite and it doesnât handle that kind of validation, it seems reasonable to want a redundant layer to catch issues along those lines.
Regardless, on top of the other issue, I also still had that vestigial sessions
table that didnât have any part in the program.
Rollback, into a Ditch
The obvious solution, at this point, was to create a migration to fix the problems. And I did exactly that. A small problem, though, is that either Rails or SQLite doesnât allow for changing reference columns if referential integrity checks fail.
Again, wouldnât it be great if those checks were performed during creation instead of waiting until someone is trying to fix the problemâŚ?
Either way, I couldnât make the necessary changes with a migration, because the schema failed to validate properly. Oops!
Fortunately, since nobody I know of is using Bicker and anybody who might be using it secretly isnât going to be able to make it do anything other than logging in and out, I can get away with being fairly heavy-handed in my solutions, rather than patiently untangling the mess I have gotten myself into. For example, I can runâŚ
rake db:drop
That wipes the entire applicationâs database. From there, I was able to rewrite the migrations to something more sensible, delete the migration creating the sessions
entirely, and then bring the database back up to date. đ
I Donât Know, CanCanCan You?
With a new database schema that wasnât completely broken, I created the scaffolding for categories and messages. I also quickly added some authentication to the controllers:
before_action :authenticate_user!, except: [:index, :show]
So, itâs now possible for anybody to read, but a user must be logged in to do anything else.
However, I had a new problem related to permissioning, so still couldnât create any new records.
ActiveModel::ForbiddenAttributesError in CategoriesController#create
Some quick research turned up an obvious solution: Tell CanCanCan to allow the parameters through. This changes the existing controller helper function from something that looks like this:
def category_params
params.fetch(:category, {})
end
âŚto this:
def category_params
params.fetch(:category, {}).permit(:name, :of, :category_id)
end
That solution works for the categories, but the messagesâŚstill needed some work. In this case, there was no Rails-level error, just an application-level error insisting that User must exist, which should already be the case. Some quick debugging suggested that the :user_id
parameter was being set correctly in a hidden_field
and that the ID matched the only user in the database.
Conveniently enough, since the default development database is just SQLite, itâs not hard to query, first by calling the following to get at the prompt.
sqlite3 db/development.sqlite3
And then querying like any other relational database:
SELECT * FROM users;
There are actually two problems âhidingâ in here, both my errors.
First, due to a stupid copy/paste error, the messageâs category was coming in as part of a different object than the rest of the messageâs parameters. Thatâs easily fixed, of course, just changing the objectâs name in the view.
Second, I forgot that a major implementation detail of Bickerâs model is that the messages donât contain any content of their own, instead farming them out into separate paragraphs. Weâll see why thatâs important in the next week or two.
This sort of thing is slightly harder to fix, but not much. I needed to do a total of three things.
Text to Paragraphs
The first step is to take the content of the message thatâs submitted and divide it up into separate paragraphs. Because we donât want to waste effort later on down the line, we probably donât want any empty paragraphs, so we end up with something like this.
def split_paragraphs msg
pars = msg[:content].split('\n').select { |line| line.length > 0 }
end
It takes the parameter set as input, grabs the text, chops it on every newline, and then filters out any line that has no text in it.
Remove the Content
We canât use the parameters to create a new message object when there are extra fields floating around, so we need to drop the content
field, here.
Fortunately, Rails provides an Object#except()
method, allowing me to ask for params.except(:content)
and get the object I need, so that Bicker can create the message object.
Add the Paragraphs
Now that we have a message object to work with, we can create the paragraph objects. Because I track the paragraphs based on the next item, that calls for Array#reverse_each
to iterate from last to first, creating a paragraph object linked to the message object and no parent.
Obviously, this needs to keep track of the paragraph that was created most recently (if any), so that it can be set as the next property of the new object. And since paragraphs are owned by the message, we canât create the paragraphs until we have the message ready to go.
And then we have the flip side of the paragraphs. We donât want to edit themâagain, the reasons will become apparent in the near futureâbut displaying the message requires re-assembling the chain of paragraphs into a single text. Right now, thereâs a naĂŻve way to handle this (print them in reverse order), but weâre going to be rearranging them in the near future, meaning that we need to keep querying for the next paragraph until we have the complete list. Tedious, but it worksâŚuntil we need to deal with replies, which will complicate that code a bit.
And that takes care of that! I finally have a basic CRUD application working that allows entry and reading of categorized messages. đ
Arts and Crafts
While doing this, I also put together some new graphic design. I re-created Bickie the mascot and the logo as an SVG, tossed together a very quick (and, frankly, substandard) Earthy color scheme, and chose some basic fonts:
I also created a very simple background texture, since I find that such patterns (like the squares on this page) make a site feel less harsh when looking at it for extended periods. In this case, I just used the GNU Image Manipulation Program to create grayscale âplasma clouds,â made the image tileable, and then dropped the opacity to something thatâs not easily noticed, but still detectable.
Itâs all subject to change, of course, and just done to keep me from staring at a stark white screen for hours on end. But it also has the nice side-effect of looking like a real application.
Handling Markdown
Because I still had some time to mess around, I decided to see what it would take to get SimpleMDE to work, here, for creating messages in Markdown for formatting.
It turned out to be trivial. I added the two links from the jsDelivr CDN and made the call to new SimpleMDE()
on the message form. Worked on the first shot!
Quick discovery along the way, though: After a while away from Ruby, I somehow convinced myself that equality comparisons looked like assignments (=
) instead of looking similar but being distinct (==
), as in C-like languages. So, displaying multiple messages was pretty broken until I fixed that.
Of course, just because the user can now type Markdown, that doesnât mean we display it properly. We store the text as the same markup code we receive, so thatâs what would get displayed. Instead, we need to convert it. And for that, it looks like the state of the art in Ruby is the Redcarpet gem.
It took a couple of tries, because I was (stupidly) trying to handle all the paragraphs in the view and because the documentation isnât clear on what the renderer
parameter should be. But after moving that logic to the controller and poking around a bit to discover the Redcarpet::Render::HTML
renderer, I was able to add code like this.
require 'redcarpet'
#...
def show
mark = Redcarpet::Markdown.new(Redcarpet::Render::HTML, extensions = {
:autolink => true,
:disable_indented_code_blocks => true,
:fenced_code_blocks => true,
:footnotes => true,
:strikethrough => true,
:superscript => true,
:tables => true,
:underline => true,
})
#...
p.content = mark.render p.content
#...
end
This comes pretty close to whatâs needed, but unfortunately, Rails tries to be smart, and so âescapesâ the HTML code so that it renders as code instead of formatted text.
Fortunately, though, thereâs a solution over in the view:
<%= raw paragraph.content %>
The raw
function instructs the view to avoid escaping the string. Because thatâs a security problem, Iâll eventually want to sanitize the input, probably by stripping out any HTML tags. Thatâs easy enough, since Rails includes CGI::escapeHTML()
for exactly that purpose.
Hand in hand with this comes RubyPants, which handles conversion of punctuationâsomething thatâs going to be very importantâto the appropriate HTML entities. The same gem is used by Jekyll for blogs like this, for example, which is why youâll see curly quotes and apostrophes, em-dashes, ellipses, and other âfancyâ versions of punctuation, here, some in this very paragraphâŚ
All this conversion, though, causes a few very minor conflicts. For example, both RubyPants and the process of separating lines breaks up code blocks into units that are no longer formatted correctly. But again, thatâs a problem to be solved another day.
Before Markdown
If you want the whole history, here, the original version of Bicker supported Textile as the markup language via RedCloth. At the time, Textile was standard for Ruby applications and, while Markdown existed (its specification dates to 2004), it wasnât in nearly the wide use it sees today. Since Iâm not shooting for backward-compatibility, Iâm opting for the more modern lightweight markup language, here.
Most of the same issues are inevitably involved in both, however. For example, I probably didnât care about code blocks at the time, but numbered lists are inevitably a mess when separating each line into its own paragraph. The conversion turns each item into its own paragraph and so has its own list, which isnât ideal.
We may not be able to fix this, unfortunately. When we start adding replies, weâll be adding the ability to split a line item into two paragraphs, anyway, making it impossible to maintain a numbered listâs ordering unless we put in a lot of work past the point of diminishing returns.
But, as Iâve said about a couple of other things, thatâs a problem to deal with another day.
Next
Now that I have a functioning CRUD application, after some cleanup, the next big step is to add the ability to insert threaded replies into messages.
The replies are critical to making Bicker a functional communication tool, after all, so the that should probably come next. However, thinking about the number of moving parts that might be required to do this in React (my intended target), I reserve the right to change my mind and shift my attention to something that would require somewhat less thoughtâŚ
Credits: The header image is unnamed by an anonymous PxHere photographer and is made available under the CC0 1.0 Universal Public Domain Dedication.
By commenting, you agree to follow the blog's Code of Conduct and that your comment is released under the same license as the rest of the blog.
Tags: ruby rails programming project devjournal bicker