diff --git a/Gemfile b/Gemfile index 1bd5c98af..e3e5d8571 100644 --- a/Gemfile +++ b/Gemfile @@ -98,6 +98,7 @@ group :development do gem "rails-erd" gem "rufo" gem "specs_to_readme" + gem "web_git" end group :development, :test do diff --git a/Gemfile.lock b/Gemfile.lock index c4571af47..d42785bf8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -83,6 +83,7 @@ GEM annotate (3.2.0) activerecord (>= 3.2, < 8.0) rake (>= 10.4, < 14.0) + ansispan (0.0.2) appdev_support (0.2.1) tabulo awesome_print (1.9.2) @@ -141,6 +142,7 @@ GEM responders warden (~> 1.2.3) diff-lcs (1.5.1) + diffy (3.4.3) domain_name (0.6.20240107) dotenv (3.1.2) draft_generators (0.0.4) @@ -164,6 +166,11 @@ GEM ffi-compiler (1.3.2) ffi (>= 1.15.5) rake + git (3.0.0) + activesupport (>= 5.0) + addressable (~> 2.8) + process_executer (~> 1.3) + rchardet (~> 1.9) globalid (1.2.1) activesupport (>= 6.1) grade_runner (0.0.12) @@ -233,6 +240,8 @@ GEM mini_mime (1.1.5) minitest (5.23.1) msgpack (1.7.2) + mustermann (3.0.3) + ruby2_keywords (~> 0.0.1) mutex_m (0.2.0) net-http (0.4.1) uri @@ -256,6 +265,7 @@ GEM oj (3.13.23) orm_adapter (0.5.0) pg (1.5.6) + process_executer (1.3.0) pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) @@ -268,6 +278,10 @@ GEM nio4r (~> 2.0) racc (1.8.0) rack (3.0.11) + rack-protection (4.1.1) + base64 (>= 0.1.0) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-session (2.0.0) rack (>= 3.0.0) rack-test (2.1.0) @@ -323,6 +337,7 @@ GEM activerecord (>= 6.1.5) activesupport (>= 6.1.5) i18n + rchardet (1.9.0) rdoc (6.7.0) psych (>= 4.0.0) redis (4.8.1) @@ -364,6 +379,7 @@ GEM ruby-vips (2.2.2) ffi (~> 1.12) logger + ruby2_keywords (0.0.5) rubyzip (2.3.2) rufo (0.18.0) sawyer (0.9.2) @@ -378,6 +394,13 @@ GEM simple_form (5.3.1) actionpack (>= 5.2) activemodel (>= 5.2) + sinatra (4.1.1) + logger (>= 1.6.0) + mustermann (~> 3.0) + rack (>= 3.0.0, < 4) + rack-protection (= 4.1.1) + rack-session (>= 2.0.0, < 3) + tilt (~> 2.0) specs_to_readme (0.1.0) sprockets (4.2.1) concurrent-ruby (~> 1.0) @@ -400,6 +423,7 @@ GEM terminal-table (3.0.2) unicode-display_width (>= 1.1.1, < 3) thor (1.3.1) + tilt (2.6.0) timeout (0.4.1) tty-screen (0.8.2) turbo-rails (2.0.5) @@ -408,6 +432,8 @@ GEM railties (>= 6.0.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) + tzinfo-data (1.2025.1) + tzinfo (>= 1.0.0) unicode-display_width (2.5.0) uri (0.13.0) warden (1.2.9) @@ -417,6 +443,13 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) + web_git (0.1.0) + actionview + ansispan + diffy + git + sinatra + tzinfo-data webdrivers (5.3.1) nokogiri (~> 1.6) rubyzip (>= 1.3.0) @@ -482,6 +515,7 @@ DEPENDENCIES turbo-rails tzinfo-data web-console + web_git webdrivers webmock diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09705d12a..6b4dcfa85 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,3 @@ class ApplicationController < ActionController::Base + before_action :authenticate_user! end diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb new file mode 100644 index 000000000..6cae78f6e --- /dev/null +++ b/app/controllers/comments_controller.rb @@ -0,0 +1,70 @@ +class CommentsController < ApplicationController + before_action :set_comment, only: %i[ show edit update destroy ] + + # GET /comments or /comments.json + def index + @comments = Comment.all + end + + # GET /comments/1 or /comments/1.json + def show + end + + # GET /comments/new + def new + @comment = Comment.new + end + + # GET /comments/1/edit + def edit + end + + # POST /comments or /comments.json + def create + @comment = Comment.new(comment_params) + + respond_to do |format| + if @comment.save + format.html { redirect_to comment_url(@comment), notice: "Comment was successfully created." } + format.json { render :show, status: :created, location: @comment } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @comment.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /comments/1 or /comments/1.json + def update + respond_to do |format| + if @comment.update(comment_params) + format.html { redirect_to comment_url(@comment), notice: "Comment was successfully updated." } + format.json { render :show, status: :ok, location: @comment } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @comment.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /comments/1 or /comments/1.json + def destroy + @comment.destroy! + + respond_to do |format| + format.html { redirect_to comments_url, notice: "Comment was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_comment + @comment = Comment.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def comment_params + params.require(:comment).permit(:author_id, :photo_id, :body) + end +end diff --git a/app/controllers/follow_requests_controller.rb b/app/controllers/follow_requests_controller.rb new file mode 100644 index 000000000..fcbdc78c4 --- /dev/null +++ b/app/controllers/follow_requests_controller.rb @@ -0,0 +1,70 @@ +class FollowRequestsController < ApplicationController + before_action :set_follow_request, only: %i[ show edit update destroy ] + + # GET /follow_requests or /follow_requests.json + def index + @follow_requests = FollowRequest.all + end + + # GET /follow_requests/1 or /follow_requests/1.json + def show + end + + # GET /follow_requests/new + def new + @follow_request = FollowRequest.new + end + + # GET /follow_requests/1/edit + def edit + end + + # POST /follow_requests or /follow_requests.json + def create + @follow_request = FollowRequest.new(follow_request_params) + + respond_to do |format| + if @follow_request.save + format.html { redirect_to follow_request_url(@follow_request), notice: "Follow request was successfully created." } + format.json { render :show, status: :created, location: @follow_request } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @follow_request.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /follow_requests/1 or /follow_requests/1.json + def update + respond_to do |format| + if @follow_request.update(follow_request_params) + format.html { redirect_to follow_request_url(@follow_request), notice: "Follow request was successfully updated." } + format.json { render :show, status: :ok, location: @follow_request } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @follow_request.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /follow_requests/1 or /follow_requests/1.json + def destroy + @follow_request.destroy! + + respond_to do |format| + format.html { redirect_to follow_requests_url, notice: "Follow request was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_follow_request + @follow_request = FollowRequest.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def follow_request_params + params.require(:follow_request).permit(:recipient_id, :sender_id, :status) + end +end diff --git a/app/controllers/likes_controller.rb b/app/controllers/likes_controller.rb new file mode 100644 index 000000000..9acbe1290 --- /dev/null +++ b/app/controllers/likes_controller.rb @@ -0,0 +1,70 @@ +class LikesController < ApplicationController + before_action :set_like, only: %i[ show edit update destroy ] + + # GET /likes or /likes.json + def index + @likes = Like.all + end + + # GET /likes/1 or /likes/1.json + def show + end + + # GET /likes/new + def new + @like = Like.new + end + + # GET /likes/1/edit + def edit + end + + # POST /likes or /likes.json + def create + @like = Like.new(like_params) + + respond_to do |format| + if @like.save + format.html { redirect_to like_url(@like), notice: "Like was successfully created." } + format.json { render :show, status: :created, location: @like } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @like.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /likes/1 or /likes/1.json + def update + respond_to do |format| + if @like.update(like_params) + format.html { redirect_to like_url(@like), notice: "Like was successfully updated." } + format.json { render :show, status: :ok, location: @like } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @like.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /likes/1 or /likes/1.json + def destroy + @like.destroy! + + respond_to do |format| + format.html { redirect_to likes_url, notice: "Like was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_like + @like = Like.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def like_params + params.require(:like).permit(:fan_id, :photo_id) + end +end diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb new file mode 100644 index 000000000..7b50a7997 --- /dev/null +++ b/app/controllers/photos_controller.rb @@ -0,0 +1,70 @@ +class PhotosController < ApplicationController + before_action :set_photo, only: %i[ show edit update destroy ] + + # GET /photos or /photos.json + def index + @photos = Photo.all + end + + # GET /photos/1 or /photos/1.json + def show + end + + # GET /photos/new + def new + @photo = Photo.new + end + + # GET /photos/1/edit + def edit + end + + # POST /photos or /photos.json + def create + @photo = Photo.new(photo_params) + + respond_to do |format| + if @photo.save + format.html { redirect_to photo_url(@photo), notice: "Photo was successfully created." } + format.json { render :show, status: :created, location: @photo } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @photo.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /photos/1 or /photos/1.json + def update + respond_to do |format| + if @photo.update(photo_params) + format.html { redirect_to photo_url(@photo), notice: "Photo was successfully updated." } + format.json { render :show, status: :ok, location: @photo } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @photo.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /photos/1 or /photos/1.json + def destroy + @photo.destroy! + + respond_to do |format| + format.html { redirect_to photos_url, notice: "Photo was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_photo + @photo = Photo.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def photo_params + params.require(:photo).permit(:image, :comments_count, :likes_count, :caption, :owner_id) + end +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 000000000..59b81cade --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,28 @@ +# == Schema Information +# +# Table name: comments +# +# id :bigint not null, primary key +# body :text not null +# created_at :datetime not null +# updated_at :datetime not null +# author_id :bigint not null +# photo_id :bigint not null +# +# Indexes +# +# index_comments_on_photo_id (photo_id) +# +# Foreign Keys +# +# fk_rails_... (author_id => users.id) +# fk_rails_... (photo_id => photos.id) +# +# app/models/comment.rb + +class Comment < ApplicationRecord + belongs_to :author, class_name: "User", counter_cache: true + belongs_to :photo, counter_cache: true + + validates :body, presence: true +end diff --git a/app/models/follow_request.rb b/app/models/follow_request.rb new file mode 100644 index 000000000..d400204aa --- /dev/null +++ b/app/models/follow_request.rb @@ -0,0 +1,6 @@ +class FollowRequest < ApplicationRecord + belongs_to :recipient, class_name: "User" + belongs_to :sender, class_name: "User" + + enum status: { pending: "pending", rejected: "rejected", accepted: "accepted" } +end diff --git a/app/models/like.rb b/app/models/like.rb new file mode 100644 index 000000000..d7a03b761 --- /dev/null +++ b/app/models/like.rb @@ -0,0 +1,24 @@ +# == Schema Information +# +# Table name: likes +# +# id :bigint not null, primary key +# created_at :datetime not null +# updated_at :datetime not null +# fan_id :bigint not null +# photo_id :bigint not null +# +# Indexes +# +# index_likes_on_fan_id (fan_id) +# index_likes_on_photo_id (photo_id) +# +# Foreign Keys +# +# fk_rails_... (fan_id => users.id) +# fk_rails_... (photo_id => photos.id) +# +class Like < ApplicationRecord + belongs_to :fan, class_name: "User", counter_cache: true + belongs_to :photo, counter_cache: true +end diff --git a/app/models/photo.rb b/app/models/photo.rb new file mode 100644 index 000000000..04661657c --- /dev/null +++ b/app/models/photo.rb @@ -0,0 +1,15 @@ +# app/models/photo.rb + +class Photo < ApplicationRecord + belongs_to :owner, class_name: "User", counter_cache: true + has_many :comments, dependent: :destroy + has_many :likes, dependent: :destroy + has_many :fans, through: :likes + + validates :caption, presence: true + validates :image, presence: true + + # Scopes + scope :past_week, -> { where(created_at: 1.week.ago..Time.current) } + scope :by_likes, -> { order(likes_count: :desc) } +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..34a61f549 --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,30 @@ +class User < ApplicationRecord + devise :database_authenticatable, :registerable, + :recoverable, :rememberable, :validatable + + # Direct associations + has_many :comments, foreign_key: :author_id, dependent: :destroy + has_many :likes, foreign_key: :fan_id, dependent: :destroy + + # FollowRequest associations + has_many :sent_follow_requests, foreign_key: :sender_id, class_name: "FollowRequest", dependent: :destroy + has_many :received_follow_requests, foreign_key: :recipient_id, class_name: "FollowRequest", dependent: :destroy + has_many :accepted_sent_follow_requests, -> { where(status: "accepted") }, + foreign_key: :sender_id, class_name: "FollowRequest" + has_many :accepted_received_follow_requests, -> { where(status: "accepted") }, + foreign_key: :recipient_id, class_name: "FollowRequest" + + # Indirect associations + has_many :leaders, through: :accepted_sent_follow_requests, source: :recipient + has_many :followers, through: :accepted_received_follow_requests, source: :sender + has_many :liked_photos, through: :likes, source: :photo + + # **This is the missing association:** + has_many :own_photos, foreign_key: :owner_id, class_name: "Photo", dependent: :destroy + + # Additional indirect associations for feed and discover, if applicable + has_many :feed, through: :leaders, source: :own_photos + has_many :discover, through: :leaders, source: :liked_photos + + validates :username, presence: true, uniqueness: true +end diff --git a/app/views/comments/_comment.html.erb b/app/views/comments/_comment.html.erb new file mode 100644 index 000000000..e5aac46fe --- /dev/null +++ b/app/views/comments/_comment.html.erb @@ -0,0 +1,17 @@ +
+ Author: + <%= comment.author_id %> +
+ ++ Photo: + <%= comment.photo_id %> +
+ ++ Body: + <%= comment.body %> +
+ +<%= notice %>
+ +<%= notice %>
+ +<%= render @comment %> + ++ Recipient: + <%= follow_request.recipient_id %> +
+ ++ Sender: + <%= follow_request.sender_id %> +
+ ++ Status: + <%= follow_request.status %> +
+ +<%= notice %>
+ ++ <%= link_to "Show this follow request", follow_request %> +
+ <% end %> +<%= notice %>
+ +<%= render @follow_request %> + +
+ <%= link_to "Show this comment", comment %> +
+ <% end %> +