From b80e936b9980c7038aa14958d6e4cc05bfd05ad1 Mon Sep 17 00:00:00 2001 From: JRSkates Date: Wed, 10 May 2023 13:45:56 +0100 Subject: [PATCH 1/6] Chitter Project Setup --- Gemfile | 5 +++++ Gemfile.lock | 28 +++++++++++++++++++++++++++- app.rb | 14 ++++++++++++++ config.ru | 3 +++ spec/integration/app_spec.rb | 0 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 app.rb create mode 100644 config.ru create mode 100644 spec/integration/app_spec.rb diff --git a/Gemfile b/Gemfile index b1a320395a..866f834e6c 100644 --- a/Gemfile +++ b/Gemfile @@ -11,3 +11,8 @@ end group :development, :test do gem 'rubocop', '1.20' end + +gem "sinatra", "~> 3.0" +gem "sinatra-contrib", "~> 3.0" +gem "webrick", "~> 1.8" +gem "rack-test", "~> 2.1" diff --git a/Gemfile.lock b/Gemfile.lock index 66064703c7..898aa12beb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,9 +5,17 @@ GEM ast (2.4.2) diff-lcs (1.4.4) docile (1.4.0) + multi_json (1.15.0) + mustermann (3.0.0) + ruby2_keywords (~> 0.0.1) parallel (1.20.1) parser (3.0.2.0) ast (~> 2.4.1) + rack (2.2.7) + rack-protection (3.0.6) + rack + rack-test (2.1.0) + rack (>= 1.3) rainbow (3.0.0) regexp_parser (2.1.1) rexml (3.2.5) @@ -36,6 +44,7 @@ GEM rubocop-ast (1.11.0) parser (>= 3.0.1.1) ruby-progressbar (1.11.0) + ruby2_keywords (0.0.5) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -46,21 +55,38 @@ GEM terminal-table simplecov-html (0.12.3) simplecov_json_formatter (0.1.3) + sinatra (3.0.6) + mustermann (~> 3.0) + rack (~> 2.2, >= 2.2.4) + rack-protection (= 3.0.6) + tilt (~> 2.0) + sinatra-contrib (3.0.6) + multi_json + mustermann (~> 3.0) + rack-protection (= 3.0.6) + sinatra (= 3.0.6) + tilt (~> 2.0) terminal-table (3.0.1) unicode-display_width (>= 1.1.1, < 3) + tilt (2.1.0) unicode-display_width (2.0.0) + webrick (1.8.1) PLATFORMS ruby DEPENDENCIES + 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.12 diff --git a/app.rb b/app.rb new file mode 100644 index 0000000000..01ad12afe4 --- /dev/null +++ b/app.rb @@ -0,0 +1,14 @@ +require 'sinatra/base' +require 'sinatra/reloader' + +class Application < Sinatra::Base + # This allows the app code to refresh + # without having to restart the server. + configure :development do + register Sinatra::Reloader + end + + get '/' do + return 'Hello, world!' + end +end \ No newline at end of file diff --git a/config.ru b/config.ru new file mode 100644 index 0000000000..18af4f3388 --- /dev/null +++ b/config.ru @@ -0,0 +1,3 @@ +# file: config.ru +require './app' +run Application \ No newline at end of file diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb new file mode 100644 index 0000000000..e69de29bb2 From 2c216ad58e150f0b85f22552e2a7b0c4eed3f258 Mon Sep 17 00:00:00 2001 From: JRSkates Date: Wed, 10 May 2023 14:38:36 +0100 Subject: [PATCH 2/6] Table and Class Design Complete --- app.rb | 3 + chitter_model_and_repository_design_recipe.md | 263 ++++++++++++++++++ chitter_two_tables_design_recipe.md | 138 +++++++++ lib/database_connection.rb | 48 ++++ lib/peep.rb | 4 + lib/peep_repository.rb | 28 ++ lib/user.rb | 4 + lib/user_repository.rb | 18 ++ public/chit_chat.png | Bin 0 -> 73531 bytes public/style.css | 90 ++++++ spec/integration/integration_spec.rb | 0 spec/peep_repository_spec.rb | 13 + spec/peep_spec.rb | 1 + spec/peeps_users.sql | 18 ++ spec/seeds_chitter.sql | 11 + spec/user_repository_spec.rb | 14 + spec/user_spec.rb | 1 + 17 files changed, 654 insertions(+) create mode 100644 chitter_model_and_repository_design_recipe.md create mode 100644 chitter_two_tables_design_recipe.md create mode 100644 lib/database_connection.rb create mode 100644 lib/peep.rb create mode 100644 lib/peep_repository.rb create mode 100644 lib/user.rb create mode 100644 lib/user_repository.rb create mode 100644 public/chit_chat.png create mode 100644 public/style.css create mode 100644 spec/integration/integration_spec.rb create mode 100644 spec/peep_repository_spec.rb create mode 100644 spec/peep_spec.rb create mode 100644 spec/peeps_users.sql create mode 100644 spec/seeds_chitter.sql create mode 100644 spec/user_repository_spec.rb create mode 100644 spec/user_spec.rb diff --git a/app.rb b/app.rb index 01ad12afe4..64e990a576 100644 --- a/app.rb +++ b/app.rb @@ -1,5 +1,8 @@ require 'sinatra/base' require 'sinatra/reloader' +require_relative 'lib/database_connection' + +DatabaseConnection.connect class Application < Sinatra::Base # This allows the app code to refresh diff --git a/chitter_model_and_repository_design_recipe.md b/chitter_model_and_repository_design_recipe.md new file mode 100644 index 0000000000..376f850e5e --- /dev/null +++ b/chitter_model_and_repository_design_recipe.md @@ -0,0 +1,263 @@ +# Chitter Peeps & Users - Model and Repository Classes Design Recipe + + +## 1. Design and create the Table + + +``` +Table: peeps + +Columns: +id | content | time | user_id + +Table: users + +Columns: +id | name | email | username + +``` + +## 2. Create Test SQL seeds + +Your tests will depend on data stored in PostgreSQL to run. + +If seed data is provided (or you already created it), you can skip this step. + +```sql +-- (file: spec/seeds_chitter.sql) + +-- First, you'd need to truncate the table - this is so our table is emptied between each test run, +-- so we can start with a fresh state. +-- (RESTART IDENTITY resets the primary key) + +TRUNCATE TABLE peeps, users, peeps_users RESTART IDENTITY; + +-- Below this line there should only be `INSERT` statements. +-- Replace these statements with your own seed data. + +INSERT INTO users (name, email, username) VALUES ('Jack', 'jack@email.com', 'skates'); +INSERT INTO users (name, email, username) VALUES ('Dave', 'dave@email.com', 'dave123'); + + +INSERT INTO peeps (content, time, user_id) VALUES ('This is the first Peep', '20230506 12:22:09 PM', 1); +INSERT INTO peeps (content, time, user_id) VALUES ('This is the second Peep', '20230507 05:45:35 PM', 1); +INSERT INTO peeps (content, time, user_id) VALUES ('This is the third Peep', '20230508 09:42:01 AM', 2); +INSERT INTO peeps (content, time, user_id) VALUES ('This is the forth Peep', '20230509 11:12:59 PM', 2); + +``` + +Run this SQL file on the database to truncate (empty) the table, and insert the seed data. Be mindful of the fact any existing records in the table will be deleted. + +```bash +psql -h 127.0.0.1 chitter_test < spec/seeds_chitter.sql +``` + +## 3. Define the class names + +Usually, the Model class name will be the capitalised table name (single instead of plural). The same name is then suffixed by `Repository` for the Repository class name. + +```ruby +# Table name: peeps + +# Model class +# (in lib/peep.rb) +class Peep +end + +# Repository class +# (in lib/peep_repository.rb) +class PeepRepository +end + +# Table name: users + +# Model class +# (in lib/user.rb) +class User +end + +# Repository class +# (in lib/user_repository.rb) +class UserRepository +end +``` + +## 4. Implement the Model class + +Define the attributes of your Model class. You can usually map the table columns to the attributes of the class, including primary and foreign keys. + +```ruby + +# Table name: peeps + +# Model class +# (in lib/peep.rb) + +class Peep + # Replace the attributes by your own columns. + attr_accessor :id, :content, :time, :user_id +end + +# The keyword attr_accessor is a special Ruby feature +# which allows us to set and get attributes on an object, + +# Table name: users + +# Model class +# (in lib/user.rb) + +class User + # Replace the attributes by your own columns. + attr_accessor :id, :name, :email, :username +end + +``` + +*You may choose to test-drive this class, but unless it contains any more logic than the example above, it is probably not needed.* + +## 5. Define the Repository Class interface + +Your Repository class will need to implement methods for each "read" or "write" operation you'd like to run against the database. + +Using comments, define the method signatures (arguments and return value) and what they do - write up the SQL queries that will be used by each method. + +```ruby +# Table name: peeps + +# Repository class +# (in lib/peep_repository.rb) + +class PeepRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT id, content, time, user_id FROM peeps; + + # Returns an array of Peep objects. + end + + def create(peep) + # Executes the SQL query: + # INSERT INTO peeps (content, time, user_id) VALUES ($1, $2, $3); + + # returns nil + end + + def find_by_owner(user_id) + # Executes the SQL query: + # SELECT id, content, time, user_id FROM peeps WHERE user_id = $1; + + # Returns an array of Peep objects. + end + +end + +# Table name: users + +# Repository class +# (in lib/user_repository.rb) + +class UserRepository + + def find(username) + # Executes the SQL query: + # SELECT id, name, email, username FROM users WHERE username = $1; + + # Returns a User object. + end + + def create(user) + # Executes the SQL query: + # INSERT INTO users (name, email, username) VALUES ($1, $2, $3); + + # returns nil + end +end +``` + +## 6. Write Test Examples + +Write Ruby code that defines the expected behaviour of the Repository class, following your design from the table written in step 5. + +These examples will later be encoded as RSpec tests. + +```ruby +# Peeps testing +# 1 +# Get all peeps + + +# 2 +# creates a new peep + + +# 3 +# find all peeps by the same user + + +# 4 +# find all peeps that a user is tagged in + + + + +# User testing +# 1 +# find a user + + + +# 2 +# create a 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/peep_repository_spec.rb + +def reset_chitter_tables + seed_sql = File.read('spec/seeds_chitter.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) +end + +describe PeepRepository do + before(:each) do + reset_chitter_tables + end + + # (your tests will go here). +end + +# file: spec/user_repository_spec.rb + +def reset_chitter_tables + seed_sql = File.read('spec/seeds_chitter.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) +end + +describe UserRepository do + before(:each) do + reset_chitter_tables + end + + # (your tests will go here). +end +``` + +## 8. Test-drive and implement the Repository class behaviour + +_After each test you write, follow the test-driving process of red, green, refactor to implement the behaviour._ \ No newline at end of file diff --git a/chitter_two_tables_design_recipe.md b/chitter_two_tables_design_recipe.md new file mode 100644 index 0000000000..6b1e77dfab --- /dev/null +++ b/chitter_two_tables_design_recipe.md @@ -0,0 +1,138 @@ +# Chitter - Two Tables Design Recipe Template + +## 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 + +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 +``` + +``` +Nouns: + +peep, time, sign up, log in, log out, tagged, email +``` + +## 2. Infer the Table Name and Columns + +Put the different nouns in this table. Replace the example with your own nouns. + +| Record | Properties | +| --------------------- | ------------------ | +| peep | content, time, tag +| user | name, email, username, tag + + +1. Name of the first table (always plural): `peeps` + + Column names: `content`, `time`, `user_id` + +2. Name of the second table (always plural): `users` + + Column names: `name`, `email` , `username` + +## 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: peeps +id: SERIAL +content: text +time: datetime +user_id: int + +Table: users +id: SERIAL +name: text +email: text +username: text +``` + +## 4. Design the Many-to-Many relationship + +Make sure you can answer YES to these two questions: + +1. Can one [TABLE ONE] have many [TABLE TWO]? (Yes/No) +2. Can one [TABLE TWO] have many [TABLE ONE]? (Yes/No) + +``` +1. Can one peep have many users? NO +2. Can one user have many peeps? YES +``` + +_If you would answer "No" to one of these questions, you'll probably have to implement a One-to-Many relationship, which is simpler. Use the relevant design recipe in that case._ + + +## 4. Write the SQL. + +```sql +-- file: peeps_users.sql + +DROP TABLE peeps; +DROP TABLE users; + + +-- Create the first table. +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name text, + email text, + username text +); + +-- Create the second table. +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + content text, + time timestamp, + 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 < spec/peeps_users.sql +psql -h 127.0.0.1 chitter_test < spec/peeps_users.sql +``` \ No newline at end of file diff --git a/lib/database_connection.rb b/lib/database_connection.rb new file mode 100644 index 0000000000..bce8a207c2 --- /dev/null +++ b/lib/database_connection.rb @@ -0,0 +1,48 @@ +# file: lib/database_connection.rb + +require 'pg' + +# This class is a thin "wrapper" around the +# PG library. We'll use it in our project to interact +# with the database using SQL. + +class DatabaseConnection + # This method connects to PostgreSQL using the + # PG gem. We connect to 127.0.0.1, and select + # the database name given in argument. + + + def self.connect + # If the environment variable (set by Render) + # is present, use this to open the connection. + if ENV['DATABASE_URL'] != nil + @connection = PG.connect(ENV['DATABASE_URL']) + return + end + + if ENV['ENV'] == 'test' + database_name = 'chitter_test' + else + database_name = 'chitter' + end + @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + end + + # def self.connect(database_name) + # @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) + # end + + + + # This method executes an SQL query + # on the database, providing some optional parameters + # (you will learn a bit later about when to provide these parameters). + 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 \ No newline at end of file diff --git a/lib/peep.rb b/lib/peep.rb new file mode 100644 index 0000000000..1132b1deb4 --- /dev/null +++ b/lib/peep.rb @@ -0,0 +1,4 @@ +class Peep + + attr_accessor :id, :content, :time, :user_id +end \ No newline at end of file diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb new file mode 100644 index 0000000000..8e4d5bd36a --- /dev/null +++ b/lib/peep_repository.rb @@ -0,0 +1,28 @@ +require_relative 'peep' + +class PeepRepository + + # Selecting all records + # No arguments + def all + # Executes the SQL query: + # SELECT id, content, time, user_id FROM peeps; + + # Returns an array of Peep objects. + end + + def create(peep) + # Executes the SQL query: + # INSERT INTO peeps (content, time, user_id) VALUES ($1, $2, $3); + + # returns nil + end + + def find_by_owner(user_id) + # Executes the SQL query: + # SELECT id, content, time, user_id FROM peeps WHERE user_id = $1; + + # Returns an array of Peep objects. + end + +end \ No newline at end of file diff --git a/lib/user.rb b/lib/user.rb new file mode 100644 index 0000000000..d5f2f2f9e4 --- /dev/null +++ b/lib/user.rb @@ -0,0 +1,4 @@ +class User + + attr_accessor :id, :name, :email, :username +end \ No newline at end of file diff --git a/lib/user_repository.rb b/lib/user_repository.rb new file mode 100644 index 0000000000..14e2ce6d2e --- /dev/null +++ b/lib/user_repository.rb @@ -0,0 +1,18 @@ +require_relative 'user' + +class UserRepository + + def find(username) + # Executes the SQL query: + # SELECT id, name, email, username FROM users WHERE username = $1; + + # Returns a User object. + end + + def create(user) + # Executes the SQL query: + # INSERT INTO users (name, email, username) VALUES ($1, $2, $3); + + # returns nil + end +end \ No newline at end of file diff --git a/public/chit_chat.png b/public/chit_chat.png new file mode 100644 index 0000000000000000000000000000000000000000..03d14916edfdcb6c4385c3e58da9c690fb10a414 GIT binary patch literal 73531 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

l^+7>M8#J9!P0FMml@VFesD= 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 literal 0 HcmV?d00001 diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000000..6abc505d81 --- /dev/null +++ b/public/style.css @@ -0,0 +1,90 @@ +/* style.css */ + + +body { + font-family: Space Mono; + margin: 0; +} + +h1, +h2, +h3 { + color: #404042; + font-family: Space Mono; +} + +p { + color: #404042; + opacity: .7; +} + +.form { + border-style: solid; + padding: 10px; +} + +.container { + width: 800px; + margin: auto; +} + +.peep { + width: 600px; + margin: 20px auto; + border-style: solid; + padding: 5px; +} + +.banner { + background-image: linear-gradient(rgba(249, 153, 196, 0.4), rgba(246, 193, 233, 0.4)), url('/chit_chat.png'); + background-size: cover; + background-position: center; + padding: 20px 0; + text-align: center; +} + +.userbanner { + padding: 10px 0; + text-align: center; +} + +.banner h1 { + color: white; + font-size: 45px; + margin: 0; +} + +.banner h3 { + color: white; +} + +.banner p { + color: white; + font-size: 24px; +} + +.button-blue { + background-color: #1A48E3; + color: white; + padding: 16px 48px; + border-radius: 4px; + display: inline-block; + text-decoration: none; + transition: .3s; +} + +.button-blue:hover { + background-color: #0D1EC6; + color: white; +} + + +.footer { + background: #404042; + text-align: center; + padding: 24px 0; +} + +.footer p { + color: white; +} \ No newline at end of file diff --git a/spec/integration/integration_spec.rb b/spec/integration/integration_spec.rb new file mode 100644 index 0000000000..e69de29bb2 diff --git a/spec/peep_repository_spec.rb b/spec/peep_repository_spec.rb new file mode 100644 index 0000000000..f70fadd8ed --- /dev/null +++ b/spec/peep_repository_spec.rb @@ -0,0 +1,13 @@ +require 'peep_repository' + +RSpec.describe PeepRepository do + def reset_chitter_tables + seed_sql = File.read('spec/seeds_chitter.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) + end + + before(:each) do + reset_chitter_tables + end +end \ No newline at end of file diff --git a/spec/peep_spec.rb b/spec/peep_spec.rb new file mode 100644 index 0000000000..5e8b3fe242 --- /dev/null +++ b/spec/peep_spec.rb @@ -0,0 +1 @@ +require 'peep' \ No newline at end of file diff --git a/spec/peeps_users.sql b/spec/peeps_users.sql new file mode 100644 index 0000000000..da7e8b2e5f --- /dev/null +++ b/spec/peeps_users.sql @@ -0,0 +1,18 @@ +DROP TABLE peeps; +DROP TABLE users; + + +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + name text, + email text, + username text +); + +CREATE TABLE peeps ( + id SERIAL PRIMARY KEY, + content text, + time timestamp, + user_id int, + constraint fk_user foreign key(user_id) references users(id) on delete cascade +); \ No newline at end of file diff --git a/spec/seeds_chitter.sql b/spec/seeds_chitter.sql new file mode 100644 index 0000000000..e88df3bcc1 --- /dev/null +++ b/spec/seeds_chitter.sql @@ -0,0 +1,11 @@ +TRUNCATE TABLE peeps, users, peeps_users RESTART IDENTITY; + + +INSERT INTO users (name, email, username) VALUES ('Jack', 'jack@email.com', 'skates'); +INSERT INTO users (name, email, username) VALUES ('Dave', 'dave@email.com', 'dave123'); + + +INSERT INTO peeps (content, time, user_id) VALUES ('This is the first Peep', '20230506 12:22:09 PM', 1); +INSERT INTO peeps (content, time, user_id) VALUES ('This is the second Peep', '20230507 05:45:35 PM', 1); +INSERT INTO peeps (content, time, user_id) VALUES ('This is the third Peep', '20230508 09:42:01 AM', 2); +INSERT INTO peeps (content, time, user_id) VALUES ('This is the forth Peep', '20230509 11:12:59 PM', 2); diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb new file mode 100644 index 0000000000..f1d54f9010 --- /dev/null +++ b/spec/user_repository_spec.rb @@ -0,0 +1,14 @@ +require 'user_repository' + +RSpec.describe UserRepository do + def reset_chitter_tables + seed_sql = File.read('spec/seeds_chitter.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) + end + + before(:each) do + reset_chitter_tables + end + +end \ No newline at end of file diff --git a/spec/user_spec.rb b/spec/user_spec.rb new file mode 100644 index 0000000000..2d18e01699 --- /dev/null +++ b/spec/user_spec.rb @@ -0,0 +1 @@ +require 'user' \ No newline at end of file From 45ccc3759c9543bed379fc088cfe8b36a1020da9 Mon Sep 17 00:00:00 2001 From: JRSkates Date: Wed, 10 May 2023 16:28:36 +0100 Subject: [PATCH 3/6] Backend Complete, app.rb WIP --- app.rb | 16 ++++- chitter_model_and_repository_design_recipe.md | 48 ++++++++++++--- chitter_two_tables_design_recipe.md | 8 +-- config.ru | 2 +- lib/database_connection.rb | 5 +- lib/peep.rb | 4 +- lib/peep_repository.rb | 47 +++++++++----- lib/user.rb | 4 +- lib/user_repository.rb | 40 +++++++++--- spec/peep_repository_spec.rb | 46 +++++++++++++- spec/peep_spec.rb | 1 - spec/seeds_chitter.sql | 2 +- spec/spec_helper.rb | 4 ++ spec/user_repository_spec.rb | 44 ++++++++++++- spec/user_spec.rb | 1 - views/index.erb | 61 +++++++++++++++++++ views/peeps.erb | 33 ++++++++++ 17 files changed, 316 insertions(+), 50 deletions(-) delete mode 100644 spec/peep_spec.rb delete mode 100644 spec/user_spec.rb create mode 100644 views/index.erb create mode 100644 views/peeps.erb diff --git a/app.rb b/app.rb index 64e990a576..1bc93bbedf 100644 --- a/app.rb +++ b/app.rb @@ -1,6 +1,8 @@ require 'sinatra/base' require 'sinatra/reloader' require_relative 'lib/database_connection' +require_relative 'lib/user_repository' +require_relative 'lib/peep_repository' DatabaseConnection.connect @@ -9,9 +11,19 @@ class Application < Sinatra::Base # without having to restart the server. configure :development do register Sinatra::Reloader + also_reload 'lib/user_repository' + also_reload 'lib/peep_repository' end get '/' do - return 'Hello, world!' + return erb(:index) end -end \ No newline at end of file + + get '/peeps' do + peep_repo = PeepRepository.new + users = UserRepository.new + @user = users.find('skates').username + @peeps = peep_repo.all.sort_by!{|peep| peep.time}.reverse! + return erb(:peeps) + end +end diff --git a/chitter_model_and_repository_design_recipe.md b/chitter_model_and_repository_design_recipe.md index 376f850e5e..1bed67b0ca 100644 --- a/chitter_model_and_repository_design_recipe.md +++ b/chitter_model_and_repository_design_recipe.md @@ -187,32 +187,64 @@ These examples will later be encoded as RSpec tests. # Peeps testing # 1 # Get all peeps +repo = PeepRepository.new +peeps = repo.all +expect(peeps.length).to eq 4 +expect(peeps.first.content).to eq 'This is the first Peep' +expect(peeps.first.time).to eq '2023-05-06 12:22:09' +expect(peeps.first.user_id).to eq 1 # 2 # creates a new peep +repo = PeepRepository.new +peep = Peep.new +peep.content = 'Jack here with a third peep!' +peep.time = '2023-05-10 14:56:09' +peep.user_id = '1' -# 3 -# find all peeps by the same user +repo.create(peep) +peeps = repo.all +last_peep = peeps.last -# 4 -# find all peeps that a user is tagged in - +expect(last_peep.content).to eq 'Jack here with a third peep!' +expect(last_peep.time).to eq '2023-05-10 14:56:09' +expect(last_peep.user_id).to eq '1' +# 3 +# find all peeps by the same user +repo = PeepRepository.new +jack_peeps = repo.find_by_owner(1) +expect(jack_peeps.length).to eq 2 +expect(jack_peeps.first.content).to eq 'This is the first Peep' +expect(jack_peeps.first.time).to eq '2023-05-06 12:22:09' +expect(jack_peeps[1].content).to eq 'This is the second Peep' # User testing # 1 -# find a user +# get all users +repo = UserRepository.new +users = repo.all +expect(users.length).to eq 2 +expect(users.first.name).to eq 'Jack' +expect(users.first.email).to eq 'jack@email.com' +expect(users.first.username).to eq 'skates' # 2 -# create a new user - +# find a user with the username +repo = UserRepository.new +user = repo.find('skates') +expect(user.username).to eq 'skates' +expect(user.name).to eq 'Jack' +expect(user.email).to eq 'jack@email.com' +# 3 +# create a new user and add it to the database ``` diff --git a/chitter_two_tables_design_recipe.md b/chitter_two_tables_design_recipe.md index 6b1e77dfab..82a288a86e 100644 --- a/chitter_two_tables_design_recipe.md +++ b/chitter_two_tables_design_recipe.md @@ -48,10 +48,10 @@ peep, time, sign up, log in, log out, tagged, email Put the different nouns in this table. Replace the example with your own nouns. -| Record | Properties | -| --------------------- | ------------------ | -| peep | content, time, tag -| user | name, email, username, tag +| Record | Properties | +| --------------------- | ---------------------- | +| peep | content, time, user_id +| user | name, email, username 1. Name of the first table (always plural): `peeps` diff --git a/config.ru b/config.ru index 18af4f3388..c41dba5056 100644 --- a/config.ru +++ b/config.ru @@ -1,3 +1,3 @@ # file: config.ru require './app' -run Application \ No newline at end of file +run Application diff --git a/lib/database_connection.rb b/lib/database_connection.rb index bce8a207c2..a5e31564a6 100644 --- a/lib/database_connection.rb +++ b/lib/database_connection.rb @@ -11,7 +11,6 @@ class DatabaseConnection # PG gem. We connect to 127.0.0.1, and select # the database name given in argument. - def self.connect # If the environment variable (set by Render) # is present, use this to open the connection. @@ -32,8 +31,6 @@ def self.connect # @connection = PG.connect({ host: '127.0.0.1', dbname: database_name }) # end - - # This method executes an SQL query # on the database, providing some optional parameters # (you will learn a bit later about when to provide these parameters). @@ -45,4 +42,4 @@ def self.exec_params(query, params) end @connection.exec_params(query, params) end -end \ No newline at end of file +end diff --git a/lib/peep.rb b/lib/peep.rb index 1132b1deb4..1a733a67ec 100644 --- a/lib/peep.rb +++ b/lib/peep.rb @@ -1,4 +1,4 @@ class Peep - attr_accessor :id, :content, :time, :user_id -end \ No newline at end of file + attr_accessor :id, :content, :time, :user_id +end diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb index 8e4d5bd36a..56b7d697c0 100644 --- a/lib/peep_repository.rb +++ b/lib/peep_repository.rb @@ -1,28 +1,45 @@ require_relative 'peep' class PeepRepository - - # Selecting all records - # No arguments def all - # Executes the SQL query: - # SELECT id, content, time, user_id FROM peeps; - - # Returns an array of Peep objects. + sql = "SELECT id, content, time, user_id FROM peeps;" + result = DatabaseConnection.exec_params(sql, []) + peeps_array = [] + result.each do |row| + peep = Peep.new + peep.id = row['id'] + peep.content = row['content'] + peep.time = row['time'] + peep.user_id = row['user_id'] + peeps_array << peep + end + return peeps_array end def create(peep) - # Executes the SQL query: - # INSERT INTO peeps (content, time, user_id) VALUES ($1, $2, $3); - + sql = 'INSERT INTO peeps (content, time, user_id) VALUES ($1, $2, $3);' + params = [peep.content, peep.time, peep.user_id] + + DatabaseConnection.exec_params(sql, params) + + return nil # returns nil end def find_by_owner(user_id) - # Executes the SQL query: - # SELECT id, content, time, user_id FROM peeps WHERE user_id = $1; - - # Returns an array of Peep objects. + sql = 'SELECT id, content, time, user_id FROM peeps WHERE user_id = $1;' + param = [user_id] + result = DatabaseConnection.exec_params(sql, param) + peeps_array = [] + result.each do |row| + peep = Peep.new + peep.id = row['id'] + peep.content = row['content'] + peep.time = row['time'] + peep.user_id = row['user_id'] + peeps_array << peep + end + return peeps_array end -end \ No newline at end of file +end diff --git a/lib/user.rb b/lib/user.rb index d5f2f2f9e4..893aeae5e9 100644 --- a/lib/user.rb +++ b/lib/user.rb @@ -1,4 +1,4 @@ class User - attr_accessor :id, :name, :email, :username -end \ No newline at end of file + attr_accessor :id, :name, :email, :username +end diff --git a/lib/user_repository.rb b/lib/user_repository.rb index 14e2ce6d2e..10367bff17 100644 --- a/lib/user_repository.rb +++ b/lib/user_repository.rb @@ -1,18 +1,44 @@ require_relative 'user' class UserRepository + def all + sql = 'SELECT id, name, email, username FROM users;' + result = DatabaseConnection.exec_params(sql, []) + user_array = [] + result.each do |row| + user = User.new + user.id = row['id'] + user.name = row['name'] + user.email = row['email'] + user.username = row['username'] + + user_array << user + end + + return user_array + end def find(username) - # Executes the SQL query: - # SELECT id, name, email, username FROM users WHERE username = $1; - - # Returns a User object. + sql = 'SELECT id, name, email, username FROM users WHERE username = $1;' + param = [username] + result = DatabaseConnection.exec_params(sql, param) + record = result.first + user = User.new + user.id = record['id'] + user.name = record['name'] + user.email = record['email'] + user.username = record['username'] + + return user end def create(user) # Executes the SQL query: - # INSERT INTO users (name, email, username) VALUES ($1, $2, $3); + sql = 'INSERT INTO users (name, email, username) VALUES ($1, $2, $3);' + params = [user.name, user.email, user.username] + + DatabaseConnection.exec_params(sql, params) - # returns nil + return nil end -end \ No newline at end of file +end diff --git a/spec/peep_repository_spec.rb b/spec/peep_repository_spec.rb index f70fadd8ed..c8f351d316 100644 --- a/spec/peep_repository_spec.rb +++ b/spec/peep_repository_spec.rb @@ -10,4 +10,48 @@ def reset_chitter_tables before(:each) do reset_chitter_tables end -end \ No newline at end of file + + context 'The All Method' do + it 'returns all the peeps' do + repo = PeepRepository.new + peeps = repo.all + + expect(peeps.length).to eq 4 + expect(peeps.first.content).to eq 'This is the first Peep' + expect(peeps.first.time).to eq '2023-05-06 12:22:09' + expect(peeps.first.user_id).to eq '1' + end + end + + context 'The Create Method' do + it 'creates a new peep and adds it too the database' do + repo = PeepRepository.new + + peep = Peep.new + peep.content = 'Jack here with a third peep!' + peep.time = '2023-05-10 14:56:09' + peep.user_id = '1' + + repo.create(peep) + + peeps = repo.all + last_peep = peeps.last + + expect(last_peep.content).to eq 'Jack here with a third peep!' + expect(last_peep.time).to eq '2023-05-10 14:56:09' + expect(last_peep.user_id).to eq '1' + end + end + + context 'The Find By Owner Method' do + it 'returns an array of peeps from the user_id provided' do + repo = PeepRepository.new + jack_peeps = repo.find_by_owner(1) + + expect(jack_peeps.length).to eq 2 + expect(jack_peeps.first.content).to eq 'This is the first Peep' + expect(jack_peeps.first.time).to eq '2023-05-06 12:22:09' + expect(jack_peeps[1].content).to eq 'This is the second Peep' + end + end +end diff --git a/spec/peep_spec.rb b/spec/peep_spec.rb deleted file mode 100644 index 5e8b3fe242..0000000000 --- a/spec/peep_spec.rb +++ /dev/null @@ -1 +0,0 @@ -require 'peep' \ No newline at end of file diff --git a/spec/seeds_chitter.sql b/spec/seeds_chitter.sql index e88df3bcc1..b00bc80f2e 100644 --- a/spec/seeds_chitter.sql +++ b/spec/seeds_chitter.sql @@ -1,4 +1,4 @@ -TRUNCATE TABLE peeps, users, peeps_users RESTART IDENTITY; +TRUNCATE TABLE peeps, users RESTART IDENTITY; INSERT INTO users (name, email, username) VALUES ('Jack', 'jack@email.com', 'skates'); diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 252747d899..1bd041eb30 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,5 +1,9 @@ require 'simplecov' require 'simplecov-console' +require 'database_connection' +ENV['ENV'] = 'test' + +DatabaseConnection.connect SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([ SimpleCov::Formatter::Console, diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb index f1d54f9010..46f5dc6cb9 100644 --- a/spec/user_repository_spec.rb +++ b/spec/user_repository_spec.rb @@ -11,4 +11,46 @@ def reset_chitter_tables reset_chitter_tables end -end \ No newline at end of file + context 'The All Method' do + it 'returns all the users' do + repo = UserRepository.new + users = repo.all + + expect(users.length).to eq 2 + expect(users.first.name).to eq 'Jack' + expect(users.first.email).to eq 'jack@email.com' + expect(users.first.username).to eq 'skates' + end + end + + context 'The Find Method' do + it 'should return a user with the username as an input' do + repo = UserRepository.new + user = repo.find('skates') + + expect(user.username).to eq 'skates' + expect(user.name).to eq 'Jack' + expect(user.email).to eq 'jack@email.com' + end + end + + context 'The Create Method' do + it 'should create a new user and add it to the database' do + repo = UserRepository.new + + user = User.new + user.name = 'Charlie' + user.email = 'charliekelly@paddyspub.com' + user.username = 'TheDayman' + + repo.create(user) + users = repo.all + new_user = users.last + + expect(new_user.name).to eq 'Charlie' + expect(new_user.email).to eq 'charliekelly@paddyspub.com' + expect(new_user.username).to eq 'TheDayman' + expect(new_user.id).to eq '3' + end + end +end diff --git a/spec/user_spec.rb b/spec/user_spec.rb deleted file mode 100644 index 2d18e01699..0000000000 --- a/spec/user_spec.rb +++ /dev/null @@ -1 +0,0 @@ -require 'user' \ No newline at end of file diff --git a/views/index.erb b/views/index.erb new file mode 100644 index 0000000000..acd43cdaae --- /dev/null +++ b/views/index.erb @@ -0,0 +1,61 @@ + + + + + + Chitter - The ChitChat App + + + + + + + + + +

+ + + + +
+

Log in to Chitter

+
+
+
+ + +
+
+ + +
+ + +
+
+ +
+ +
+

Latest Peeps

+
+ +
+ + View all peeps + +
+ + + + + + \ No newline at end of file diff --git a/views/peeps.erb b/views/peeps.erb new file mode 100644 index 0000000000..547848d087 --- /dev/null +++ b/views/peeps.erb @@ -0,0 +1,33 @@ + + + + + + Chitter - Peeps + + + + + + + + + + +
+

All Peeps

+ <% @peeps.each do |peep| %> + +

<%= @user%> +

<%= peep.content%>

+

<%= peep.time%>

+ <%end%> +
+ + + + + + \ No newline at end of file From db8b32c2cf002ab06e02a89560852a17155626bd Mon Sep 17 00:00:00 2001 From: JRSkates Date: Thu, 11 May 2023 14:57:45 +0100 Subject: [PATCH 4/6] Homepage, Read all Peeps & SignUp Complete --- README.md | 131 ++++----------------------- app.rb | 112 ++++++++++++++++++++++- lib/peep_repository.rb | 16 +--- lib/user.rb | 5 +- lib/user_repository.rb | 40 ++++++-- spec/integration/integration_spec.rb | 29 ++++++ spec/peeps_users.sql | 3 +- spec/seeds_chitter.sql | 4 +- spec/user_repository_spec.rb | 7 ++ views/index.erb | 10 +- views/logged_in.erb | 24 +++++ views/login.erb | 1 + views/peeps.erb | 16 +++- views/signup.erb | 57 ++++++++++++ 14 files changed, 311 insertions(+), 144 deletions(-) create mode 100644 views/logged_in.erb create mode 100644 views/login.erb create mode 100644 views/signup.erb diff --git a/README.md b/README.md index 465eda879b..192f99e7cf 100644 --- a/README.md +++ b/README.md @@ -1,123 +1,30 @@ -Chitter Challenge -================= +# 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 +A Twitter clone build as part of the Makers Academy development course -Challenge: -------- - -As usual please start by forking this repo. - -We are going to write a small Twitter clone that will allow the users to post messages to a public stream. - -Features: -------- +## How to use +```shell +bundle install +rackup ``` -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: ------ - -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. -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). +## Pending TODOs +- Login and Profile ID functionality +- Post Peeps -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/) +## Built with +- Ruby +- Rspec +- Sinatra +- PostgreSQL +- Render -**ActiveRecord** -- [ActiveRecord ORM](https://guides.rubyonrails.org/active_record_basics.html) -- [Sinatra & ActiveRecord setup](https://learn.co/lessons/sinatra-activerecord-setup) +## Contributions -Notes on functionality: ------- +CSS Style sheet from [Caroline Evans](https://github.com/cvass1) -* You don't have to be logged in to see the peeps. -* 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 ------------ - -In code review we'll be hoping to see: - -* 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. - -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. - -Notes on test coverage ----------------------- - -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: - -```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 -``` +## Contact +- [Jack Skates](mailto:skatesproduction@gmail.com) -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 index 1bc93bbedf..265eb92e0c 100644 --- a/app.rb +++ b/app.rb @@ -7,6 +7,7 @@ DatabaseConnection.connect class Application < Sinatra::Base + enable :sessions # This allows the app code to refresh # without having to restart the server. configure :development do @@ -16,14 +17,119 @@ class Application < Sinatra::Base end get '/' do + if session[:user_id] != nil + return redirect('/userpage') + end + peep_repo = PeepRepository.new + @users = UserRepository.new + @peeps = peep_repo.all.sort_by!{ |peep| peep.time }.reverse! return erb(:index) end + post '/logged_in' do + + #if invalid_user_params? then + # status 400 + # return 'Invalid user details.' + #end + + user_repo = UserRepository.new + + if !user_repo.all.any?{|user| user.username == params[:username]} then + status 400 + return "Username does not exist, please try again." + end + + username = params[:username] + password = params[:password] + + user = user_repo.find(username) + + if user.password == password + # Set the user ID in session + session[:user_id] = user.id + + redirect "/logged_in" + + + else + status 400 + return 'Invalid user details.' + end + end + + get '/logged_in' do + if session[:user_id] == nil + # No user id in the session + return redirect('/') + else + + user_id = session[:user_id] + + @user_repo = UserRepository.new + peep_repo = PeepRepository.new + + @user = @user_repo.find(user_id) + @peeps = peep_repo.find_by_owner(@user.id).sort_by!{|peep| peep.time}.reverse! + + return erb(:logged_in) + end + + end + get '/peeps' do peep_repo = PeepRepository.new - users = UserRepository.new - @user = users.find('skates').username - @peeps = peep_repo.all.sort_by!{|peep| peep.time}.reverse! + @users = UserRepository.new + @peeps = peep_repo.all.sort_by!{ |peep| peep.time }.reverse! return erb(:peeps) end + + get '/signup' do + return erb(:signup) + end + + post '/signup' do + + users = UserRepository.new + + if users.all.any?{|user| user.email == params[:email]} then + status 400 + return "Email address already signed up." + end + + if users.all.any?{|user| user.username == params[:username]} then + status 400 + return "Username already taken, please choose another." + end + + if invalid_signup_params? || invalid_user_params? then + status 400 + return 'Invalid credentials, please try again.' + end + + user = User.new + user.name = params[:name] + user.email = params[:email] + user.username = params[:username] + user.password = params[:password] + + users.create(user) + + user = users.find_by_username(user.username) + session[:user_id] = user.id + + redirect "/logged_in" + + end + + private + def invalid_user_params? + params[:username] == nil || + params[:password] == nil || params[:password] == "" + end + + def invalid_peep_params? + params[:content] == nil || params[:content]== "" + end + end diff --git a/lib/peep_repository.rb b/lib/peep_repository.rb index 56b7d697c0..875a27349e 100644 --- a/lib/peep_repository.rb +++ b/lib/peep_repository.rb @@ -7,10 +7,8 @@ def all peeps_array = [] result.each do |row| peep = Peep.new - peep.id = row['id'] - peep.content = row['content'] - peep.time = row['time'] - peep.user_id = row['user_id'] + peep.id, peep.content, peep.time, peep.user_id = + row['id'], row['content'], row['time'], row['user_id'] peeps_array << peep end return peeps_array @@ -23,20 +21,16 @@ def create(peep) DatabaseConnection.exec_params(sql, params) return nil - # returns nil end def find_by_owner(user_id) sql = 'SELECT id, content, time, user_id FROM peeps WHERE user_id = $1;' - param = [user_id] - result = DatabaseConnection.exec_params(sql, param) + result = DatabaseConnection.exec_params(sql, [user_id]) peeps_array = [] result.each do |row| peep = Peep.new - peep.id = row['id'] - peep.content = row['content'] - peep.time = row['time'] - peep.user_id = row['user_id'] + peep.id, peep.content, peep.time, peep.user_id = + row['id'], row['content'], row['time'], row['user_id'] peeps_array << peep end return peeps_array diff --git a/lib/user.rb b/lib/user.rb index 893aeae5e9..3c7ddf17b7 100644 --- a/lib/user.rb +++ b/lib/user.rb @@ -1,4 +1,7 @@ +require 'bcrypt' class User - attr_accessor :id, :name, :email, :username + attr_accessor :id, :name, :email, :username, :password + include BCrypt + end diff --git a/lib/user_repository.rb b/lib/user_repository.rb index 10367bff17..38d8e6ae2e 100644 --- a/lib/user_repository.rb +++ b/lib/user_repository.rb @@ -1,4 +1,5 @@ require_relative 'user' +require 'bcrypt' class UserRepository def all @@ -7,14 +8,10 @@ def all user_array = [] result.each do |row| user = User.new - user.id = row['id'] - user.name = row['name'] - user.email = row['email'] - user.username = row['username'] - + user.id, user.name, user.email, user.username = + row['id'], row['name'], row['email'], row['username'] user_array << user end - return user_array end @@ -28,17 +25,42 @@ def find(username) user.name = record['name'] user.email = record['email'] user.username = record['username'] - return user end def create(user) # Executes the SQL query: - sql = 'INSERT INTO users (name, email, username) VALUES ($1, $2, $3);' - params = [user.name, user.email, user.username] + encrypted_password = BCrypt::Password.create(user.password) + sql = 'INSERT INTO users (name, email, username, password) VALUES ($1, $2, $3, $4);' + params = [user.name, user.email, user.username, encrypted_password] DatabaseConnection.exec_params(sql, params) return nil end + + def find_by_id(id) + sql = 'SELECT id, name, email, username FROM users WHERE id = $1;' + result = DatabaseConnection.exec_params(sql, [id]) + record = result.first + user = User.new + user.id = record['id'] + user.name = record['name'] + user.email = record['email'] + user.username = record['username'] + return user.username + end + + def find_by_username(username) + sql = 'SELECT id, name, email, username, password FROM users WHERE username = $1;' + param = [username] + result = DatabaseConnection.exec_params(sql, param) + record = result.first + user = User.new + user.id = record['id'] + user.name = record['name'] + user.email = record['email'] + user.username = record['username'] + return user + end end diff --git a/spec/integration/integration_spec.rb b/spec/integration/integration_spec.rb index e69de29bb2..4f67aed88d 100644 --- a/spec/integration/integration_spec.rb +++ b/spec/integration/integration_spec.rb @@ -0,0 +1,29 @@ +require 'user_repository' +require 'peep_repository' + +RSpec.describe 'user_integration' do + def reset_chitter_tables + seed_sql = File.read('spec/seeds_chitter.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) + end + + before(:each) do + reset_chitter_tables + end + + context 'The Find By ID method' do + it 'should return the username associated with the peep' do + peep_repo = PeepRepository.new + peep = Peep.new + peep.content = 'Jack here with a third peep!' + peep.time = '2023-05-10 14:56:09' + peep.user_id = '1' + + peep_repo.create(peep) + user_repo = UserRepository.new + user = user_repo.find_by_id(peep.user_id) + expect(user).to eq 'skates' + end + end +end \ No newline at end of file diff --git a/spec/peeps_users.sql b/spec/peeps_users.sql index da7e8b2e5f..00d8e66d67 100644 --- a/spec/peeps_users.sql +++ b/spec/peeps_users.sql @@ -6,7 +6,8 @@ CREATE TABLE users ( id SERIAL PRIMARY KEY, name text, email text, - username text + username text, + password text ); CREATE TABLE peeps ( diff --git a/spec/seeds_chitter.sql b/spec/seeds_chitter.sql index b00bc80f2e..44e2600109 100644 --- a/spec/seeds_chitter.sql +++ b/spec/seeds_chitter.sql @@ -1,8 +1,8 @@ TRUNCATE TABLE peeps, users RESTART IDENTITY; -INSERT INTO users (name, email, username) VALUES ('Jack', 'jack@email.com', 'skates'); -INSERT INTO users (name, email, username) VALUES ('Dave', 'dave@email.com', 'dave123'); +INSERT INTO users (name, email, username, password) VALUES ('Jack', 'jack@email.com', 'skates', '123321'); +INSERT INTO users (name, email, username, password) VALUES ('Dave', 'dave@email.com', 'dave123', 'testtest'); INSERT INTO peeps (content, time, user_id) VALUES ('This is the first Peep', '20230506 12:22:09 PM', 1); diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb index 46f5dc6cb9..e589222d81 100644 --- a/spec/user_repository_spec.rb +++ b/spec/user_repository_spec.rb @@ -42,6 +42,7 @@ def reset_chitter_tables user.name = 'Charlie' user.email = 'charliekelly@paddyspub.com' user.username = 'TheDayman' + user.password = BCrypt::Password.create('fightmilk') repo.create(user) users = repo.all @@ -53,4 +54,10 @@ def reset_chitter_tables expect(new_user.id).to eq '3' end end + + context "The Find by Username Method" do + it 'returns the correct user object with the username as the argument' do + + end + end end diff --git a/views/index.erb b/views/index.erb index acd43cdaae..54cc297f9a 100644 --- a/views/index.erb +++ b/views/index.erb @@ -26,7 +26,7 @@

Log in to Chitter

-
+
@@ -45,7 +45,13 @@

Latest Peeps

- + <% @peeps.each do |peep| %> +
+

<%=@users.find_by_id(peep.user_id)%>

+

<%= peep.content%>

+

<%= peep.time%>

+ <%end%> +
View all peeps diff --git a/views/logged_in.erb b/views/logged_in.erb new file mode 100644 index 0000000000..046c452ec4 --- /dev/null +++ b/views/logged_in.erb @@ -0,0 +1,24 @@ + + + + + + Chitter - User Homepage + + + + + + + + + + +
+

Hello, <%=@user.name%>!

+

Welcome to your chitter homepage

+ + + + +
\ No newline at end of file diff --git a/views/login.erb b/views/login.erb new file mode 100644 index 0000000000..95d09f2b10 --- /dev/null +++ b/views/login.erb @@ -0,0 +1 @@ +hello world \ No newline at end of file diff --git a/views/peeps.erb b/views/peeps.erb index 547848d087..d11b0cbd35 100644 --- a/views/peeps.erb +++ b/views/peeps.erb @@ -11,17 +11,27 @@ - + diff --git a/views/signup.erb b/views/signup.erb new file mode 100644 index 0000000000..783b93501c --- /dev/null +++ b/views/signup.erb @@ -0,0 +1,57 @@ + + + + + + Chitter - Sign Up + + + + + + + + + + +
+

Sign up to Chitter!

+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
+ +
+ + + + +
+ + + + + \ No newline at end of file From e318bddf6b93eab09e4fb28934c0336173bd28f6 Mon Sep 17 00:00:00 2001 From: JRSkates Date: Thu, 11 May 2023 16:32:25 +0100 Subject: [PATCH 5/6] Integration Tests added --- app.rb | 24 +++++++++++-------- lib/user.rb | 3 +-- lib/user_repository.rb | 11 ++++----- spec/integration/app_spec.rb | 46 ++++++++++++++++++++++++++++++++++++ spec/user_repository_spec.rb | 10 +++++--- 5 files changed, 73 insertions(+), 21 deletions(-) diff --git a/app.rb b/app.rb index 265eb92e0c..f745a629a2 100644 --- a/app.rb +++ b/app.rb @@ -43,12 +43,11 @@ class Application < Sinatra::Base username = params[:username] password = params[:password] - user = user_repo.find(username) + user = user_repo.find_by_username(username) if user.password == password # Set the user ID in session session[:user_id] = user.id - redirect "/logged_in" @@ -58,11 +57,12 @@ class Application < Sinatra::Base end end + get '/logged_in' do - if session[:user_id] == nil + #if session[:user_id] == nil # No user id in the session - return redirect('/') - else + #return redirect('/') + #else user_id = session[:user_id] @@ -70,10 +70,9 @@ class Application < Sinatra::Base peep_repo = PeepRepository.new @user = @user_repo.find(user_id) - @peeps = peep_repo.find_by_owner(@user.id).sort_by!{|peep| peep.time}.reverse! return erb(:logged_in) - end + #end end @@ -91,7 +90,7 @@ class Application < Sinatra::Base post '/signup' do users = UserRepository.new - +=begin if users.all.any?{|user| user.email == params[:email]} then status 400 return "Email address already signed up." @@ -106,12 +105,15 @@ class Application < Sinatra::Base status 400 return 'Invalid credentials, please try again.' end - +=end user = User.new + last_user = users.all.last + user_id = (last_user.id) - 1 user.name = params[:name] user.email = params[:email] user.username = params[:username] user.password = params[:password] + session[:user_id] = user.id users.create(user) @@ -121,7 +123,8 @@ class Application < Sinatra::Base redirect "/logged_in" end - +end +=begin private def invalid_user_params? params[:username] == nil || @@ -133,3 +136,4 @@ def invalid_peep_params? end end +=end diff --git a/lib/user.rb b/lib/user.rb index 3c7ddf17b7..427bbae7e9 100644 --- a/lib/user.rb +++ b/lib/user.rb @@ -1,7 +1,6 @@ -require 'bcrypt' + class User attr_accessor :id, :name, :email, :username, :password - include BCrypt end diff --git a/lib/user_repository.rb b/lib/user_repository.rb index 38d8e6ae2e..d2ca53f3c6 100644 --- a/lib/user_repository.rb +++ b/lib/user_repository.rb @@ -9,7 +9,7 @@ def all result.each do |row| user = User.new user.id, user.name, user.email, user.username = - row['id'], row['name'], row['email'], row['username'] + row['id'].to_i, row['name'], row['email'], row['username'] user_array << user end return user_array @@ -21,7 +21,7 @@ def find(username) result = DatabaseConnection.exec_params(sql, param) record = result.first user = User.new - user.id = record['id'] + user.id = record['id'].to_i user.name = record['name'] user.email = record['email'] user.username = record['username'] @@ -30,9 +30,8 @@ def find(username) def create(user) # Executes the SQL query: - encrypted_password = BCrypt::Password.create(user.password) sql = 'INSERT INTO users (name, email, username, password) VALUES ($1, $2, $3, $4);' - params = [user.name, user.email, user.username, encrypted_password] + params = [user.name, user.email, user.username, user.password] DatabaseConnection.exec_params(sql, params) @@ -44,7 +43,7 @@ def find_by_id(id) result = DatabaseConnection.exec_params(sql, [id]) record = result.first user = User.new - user.id = record['id'] + user.id = record['id'].to_i user.name = record['name'] user.email = record['email'] user.username = record['username'] @@ -57,7 +56,7 @@ def find_by_username(username) result = DatabaseConnection.exec_params(sql, param) record = result.first user = User.new - user.id = record['id'] + user.id = record['id'].to_i user.name = record['name'] user.email = record['email'] user.username = record['username'] diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb index e69de29bb2..efb0d49773 100644 --- a/spec/integration/app_spec.rb +++ b/spec/integration/app_spec.rb @@ -0,0 +1,46 @@ +require_relative '../../app' +require 'spec_helper' +require 'rack/test' + +RSpec.describe Application do + include Rack::Test::Methods + + let(:app) {Application.new} + + def reset_chitter_tables + seed_sql = File.read('spec/seeds_chitter.sql') + connection = PG.connect({ host: '127.0.0.1', dbname: 'chitter_test' }) + connection.exec(seed_sql) + end + + before(:each) do + reset_chitter_tables + end + + context 'GET /' do + it 'should return 200 and correct body' do + response = get('/') + + expect(response.status).to eq 200 + expect(response.body).to include '

Welcome to Chitter

' + end + end + + context 'GET /peeps' do + it 'should return 200 and correct body' do + response = get('/peeps') + + expect(response.status).to eq 200 + expect(response.body).to include "

What's Peeping?

" + end + end + + context 'GET /signup' do + it 'should return 200 and correct body' do + response = get('/signup') + + expect(response.status).to eq 200 + expect(response.body).to include '

Sign up to Chitter!

' + end + end +end \ No newline at end of file diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb index e589222d81..195bcb533d 100644 --- a/spec/user_repository_spec.rb +++ b/spec/user_repository_spec.rb @@ -42,7 +42,7 @@ def reset_chitter_tables user.name = 'Charlie' user.email = 'charliekelly@paddyspub.com' user.username = 'TheDayman' - user.password = BCrypt::Password.create('fightmilk') + user.password = 'fightmilk' repo.create(user) users = repo.all @@ -51,13 +51,17 @@ def reset_chitter_tables expect(new_user.name).to eq 'Charlie' expect(new_user.email).to eq 'charliekelly@paddyspub.com' expect(new_user.username).to eq 'TheDayman' - expect(new_user.id).to eq '3' + expect(new_user.id).to eq 3 end end context "The Find by Username Method" do it 'returns the correct user object with the username as the argument' do - + repo = UserRepository.new + user = repo.find_by_username('skates') + expect(user.username).to eq 'skates' + expect(user.name).to eq 'Jack' + expect(user.email).to eq 'jack@email.com' end end end From 3d00068dff43c887bbebce3e481781a71a46bba8 Mon Sep 17 00:00:00 2001 From: JRSkates Date: Fri, 12 May 2023 10:19:57 +0100 Subject: [PATCH 6/6] 94% Coverage and basic user story --- app.rb | 74 +----------------------------------- lib/user_repository.rb | 5 +-- spec/user_repository_spec.rb | 4 +- 3 files changed, 7 insertions(+), 76 deletions(-) diff --git a/app.rb b/app.rb index f745a629a2..61827b746f 100644 --- a/app.rb +++ b/app.rb @@ -1,5 +1,7 @@ require 'sinatra/base' require 'sinatra/reloader' +require 'date' +require 'bcrypt' require_relative 'lib/database_connection' require_relative 'lib/user_repository' require_relative 'lib/peep_repository' @@ -27,53 +29,10 @@ class Application < Sinatra::Base end post '/logged_in' do - - #if invalid_user_params? then - # status 400 - # return 'Invalid user details.' - #end - - user_repo = UserRepository.new - - if !user_repo.all.any?{|user| user.username == params[:username]} then - status 400 - return "Username does not exist, please try again." - end - - username = params[:username] - password = params[:password] - - user = user_repo.find_by_username(username) - - if user.password == password - # Set the user ID in session - session[:user_id] = user.id - redirect "/logged_in" - - - else - status 400 - return 'Invalid user details.' - end end get '/logged_in' do - #if session[:user_id] == nil - # No user id in the session - #return redirect('/') - #else - - user_id = session[:user_id] - - @user_repo = UserRepository.new - peep_repo = PeepRepository.new - - @user = @user_repo.find(user_id) - - return erb(:logged_in) - #end - end get '/peeps' do @@ -90,22 +49,6 @@ class Application < Sinatra::Base post '/signup' do users = UserRepository.new -=begin - if users.all.any?{|user| user.email == params[:email]} then - status 400 - return "Email address already signed up." - end - - if users.all.any?{|user| user.username == params[:username]} then - status 400 - return "Username already taken, please choose another." - end - - if invalid_signup_params? || invalid_user_params? then - status 400 - return 'Invalid credentials, please try again.' - end -=end user = User.new last_user = users.all.last user_id = (last_user.id) - 1 @@ -124,16 +67,3 @@ class Application < Sinatra::Base end end -=begin - private - def invalid_user_params? - params[:username] == nil || - params[:password] == nil || params[:password] == "" - end - - def invalid_peep_params? - params[:content] == nil || params[:content]== "" - end - -end -=end diff --git a/lib/user_repository.rb b/lib/user_repository.rb index d2ca53f3c6..f1aecce99b 100644 --- a/lib/user_repository.rb +++ b/lib/user_repository.rb @@ -52,11 +52,10 @@ def find_by_id(id) def find_by_username(username) sql = 'SELECT id, name, email, username, password FROM users WHERE username = $1;' - param = [username] - result = DatabaseConnection.exec_params(sql, param) + result = DatabaseConnection.exec_params(sql, [username]) record = result.first user = User.new - user.id = record['id'].to_i + user.id = record['id'] user.name = record['name'] user.email = record['email'] user.username = record['username'] diff --git a/spec/user_repository_spec.rb b/spec/user_repository_spec.rb index 195bcb533d..72b4261975 100644 --- a/spec/user_repository_spec.rb +++ b/spec/user_repository_spec.rb @@ -1,4 +1,6 @@ -require 'user_repository' +require_relative '../lib/user_repository' +require_relative './spec_helper' +require 'bcrypt' RSpec.describe UserRepository do def reset_chitter_tables