diff --git a/Gemfile b/Gemfile index b1a320395a..ea268b2e19 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,12 @@ end group :development, :test do gem 'rubocop', '1.20' end + +gem "pg", "~> 1.4" + +gem "sinatra", "~> 3.0" +gem "sinatra-contrib", "~> 3.0" +gem "webrick", "~> 1.8" +gem "rack-test", "~> 2.1" +gem 'bcrypt', '~> 3.1.7' + diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..0a0ac1a486 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -3,27 +3,37 @@ GEM specs: ansi (1.5.0) ast (2.4.2) - diff-lcs (1.4.4) + bcrypt (3.1.18) + diff-lcs (1.5.0) docile (1.4.0) - parallel (1.20.1) - parser (3.0.2.0) + multi_json (1.15.0) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) + parallel (1.22.1) + parser (3.2.2.0) ast (~> 2.4.1) - rainbow (3.0.0) - regexp_parser (2.1.1) + pg (1.4.6) + rack (2.2.6.4) + rack-protection (3.0.5) + rack + rack-test (2.1.0) + rack (>= 1.3) + rainbow (3.1.1) + regexp_parser (2.7.0) rexml (3.2.5) - rspec (3.10.0) - rspec-core (~> 3.10.0) - rspec-expectations (~> 3.10.0) - rspec-mocks (~> 3.10.0) - rspec-core (3.10.1) - rspec-support (~> 3.10.0) - rspec-expectations (3.10.1) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.1) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-mocks (3.10.2) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.10.0) - rspec-support (3.10.2) + rspec-support (~> 3.12.0) + rspec-support (3.12.0) rubocop (1.20.0) parallel (~> 1.10) parser (>= 3.0.0.0) @@ -33,10 +43,11 @@ GEM rubocop-ast (>= 1.9.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.11.0) - parser (>= 3.0.1.1) - ruby-progressbar (1.11.0) - simplecov (0.21.2) + rubocop-ast (1.28.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + ruby2_keywords (0.0.5) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) @@ -45,22 +56,42 @@ GEM simplecov terminal-table simplecov-html (0.12.3) - simplecov_json_formatter (0.1.3) - terminal-table (3.0.1) + simplecov_json_formatter (0.1.4) + sinatra (3.0.5) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.5) + tilt (~> 2.0) + sinatra-contrib (3.0.5) + multi_json + mustermann (~> 3.0) + rack-protection (= 3.0.5) + sinatra (= 3.0.5) + tilt (~> 2.0) + terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) - unicode-display_width (2.0.0) + tilt (2.1.0) + unicode-display_width (2.4.2) + webrick (1.8.1) PLATFORMS ruby + x86_64-linux DEPENDENCIES + bcrypt (~> 3.1.7) + pg (~> 1.4) + rack-test (~> 2.1) rspec rubocop (= 1.20) simplecov simplecov-console + sinatra (~> 3.0) + sinatra-contrib (~> 3.0) + webrick (~> 1.8) RUBY VERSION ruby 3.0.2p107 BUNDLED WITH - 2.2.26 + 2.4.10 diff --git a/README.md b/README.md index 465eda879b..65fa756631 100644 --- a/README.md +++ b/README.md @@ -1,73 +1,36 @@ -Chitter Challenge -================= + -* Feel free to use Google, your notes, books, etc. but work on your own -* If you refer to the solution of another coach or student, please put a link to that in your README -* If you have a partial solution, **still check in a partial solution** -* You must submit a pull request to this repo with your code by 10am Monday morning +# _Chitter Challenge_ -Challenge: -------- +#### By _**Sidra Iqbal, adapted from Makers Academy**_ -As usual please start by forking this repo. +Technologies Used +----- -We are going to write a small Twitter clone that will allow the users to post messages to a public stream. +* Ruby +* CSS +* HTML +* Postgresql +* Sinatra -Features: +Description: ------- -``` -STRAIGHT UP - -As a Maker -So that I can let people know what I am doing -I want to post a message (peep) to chitter - -As a maker -So that I can see what others are saying -I want to see all peeps in reverse chronological order - -As a Maker -So that I can better appreciate the context of a peep -I want to see the time at which it was made - -As a Maker -So that I can post messages on Chitter as me -I want to sign up for Chitter - -HARDER - -As a Maker -So that only I can post messages on Chitter as me -I want to log in to Chitter - -As a Maker -So that I can avoid others posting messages on Chitter as me -I want to log out of Chitter - -ADVANCED - -As a Maker -So that I can stay constantly tapped in to the shouty box of Chitter -I want to receive an email if I am tagged in a Peep -``` - -Technical Approach: ------ +This project is designed to mimic the popular 'Twitter' app for Makers student use. Chitter will have basic functionality like: creating peeps, viewing peeps (including the time of peep in chronological order), as well as login, sign-up and logout capabilities. -In the last two weeks, you integrated a database using the `pg` gem and Repository classes. You also implemented small web applications using Sinatra, RSpec, HTML and ERB views to make dynamic webpages. You can continue to use this approach when building Chitter Challenge. +The user should have visibility to the login and register links when they are not logged in and similarly only have visibility to the sign-out link when they are logged in, to give a positive user experience. -You can refer to the [guidance on Modelling and Planning a web application](https://github.com/makersacademy/web-applications/blob/main/pills/modelling_and_planning_web_application.md), to help you in planning the different web pages you will need to implement this challenge. If you'd like to deploy your app to Heroku so other people can use it, [you can follow this guidance](https://github.com/makersacademy/web-applications/blob/main/html_challenges/07_deploying.md). +[![Screenshot of main page](https://i.imgur.com/g3bzkKR.png)](https://chitter-app-59gf.onrender.com/) -If you'd like more technical challenge now, try using an [Object Relational Mapper](https://en.wikipedia.org/wiki/Object-relational_mapping) as the database interface, instead of implementing your own Repository classes. -Some useful resources: -**Ruby Object Mapper** -- [ROM](https://rom-rb.org/) +Features: +------- -**ActiveRecord** -- [ActiveRecord ORM](https://guides.rubyonrails.org/active_record_basics.html) -- [Sinatra & ActiveRecord setup](https://learn.co/lessons/sinatra-activerecord-setup) +* Register: User can register to Chitter with their email and create an account with a username and password. +* Login: User can login to their account from the homepage by entering their email address and password. +* Logout: User can logout from the homepage by clicking on the logout link. +* New Peep: Logged in user can have the option to create a new peep by clicking the new peep link on the homepage, where they are directed to the /new_peep page and can submit a peep +* View Peeps: Irrespective of logged in status, peeps will be shown on the homepage in order of the most recent peep first. The peep contents and time are shown on the homepage. Notes on functionality: ------ @@ -76,48 +39,33 @@ Notes on functionality: * Makers sign up to chitter with their email, password, name and a username (e.g. samm@makersacademy.com, password123, Sam Morgan, sjmog). * The username and email are unique. * Peeps (posts to chitter) have the name of the maker and their user handle. -* Your README should indicate the technologies used, and give instructions on how to install and run the tests. - -Bonus: ------ - -If you have time you can implement the following: - -* In order to start a conversation as a maker I want to reply to a peep from another maker. - -And/Or: - -* Work on the CSS to make it look good. -Good luck and let the chitter begin! - -Code Review ------------ +Installation & Requirements +------ -In code review we'll be hoping to see: +Clone the repo and add the relevant gems: -* All tests passing -* High [Test coverage](https://github.com/makersacademy/course/blob/main/pills/test_coverage.md) (>95% is good) -* The code is elegant: every class has a clear responsibility, methods are short etc. +``` +git clone https://github.com/siqbal181/chitter-challenge.git +gem install sinatra -v '~> 3.0' +gem install sinatra-contrib -v '~> 3.0' +gem install webrick -v '~> 1.8' +gem install rack-test -v '~> 2.1' +gem install bcrypt -v '~> 3.1.7' +gem install pg -v '~> 1.4' +``` -Reviewers will potentially be using this [code review rubric](docs/review.md). Referring to this rubric in advance may make the challenge somewhat easier. You should be the judge of how much challenge you want at this moment. +Testing: +```ruby +gem 'rspec' +gem 'simplecov', require: false +gem 'simplecov-console', require: false +``` -Notes on test coverage ----------------------- +## Contact -Please ensure you have the following **AT THE TOP** of your spec_helper.rb in order to have test coverage stats generated -on your pull request: +Sidra Iqbal -```ruby -require 'simplecov' -require 'simplecov-console' - -SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ - SimpleCov::Formatter::Console, - # Want a nice code coverage website? Uncomment this next line! - # SimpleCov::Formatter::HTMLFormatter -]) -SimpleCov.start -``` +Project Link: [https://github.com/siqbal181/chitter-challenge](https://github.com/siqbal181/chitter-challenge) -You can see your test coverage when you run your tests. If you want this in a graphical form, uncomment the `HTMLFormatter` line and see what happens! +
diff --git a/app.rb b/app.rb new file mode 100644 index 0000000000..053b8d107b --- /dev/null +++ b/app.rb @@ -0,0 +1,99 @@ +require 'sinatra' +require "sinatra/reloader" +require_relative 'lib/database_connection' +require_relative 'lib/peep_repository' +require_relative 'lib/user_repository' +require 'date' + +DatabaseConnection.connect('chitter_database') + +class Application < Sinatra::Base + configure :development do + register Sinatra::Reloader + end + + enable :sessions + + get '/' do + repo = PeepRepository.new + @peeps = repo.all + + if session[:email_address] + user_repo = UserRepository.new + @user = user_repo.find_by_email(session[:email_address]) + end + + return erb(:homepage) + end + + get '/register' do + return erb(:register) + end + + post '/register' do # once user has registered + unless params[:email_address].include?('@') + status 400 + return '' + end + + repo = UserRepository.new + user = User.new + user.email_address = params['email_address'] + user.username = params['username'] + user.password = params['password'] + repo.create(user) + + redirect '/' + end + + get '/login' do + return erb(:login) + end + + post '/login' do + email_address = params[:email_address] + password = params[:password] + + user = UserRepository.new.find_by_email(email_address) + + if user && user.password == password + session[:email_address] = user.email_address + + peep_repo = PeepRepository.new + @peeps = peep_repo.all + @user = user + erb(:homepage) + else + @error = true + erb(:login) + end + end + + get '/logout' do + session.clear + redirect '/' + end + + get '/new_peep' do + return erb(:new_peep) + end + + post '/new_peep' do + contents = params[:contents] + + if session[:email_address] + @user = UserRepository.new.find_by_email(session[:email_address]) + end + + peep_repo = PeepRepository.new + new_peep = Peep.new + new_peep.contents = contents + new_peep.time = Time.now.strftime("%d/%m/%Y %H:%M") + new_peep.user_id = @user.id + new_peep.username = @user.username + peep_repo.create_peep(new_peep) + @peeps = peep_repo.all + + return erb(:homepage) + end +end diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..c41dba5056 --- /dev/null +++ b/config.ru @@ -0,0 +1,3 @@ +# file: config.ru +require './app' +run Application diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 0000000000..bb3c976a45 --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,29 @@ +# file: lib/database_connection.rb + +require 'pg' + +class DatabaseConnection + + def self.connect(database_name) + if ENV['DATABASE_URL'] != nil + @connection = PG.connect(ENV['DATABASE_URL']) + return + end + + if ENV['ENV'] == 'test' + database_name = 'chitter_database_test' + else + database_name = 'chitter_database' + end + @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + end + + def self.exec_params(query, params) + if @connection.nil? + raise 'DatabaseConnection.exec_params: Cannot run a SQL query as the connection to'\ + 'the database was never opened. Did you make sure to call first the method '\ + '`DatabaseConnection.connect` in your app.rb file (or in your tests spec_helper.rb)?' + end + @connection.exec_params(query, params) + end +end diff --git a/lib/ignore.rb b/lib/ignore.rb new file mode 100644 index 0000000000..80c563906a --- /dev/null +++ b/lib/ignore.rb @@ -0,0 +1,121 @@ +# repeating app.rb so I understand it all + +require 'sinatra' +require 'sinatra/reloader' +# automatically reload app when change detected +require_relative 'lib/database_connection' +require_relative 'lib/peep_repository' +require_relative 'lib/user_repository' +require 'date' + +DatabaseConnection.connect('chitter_database') + +class Application < Sinatra::Base +# ^ makes a new class 'Application' that inherits from the Sinatra::Base +# Base is main class for web application building with the Sinatra web framework +# Allows us to define routes, configure settings, and add extensions + configue :development do + # sets Sinatra configuration to development environment + register Sinatra::Reloader + end + + enable: sessions + # Store user data for a specific session + + get '/' do + repo = PeepRepository.new + @peeps = repo.all + # Above need to make a new peep repository to show all peeps to users + # Instance variable for the erb file to access + + if session[:email_address] + # above checks if user prev logged in, if they were, the email is stored + # on the session, if not then the email is not stored on the session + # @user instance variable pulls our the user based on finding by email address + # This allows us to access all the elements of the user + @user = UserRepository.new.find_by_email(session[:email_address]) + return erb(:homepage) + else + return erb(:homepage) + end + end + + get '/register' do + return erb(:register) + end + + post '/register' do + if !params[:email_address].include?('@') || + status 400 + return '' + end # In view we have a minlength reference, so dont need to worry about nil + + repo = UserRepository.new + user = User.new + user.email_address = params['email_address'] + user.username = params['username'] + user.password = params['password'] + repo.create(user) + + redirect '/' + end + + get '/login' do + return erb(:login) + end + + post '/login' do + email_address = params[email_address] + password = params[password] + + user = UserRepository.new(find_by_email(email_address)) + # Above assigns a user variable to a new user repo with + # find_by_email, and this finds the user based on email_address + + if user && user.password == password + # if user means if the user is true from line 71 (it exists) + # user.password == password checks the password with decrypt matches + # session[email_address] is created based on user.email_address + session[:email_address] = user.email_address + + peep_repo = PeepRepository.new + @peeps = peep_repo.all + # peep repo needs to be done for every route as it doesn't save per route + @user = user # create this as an instance variable + erb(:homepage) + else + @error = true # error message defined in login page + erb(:login) + end + + get 'logout' do + session.clear # clears the user session + redirect '/' + end + + get '/new_peep' do + return erb(:new_peep) + end + + post '/new_peep' do + contents = params[:contents] + + if session[:email_address] + @user = UserRepository.new.find_by_email(session[:email_address]) + end + + peep_repo = PeepRepository.new + + new_peep = Peep.new + new_peep.contents = contents + new_peep.time = Time.now.strftime("%d/%m/%Y %H:%M") + new_peep.user_id = @user.id + + peep_repo.create_peep(new_peep) + @peeps = peep_repo.all + # Above first is making a new peep based on contents submitted on + # new_peep erb, then relaying all the peeps, maybe i dont have to do this + + return erb(:homepage) + end +end \ No newline at end of file diff --git a/lib/peep.rb b/lib/peep.rb new file mode 100644 index 0000000000..da1e0c5ba4 --- /dev/null +++ b/lib/peep.rb @@ -0,0 +1,3 @@ +class Peep + attr_accessor :id, :time, :contents, :user_id, :username +end diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb new file mode 100644 index 0000000000..5d5697ddf0 --- /dev/null +++ b/lib/peep_repository.rb @@ -0,0 +1,37 @@ +require_relative './peep' +require 'date' + +class PeepRepository + + def all + sql = 'SELECT peeps.id AS "peeps_id", users.username AS "username", + users.id AS "user_id", peeps.contents AS "contents", peeps.time AS "time" + FROM peeps + INNER JOIN users ON peeps.user_id=users.id ORDER BY time desc;' + result_set = DatabaseConnection.exec_params(sql, []) + + peeps = [] + + result_set.each do |row| + peep = Peep.new + peep.id = row['peeps_id'] + peep.time = row['time'] + peep.contents = row['contents'] + peep.user_id = row['user_id'] + peep.username = row['username'] + peeps << peep + end + return peeps + end + + def create_peep(peep) + sql = 'INSERT INTO peeps (time, contents, user_id) + VALUES ($1, $2, $3);' + + time = Time.now.strftime("%d/%m/%Y %H:%M") + sql_params = [time, peep.contents, peep.user_id] + + result_set = DatabaseConnection.exec_params(sql, sql_params) + return nil + end +end diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 0000000000..6967c2d3d7 --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,3 @@ +class User + attr_accessor :id, :email_address, :username, :password +end diff --git a/lib/user_repository.rb b/lib/user_repository.rb new file mode 100644 index 0000000000..61d815ddf2 --- /dev/null +++ b/lib/user_repository.rb @@ -0,0 +1,52 @@ +require_relative './user' +require 'bcrypt' + +class UserRepository + def all + sql = 'SELECT email_address, username, password FROM users;' + result_set = DatabaseConnection.exec_params(sql, []) + + users = [] + + result_set.each do |row| + user = User.new + user.id = row['id'] + user.email_address = row['email_address'] + user.username = row['username'] + user.password = BCrypt::Password.new(row['password']) + users << user + end + return users + end + + def find_by_email(email) + sql = 'SELECT id, email_address, username, password FROM users WHERE email_address = $1;' + result_set = DatabaseConnection.exec_params(sql, [email]) + + if result_set.ntuples.positive? + user_row = result_set[0] + user = User.new + user.id = user_row['id'] + user.email_address = user_row['email_address'] + user.username = user_row['username'] + user.password = BCrypt::Password.new(user_row['password']) + return user + else + nil + end + end + + def create(new_user) + encrypted_password = BCrypt::Password.create(new_user.password) + + sql = 'INSERT INTO users (email_address, username, password) + VALUES($1, $2, $3);' + sql_params = [ + new_user.email_address, + new_user.username, + encrypted_password] + DatabaseConnection.exec_params(sql, sql_params) + + return new_user + end +end diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000000..cde1c13226 --- /dev/null +++ b/public/style.css @@ -0,0 +1,106 @@ +body { + font-family: Arial, Helvetica, sans-serif; + margin: 20; + padding: 20; +} + +h1 { + font-size: 32px; + font-weight: bold; +} + +h2 { + font-size: 24px; + font-weight: bold; +} + +p { + font-size: 16px; +} + +a { + color: #0077cc; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +/* Homepage specific */ + +.options-box { + border: 1px; + background-color: lightblue; + padding: 5px; +} + +.welcome-box { + border: 1px; + background-color: mediumturquoise; + padding: 5px; +} + +.peeps-box { + width: 80%; + margin: auto; + max-height: 300px; + overflow: scroll; +} + + .scrollable-peeps { + padding: 10px; +} + + +/* Register specific */ + +input[type="submit"] { + background-color: lightblue; + color: black; + padding: 10px 20px; + font-weight: bold; + border: grey; + border-radius: 5px; + cursor: pointer; + margin: auto; + width: 50%; +} + +form { + margin: auto; + margin-top: 50px; + width: 50%; +} + +input[type=text] { + margin-bottom: 10px; + padding: 5px; +} + +.join-box { + border: 1px; + background-color: mediumturquoise; + padding: 5px; + margin-bottom: 10px; +} + + +/* Login specific */ + +.login-box { + border: 1px; + background-color: mediumturquoise; + padding: 5px; + margin-bottom: 10px; +} + +/* New Peep specific */ + +.question-peep { + font-family: Arial, Helvetica, sans-serif; + border: 1px; + background-color: mediumturquoise; + padding: 5px; + margin-bottom: 10px; +} \ No newline at end of file diff --git a/repo_and_db_recipes/database_table_recipe.md b/repo_and_db_recipes/database_table_recipe.md new file mode 100644 index 0000000000..f0b9fb5e1b --- /dev/null +++ b/repo_and_db_recipes/database_table_recipe.md @@ -0,0 +1,125 @@ +## 1. Extract nouns from the user stories or specification + +``` +STRAIGHT UP + +As a Maker +So that I can let people know what I am doing +I want to post a message (peep) to chitter + +As a maker +So that I can see what others are saying +I want to see all peeps in reverse chronological order + +As a Maker +So that I can better appreciate the context of a peep +I want to see the time at which it was made + +As a Maker +So that I can post messages on Chitter as me +I want to sign up for Chitter + +HARDER + +As a Maker +So that only I can post messages on Chitter as me +I want to log in to Chitter + +As a Maker +So that I can avoid others posting messages on Chitter as me +I want to log out of Chitter +``` + +``` +Nouns: + +peep, time +maker +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| users | email_address, username, password +| peeps | time, contents, user_id + +1. Name of the first table (always plural): `users` + + Column names: `email_address`, `username`, `password` + +2. Name of the second table (always plural): `peeps` + + Column names: `time`, `contents`, `user_id` + +## 3. Decide the column types. + +[Here's a full documentation of PostgreSQL data types](https://www.postgresql.org/docs/current/datatype.html). + +Most of the time, you'll need either `text`, `int`, `bigint`, `numeric`, or `boolean`. If you're in doubt, do some research or ask your peers. + +Remember to **always** have the primary key `id` as a first column. Its type will always be `SERIAL`. + +``` +# EXAMPLE: + +Table: users +id: SERIAL +email_address: varchar +username: varchar +password: varchar + +Table: peeps +id: SERIAL +time: DATETIME +contents: text +user_id: user_id +``` + +## 4. Decide on The Tables Relationship +``` + +1. Can one user have many posts? YES +2. Can one post have many users? NO + +-> Therefore, +-> An user HAS MANY posts +-> An post BELONGS TO an user + +-> Therefore, the foreign key is on the peeps table. +``` + +## 4. Write the SQL. + +```sql +-- EXAMPLE +-- file: chitter_table.sql + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + email_address varchar(320), + username varchar(50) NOT NULL, + password varchar(60) NOT NULL); + +-- Then the table with the foreign key first. +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + time TIMESTAMP, + contents text, +-- The foreign key name is always {other_table_singular}_id + user_id int, + constraint fk_user foreign key(user_id) + references users(id) + on delete cascade +); +``` + +## 5. Create the tables. + +```bash +psql -h 127.0.0.1 chitter_database < chitter_table.sql +``` + + diff --git a/repo_and_db_recipes/peep_model_repo_class_recipe.md b/repo_and_db_recipes/peep_model_repo_class_recipe.md new file mode 100644 index 0000000000..4e282b6ad2 --- /dev/null +++ b/repo_and_db_recipes/peep_model_repo_class_recipe.md @@ -0,0 +1,97 @@ +# {{chitter_database}} Model and Repository Classes Design Recipe + +## 2. Create Test SQL seeds + +```sql +-- (file: spec/seeds_.sql) + +TRUNCATE TABLE peeps, users RESTART IDENTITY; + +-- Encrypt passwords using bcrypt +INSERT INTO users (email_address, username, password) +VALUES ('sidra@fake.com', 'sidra_fake', crypt('12345', gen_salt('bf'))); +INSERT INTO users (email_address, username, password) +VALUES ('bobby@fake.com', 'bobby_fake', crypt('password', gen_salt('bf'))); +INSERT INTO users (email_address, username, password) +VALUES ('tina@fake.com', 'tina_fake', crypt('password123', gen_salt('bf'))); + +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-12-04 12:03:00', 'This is my post, 1'); +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-13-04 13:03:00', 'Here is my new post, 2'); +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-14-04 13:03:00', 'Here is my other post, 1'); +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-15-04 13:03:00', 'User 3 newer post, 3'); + +``` +```bash +psql -h 127.0.0.1 your_database_name < seeds_{table_name}.sql +``` + +## 4. Implement the Model class + +```ruby +# Table name: peeps + +# Model class +# (in lib/peep.rb) + +class Peep + attr_accessor :id, :time, :contents, :user_id +end +``` + +## 5. Define the Repository Class interface +```ruby +# Table name: peeps + +# Repository class +# (in lib/peep_repository.rb) + +require 'date' + +class PeepRepository + + def all_peeps + # order by time in SQL + end + + def create_peep + # relay the time of the peep + # contents of the peep + end +end +``` + +## 6. Write Test Examples + +```ruby + +# 1 - Shows all peeps +repo = PeepRepository.new +peeps = repo.all +expect(peeps.length).to eq(4) +expect(users.last.contents).to eq('User 3 newer post') + +``` + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +```ruby +# file: spec/student_repository_spec.rb + +def reset_students_table + seed_sql = File.read('spec/seeds_students.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'students' }) + connection.exec(seed_sql) +end + +describe StudentRepository do + before(:each) do + reset_students_table + end +end +``` + +## 8. Test-drive and implement the Repository class behaviour + + diff --git a/repo_and_db_recipes/user_model_repo_class_recipe copy.md b/repo_and_db_recipes/user_model_repo_class_recipe copy.md new file mode 100644 index 0000000000..1a37e31f0e --- /dev/null +++ b/repo_and_db_recipes/user_model_repo_class_recipe copy.md @@ -0,0 +1,139 @@ +# {{chitter_database}} Model and Repository Classes Design Recipe + +## 2. Create Test SQL seeds + +```sql +-- (file: spec/seeds_.sql) + +TRUNCATE TABLE peeps, users RESTART IDENTITY; + +-- Encrypt passwords using bcrypt +INSERT INTO users (email_address, username, password) +VALUES ('sidra@fake.com', 'sidra_fake', crypt('12345', gen_salt('bf'))); +INSERT INTO users (email_address, username, password) +VALUES ('bobby@fake.com', 'bobby_fake', crypt('password', gen_salt('bf'))); +INSERT INTO users (email_address, username, password) +VALUES ('tina@fake.com', 'tina_fake', crypt('password123', gen_salt('bf'))); + +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-12-04 12:03:00', 'This is my post, 1'); +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-13-04 13:03:00', 'Here is my new post, 2'); +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-14-04 13:03:00', 'Here is my other post, 1'); +INSERT INTO peeps (time, contents, user_id) VALUES ('2023-15-04 13:03:00', 'User 3 newer post, 3'); + +``` +```bash +psql -h 127.0.0.1 your_database_name < seeds_{table_name}.sql +``` + +## 4. Implement the Model class + +```ruby +# Table name: user + +# Model class +# (in lib/user.rb) + +class User + attr_accessor :id, :email_address, :user_name, :password +end +``` + +## 5. Define the Repository Class interface +```ruby +# Table name: users + +# Repository class +# (in lib/user_repository.rb) + +class UserRepository + + def create(new_user) + encrypted_password = BCrypt::Password.create(new_user.password) + + sql = ' + INSERT INTO users (email_address, user_name, password) + VALUES($1, $2, $3); + ' + sql_params = [ + new_user.email_address, + new_user.user_name + encrypted_password + ] + + result_set = DatabaseConnection.exec_params(sql, sql_params) + + return new_user + end + + def find_by_email(email) + sql = 'SELECT id, email_address, user_name, password FROM users WHERE email_address = $1;' + result_set = DatabaseConnection.exec_params(sql, [email_address]) + end + + def create(student) + end +end +``` + +## 6. Write Test Examples + +```ruby +# EXAMPLES + +# 1 - Shows all users + +repo = UserRepository.new +users = repo.all +expect(users.length).to eq(3) +expect(users.first.user_name).to eq('sidra_fake') + +# 2 - Find a user by email +repo = UserRepository.new +user = repo.find('sidra@fake.com') + +expect(user.username).to eq('sidra_fake') + +# 3 - Create a new user + +repo = UserRepository.new + +new_user = User.new +new_user.email_address = 'angel@fake.com' +new_user.username = 'angel_fake' +new_user.password = 'password123' # set the plaintext password + +encrypted_password = BCrypt::Password.create(new_user.password) +new_user.password = encrypted_password # set the encrypted password + +repo.create(new_user) + + +``` + +Encode this example as a test. + +## 7. Reload the SQL seeds before each test run + +Running the SQL code present in the seed file will empty the table and re-insert the seed data. + +This is so you get a fresh table contents every time you run the test suite. + +```ruby +# file: spec/student_repository_spec.rb + +def reset_students_table + seed_sql = File.read('spec/seeds_students.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'students' }) + connection.exec(seed_sql) +end + +describe StudentRepository do + before(:each) do + reset_students_table + end +end +``` + +## 8. Test-drive and implement the Repository class behaviour + + diff --git a/route_recipes/homepage_route_recipe.md b/route_recipes/homepage_route_recipe.md new file mode 100644 index 0000000000..17fbdb41d9 --- /dev/null +++ b/route_recipes/homepage_route_recipe.md @@ -0,0 +1,172 @@ +# {{ GET }} {{ / }} Route Design Recipe + +## 1. Design the Route Signature + +You'll need to include: + * the HTTP method - GET + * the path - "/" + * any query parameters (passed in the URL) N/A + * or body parameters (passed in the request body) N/A + +## 2. Design the Response + +The route might return different responses, depending on the result. + +For example, a route for a specific blog post (by its ID) might return `200 OK` if the post exists, but `404 Not Found` if the post is not found in the database. + +Your response might return plain text, JSON, or HTML code. + +_Replace the below with your own design. Think of all the different possible responses your route will return._ + +```html + + + + + + + +Options
+ Log outOptions
+ Log in + Log in + +What do you want to say?
+