Like Wikipedia, Not Wikipedia is a user-maintained encyclopedia. Anyone can view the information on the site, however, to get involved with creating and maintaining wikis, a user needs to create a free account. From there, a user can upgrade to a paid membership, allowing the creation of private wikis, which can be shared with individuals the user wants to collaborate with.
- Anyone can view public wikis by browsing the site.
- Users can create, edit, delete, and maintain any public wiki using Markdown syntax.
- Users can pay to upgrade to a premium account, allowing the creation of private wikis.
- Premium users can invite other users to collaborate on private wikis they have created.
- Premium users can cancel their subscription.
- When a user downgrades their account, their private wikis will automatically be made public.
Users and User Roles
Rather than creating an authentication system from scratch, Devise was used for user authentication. Devise provides a plethora of customizable options, though only some, such as account recovery via email, were included. By including this Gem and following along with the Devise documentation, I was quickly able to allow users to sign up for Not Wikipedia. However, this was the easiest step.
One challenge of this website was to create different user roles - standard, premium, or admin - as well as handling non-users. A guest should be able to browse through wikis, but they should not be able to edit or delete anything. Standard users should be able to edit and delete public pages, while premium users should be able to create private wikis which they can invite others to collaborate on. Clearly, care needed to be taken handling all of the different roles and permissions. Pundit was chosen to handle authorization for the wiki pages.
Pundit is centered around creating policies which govern whether a user is authorized to perform a particular RESTful action. For example, the code below, which is contained in the wiki policy, checks whether a user can delete a wiki. If the wiki is private, the code checks if the user is the owner, an admin, or a collaborator. If they are, then they can delete the wiki; if they aren’t, the action is not allowed. If the wiki is public, the code just verifies that a user is present. Similar code can be found for all RESTful actions in
Calling such policies is actually quite easy. In the controller, one simply must specify a user and a wiki.
In the controller, one must simply pass a wiki and Pundit magically infers
current_user as the user (if desired, one can explicitly call pundit passing both a user and a wiki).
Pundit was also used for scoping and setting the policy for collaborators, both of which will be discussed in later sections.
Upgrading to Premium
I like money, so getting users to upgrade and give me their cash was pretty important. A premium user has the ability to both create private wikis and invite “collaborators” to work on their private wikis. Collaborators do not have to be premium users. A private wiki can be made public at any time.
Stripe was the way I went for payments and if you haven’t tried it out, you should. The difficulty in implementing the code is that both ActiveRecord (user and subscription models) and the Stripe database (customer, subscription, plan, and credit card) all had to communicate with each other and be maintained. The subscription cycle is as follows:
- The user decides to upgrade to a premium plan.
- The user enters their credit card information.
- The credit card information is securely sent to Stripe.
- Stripe sends back a token which is used to create a customer and subscription.
5. The user’s role is changed from standard to premium and a new subscription is created in ActiveRecord. Sensitive information, such as credit card number, is not stored. Stripe customer and subscription ids are stored and are used when communicating with Stripe, however.
6. After a month, Stripe attempts to charge the card again and renew the subscription resulting in a success or failure. The resulting event object is sent by Stripe via an HTTP POST request which is handled by the stripe controller. The “webhook” route is specified in
routes.rb. If the payment is successful, the subscription dates are upgraded in ActiveRecord. If the payment is unsuccessful, the subscription is deleted on both Stripe and in ActiveRecord and the user is downgraded accordingly.
7. Lastly, a user may want to turn off autopay, leading to the cancellation of their account at the end of the billing period. This can be done on the user’s homepage. Doing so does not cancel the account immediately. Rather, Stipe is told to cancel the subscription at the end of the cycle via
at_period_end: true and ActiveRecord’s
Subscription.autopay is set to
subscriptions_controller#turn_off_autopay). Likewise, autopay can be turned on again anytime before the subscription period ends in the same manner (
subscriptions_controller#turn_on_autopay). When it comes time to charge the user again, if
true, the user will not be charged and their subscriptions will be deleted from Stripe and ActiveRecord.
All of the wiki pages are accompanied by a collapsible sidebar with options that change depending on the page and the user’s role. For example, a guest obviously should not have options to see their own wikis because they don’t have any. As seen in Figure 3, for a guest, the option to create a new wiki redirects to the login page and is left as an incentive for guests to create an account. Each of the options on the sidebar is tailored to each user and page in such a way.
The wiki index page can be seen in the above four images, Figures 1-4. Figures 1-3 show all wikis available for an admin, standard user, and guest respectively. Notice that an admin can see more wikis than a standard or guest user. This is due to the scoping policy specified with our old friend Pundit. In the index action of with wikis controller, one can simply call
policy_scope(Wiki) and an array of all permitted wikis for the current user will be returned. In the policy below, it can be seen that:
- Admin users can view all wikis.
- Premium users can view private wikis they own, private wikis they are collaborating on, and public wikis.
- Standard users can view private wikis they are collaborating on and public wikis.
- Guests can only view public wikis (lame).
With scoping out of the way, the last concern was displaying the relevant wikis for users and allowing them to filter those wikis. In Figures 1, 2, and 4 it can be seen that a user can choose to see all wikis (those rendered by
policy_scope), the own wikis they’ve created, and the private wikis they are collaborating on. Figure 4 displays a screenshot of the wikis a sample standard user is collaborating on. It should be noted that all filtering is done by securely passing a param to the
wiki#index action, re-rendering the view appropriately.
Live Editing of Wiki Pages
wiki#show view, makdown is rendered through Redcarpet, Markdown parser for Ruby. Since the show view is static - no data is changing - this option works great. The wiki’s body is grabbed and passed through the Redcarpet, yielding beautiful markdown language. The problem is, in the
The last major aspect of the site is adding collaborators to private wiki pages. A premium user can create private wikis and invite other users to help maintain the wiki. The first challenge of collaborators was deciding on a data model. What exactly is being created? What is the relationship between a wiki page and a user? Of course, a user has many wikis and a wiki belongs_to a user, but something more was needed here.
The solution was to use the has many through association. I debated for a while on what to call the model which associates the user and wiki models. At first I thought that “collaborators” might be a good name for the model. This would mean that both a wiki and a user have many collaborators. While this sounds logical, it’s actually quite misleading. For example, saying
User A has many collaborators sounds as if the association is describing
User D, etc. This is incorrect. The association is describing the relationship
User A has with the wikis they are collaborating on, not other users who are collaborating on
User A’s wikis. So,
User A has many collaborators would mean that
User A has many wikis which they are collaborating on - a very different meaning.
Scrapping the naming convention described above, I decided the best name for the association was “collaboration.” Both a user and a wiki have many collaborations. Further, a user has many wikis they are “collaborating” on through the collaboration model. Likewise, a wiki has many “collaborators” (different than the hypothetical collaborators above) through the collaboration model. For example,
User A has many collaborations and they are “collaborating” on many wikis as well.
Wiki XYZ has many collaborations as well as “collaborators.” The language is a little tricky, but I find it somewhat reminiscent of followers and following on Twitter or Instagram. Just remember “collaborations” associate the user and wiki models, users are “collaborating” on wikis, and wikis have “collaborators.”
A final thing worth mentioning is that custom names were specified using the source option. “Collaborating” was chosen instead of “wikis” (which would have resulted in conflicted naming) and “collaborators” was chosen instead of “users” for the has many through relationships. These has many through relationships were put to good use many times throughout the site, DRYing up a lot of code. For example, when filtering wikis, like in Figures 1-4, the wikis
current_user is collaborating on can be retrieved with
current_user.collaborating. Likewise, in Figure 6, the collaborators of a given wiki called
wiki can be retrieved with
As seen in Figure 6, collaborators can be added via email and deleted with a button click. Creation and deletion of a collaborator each require a separate request to the collaborations controller and thus a page reload. I didn’t like the idea of a user having to reload the page every time they added or removed a user, so I decided to make the request with AJAX. The flow is as follows:
1. A user searches for a collaborator in the search bar. Data-remote stops the browser from posting a request and instead sends the request via AJAX.
destroy.js.erb are rendered and the view is updated. The
If you were reading carefully, you may have noticed the line
authorize @collaboration (if you didn’t, it’s ok…I still like you). That’s right, I included another Pundit policy for collaborations. This policy specified that only admins, owners, and collaborators may add or delete collaborations. No scoping was necessary for collaborations.
There are many other intricacies that weren’t discussed here, such as all collaborators being destroyed if a user makes a private wiki public and rendering appropriate warning messages or asking the user for confirmation before leaving the page if data has been entered. The source code is rather clean and well documented so it should be fairly easy to read. A lot of care was taken to make the UX go smoothly and cover these fringe cases.
Due to how long this post is already, I’ll briefly describe the other, less interesting, pages on the site. Each user has a profile page, where they can view basic membership information, update their subscription/autopay status, and view their wikis. A user also has a settings page where they can update basic information such as their email and password. They can also cancel their account. Devise also constructs user views for account confirmation, lost confirmations, forgotten passwords, etc. These pages are quite basic and could use more customization.
While Not Wikipedia employs a lot of great code, there was still so much I wanted to address. Here’s a list of a few things off the top of my head:
1. More testing. While testing was done - extensively for Pundit - more could always be added.
2. Skinnier controllers, fatter models. There were a few times I definitely could have made some methods in the model and removed it from the controller.
3. Better error messages and redirects for Pundit.
4. Better styling. The layout is a little dull.
5. A search bar for wikis rather than just browsing. Completely unscalable.
6. Some sort of system similar to GitHub where versions of the wiki page are saved and users can jump to other states of the wiki.
7. Better permissions, e.g. no deleting other people’s wiki pages, only owners of a page can make it public, choose if your collaborators can add or delete other collaborators, approval from owner before edits are added to the page, etc.
8. Allow users to change their credit card. Since Not Wikipedia doesn’t store any information in the database, this would involve creating an ActiveRecord model for credit cards and associating it with the users model. Of course, full credit card numbers and sensitive information wouldn’t be stored in ActiveRecord.
Thanks for reading and please check out the other posts on my site. If you have any remarks send me a message or comment below. If you are looking to employ me or in some other way give me your money, you can contact me through any of the methods listed on the top of the home page. Cheers.
Setup and Configuration
Languages and Frameworks: Ruby on Rails and Bootstrap
Ruby version 2.4.0
Databases: SQLite (Test, Development), PostgreSQL (Production)
Development Tools and Gems include:
- Devise for user authentication
- Redcarpet for Markdown formatting
- Markdown-js for live Markdown formatting
- Pundit for authorization
- Stripe for payments
- Rspec and FactoryGirl for tests
- Environment variables were set using Figaro and are stored in
config/application.yml(ignored by git).
config/application.example.ymldemonstrates how to store environment variables.
To run Not Wikipedia locally: