diff --git a/.gitignore b/.gitignore index 64442bb..14a523f 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ /yarn-error.log yarn-debug.log* .yarn-integrity +dump.rdb \ No newline at end of file diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 8d6c2a1..e3ce64d 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -2,5 +2,20 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + # identified_by :current_user + # def connect + # self.current_user = find_verified_user + # end + + # private + # def find_verified_user + # if + # verified_user = User.find_by(id: cookies.encrypted['session']['current_user_id']) + + # verified_user + # else + # reject_unauthorized_connection + # end + # end end end diff --git a/app/channels/poll_channel.rb b/app/channels/poll_channel.rb new file mode 100644 index 0000000..16f302b --- /dev/null +++ b/app/channels/poll_channel.rb @@ -0,0 +1,10 @@ +class PollChannel < ApplicationCable::Channel + + def subscribed + stream_from "poll_#{params[:room]}" + end + + def speak(data) + ActionCable.server.broadcast("poll_#{params[:room]}", { message: data["message"] }) + end +end \ No newline at end of file diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index d71aa11..e8119ff 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,6 +19,10 @@ def current_user def current_session_expire current_session = session[:expire_at] current_time = Time.current - session.delete(:current_user_id) if current_session && current_session < current_time + if current_session && current_session < current_time + session.delete(:current_user_id) + else + current_session = 20.minutes.from_now + end end end diff --git a/app/controllers/chats_controller.rb b/app/controllers/chats_controller.rb new file mode 100644 index 0000000..4259676 --- /dev/null +++ b/app/controllers/chats_controller.rb @@ -0,0 +1,3 @@ +class ChatsController < ApplicationController + +end \ No newline at end of file diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb new file mode 100644 index 0000000..04fbd85 --- /dev/null +++ b/app/controllers/invites_controller.rb @@ -0,0 +1,30 @@ +class InvitesController < ApplicationController + rescue_from ActiveRecord::RecordNotFound do + render :new + flash[:error] = 'User not found' + end + + def new + @poll = Poll.find(params[:poll_id]) + authorize! @poll, to: :invite? + end + + def create + @poll = Poll.find(params[:poll_id]) + authorize! @poll, to: :invite? + @invited_user = User.where(username: params[:query]).or(User.where(email: params[:query])).take! + if !invited? + @poll.members << @invited_user + redirect_to polls_path, success: "You invite #{@invited_user.username} to your poll" + else + render :new + flash[:error] = 'User alredy invited' + end + end + + private + def invited? + @poll.memberships.where(user_id: @invited_user.id).exists? + end + +end \ No newline at end of file diff --git a/app/controllers/polls_controller.rb b/app/controllers/polls_controller.rb new file mode 100644 index 0000000..c2dde5a --- /dev/null +++ b/app/controllers/polls_controller.rb @@ -0,0 +1,68 @@ +class PollsController < ApplicationController + + rescue_from ActiveRecord::RecordNotFound do + redirect_to polls_path, error: I18n.t('flash.poll.not_found') + end + + def index + authorize! Poll, to: :index? + @active = current_user.polls.active + @upcoming = current_user.polls.upcoming + @ended = current_user.polls.ended + end + + def show + @poll = Poll.find(params[:id]) + authorize! @poll, to: :show? + @options = @poll.options + @members = @poll.members.count + end + + def new + authorize! Poll, to: :create? + @poll = Poll.new + 2.times { @poll.options.build } + end + + def create + authorize! Poll, to: :create? + @poll = current_user.own_polls.new(poll_params) + if @poll.save + @poll.members << current_user + redirect_to polls_path, success: I18n.t('flash.poll.create') + else + render :new + end + end + + def edit + @poll = Poll.find(params[:id]) + @options = @poll.options + authorize! @poll, to: :update? + end + + def update + @poll = Poll.find(params[:id]) + authorize! @poll, to: :update? + + if @poll.update(poll_params) + redirect_to poll_path(@poll.id), success: I18n.t('flash.poll.update') + else + render :new + end + end + + def destroy + @poll = Poll.find(params[:id]) + authorize! @poll, to: :destroy? + @poll.destroy + redirect_to polls_path, success: I18n.t('flash.poll.deleted') + end + + + private + + def poll_params + params.require(:poll).permit(:title, :description, :start_date, :end_date, options_attributes: [:id, :vote_option, :_destroy]) + end +end diff --git a/app/controllers/votes_controller.rb b/app/controllers/votes_controller.rb new file mode 100644 index 0000000..b42694e --- /dev/null +++ b/app/controllers/votes_controller.rb @@ -0,0 +1,14 @@ +class VotesController < ApplicationController + + def update + @poll = current_user.polls.find(params[:poll_id]) + @option = @poll.options.find(params[:id]) + authorize! @poll, to: :vote? + + if @poll.memberships.update(poll_option_id: @option.id) + redirect_to poll_path(@poll.id), success: 'Successfully voted' + else + render :show + end + end +end \ No newline at end of file diff --git a/app/javascript/channels/consumer.js b/app/javascript/channels/consumer.js index 8ec3aad..40c20f8 100644 --- a/app/javascript/channels/consumer.js +++ b/app/javascript/channels/consumer.js @@ -3,4 +3,7 @@ import { createConsumer } from "@rails/actioncable" +createConsumer('http://127.0.0.1:3000/cable'); + export default createConsumer() + diff --git a/app/javascript/channels/poll_channel.js b/app/javascript/channels/poll_channel.js new file mode 100644 index 0000000..9382f83 --- /dev/null +++ b/app/javascript/channels/poll_channel.js @@ -0,0 +1,36 @@ +import consumer from "./consumer" + + + +const poll = document.querySelector('#poll'); +const poll_id = poll.dataset.pollId; + + const pollChannel = consumer.subscriptions.create({channel: "PollChannel", room: `poll_${poll_id}`}, { + connected() { + // Called when the subscription is ready for use on the server + + console.log("Connected to the poll room!"); + }, + received(data) { + let mess = document.createElement('span'), + col = document.createElement('div'); + col.classList.add('col', 'mb-4') + mess.classList.add('bg-info', 'rounded-pill', 'text-white', 'p-2'); + mess.innerHTML = data.message; + col.insertAdjacentElement('afterbegin', mess); + document.querySelector('#chat_holder').insertAdjacentElement('beforeend', col) + + console.log(data); + }, + + disconnected() { + // Called when the subscription has been terminated by the server + }, + speak(message) { + this.perform('speak', { message: message }) + } + }); + + +export default pollChannel; + \ No newline at end of file diff --git a/app/javascript/packs/add_button_script/script.js b/app/javascript/packs/add_button_script/script.js new file mode 100644 index 0000000..52fdb6e --- /dev/null +++ b/app/javascript/packs/add_button_script/script.js @@ -0,0 +1,30 @@ +document.addEventListener('DOMContentLoaded', () => { + + const addButton = document.querySelector('#btn'), + options = document.querySelector('#options'); + + + let counter = 2; + + addButton.addEventListener('click', (e) => { + const optionArray = document.querySelectorAll('[name*="[vote_option]"]'); + e.preventDefault(); + if (optionArray.length < 5) { + const col = document.createElement('div'); + const input = ` + `; + + col.classList.add('col-4'); + col.innerHTML = input; + options.insertAdjacentElement('beforeend', col); + + counter++; + } else { + addButton.style.display = 'none'; + + } + }); + +}); \ No newline at end of file diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index f710851..f15ffea 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -7,7 +7,27 @@ import Rails from "@rails/ujs" import Turbolinks from "turbolinks" import * as ActiveStorage from "@rails/activestorage" import "channels" +import pollChannel from "../channels/poll_channel" Rails.start() -Turbolinks.start() +// Turbolinks.start() ActiveStorage.start() + +require('./add_button_script/script'); + +document.addEventListener('DOMContentLoaded', () => { + const btn = document.querySelector('#button-addon2'), + input = document.querySelector('#message_input'); + + btn.addEventListener('click', (e) => { + e.preventDefault(); + let message = input.value; + if (message.length > 0) { + pollChannel.speak(message); + input.value = ""; + } + }); +}) + + + diff --git a/app/models/poll.rb b/app/models/poll.rb new file mode 100644 index 0000000..cdae8b7 --- /dev/null +++ b/app/models/poll.rb @@ -0,0 +1,28 @@ +class Poll < ApplicationRecord + belongs_to :user + has_many :memberships, class_name: 'PollMembership', foreign_key: "poll_id" + has_many :members, -> { distinct }, through: :memberships, source: :user, dependent: :destroy + has_many :options, class_name: "PollOption", dependent: :destroy + + accepts_nested_attributes_for :options, allow_destroy: true, reject_if: lambda {|attributes| attributes['vote_option'].blank?} + + + validates :title, presence: true + validates :start_date, presence: true + validates :end_date, presence: true + validate :end_date_before_start_date + + scope :upcoming, -> { where('start_date > ?', Date.today) } + scope :active, -> { where('start_date <= ? and end_date >= ?', Date.today, Date.today) } + scope :ended, -> { where('end_date < ?', Date.today) } + + private + def end_date_before_start_date + return if start_date.blank? || end_date.blank? + + errors.add(:end_date, "can't be earlier than start date ") if end_date <= start_date + end + + + +end diff --git a/app/models/poll_membership.rb b/app/models/poll_membership.rb new file mode 100644 index 0000000..83239fc --- /dev/null +++ b/app/models/poll_membership.rb @@ -0,0 +1,5 @@ +class PollMembership < ApplicationRecord + belongs_to :user + belongs_to :poll + belongs_to :poll_option, dependent: :destroy, optional: true +end diff --git a/app/models/poll_option.rb b/app/models/poll_option.rb new file mode 100644 index 0000000..a1d3c62 --- /dev/null +++ b/app/models/poll_option.rb @@ -0,0 +1,6 @@ +class PollOption < ApplicationRecord + has_many :memberships, dependent: :nullify, class_name: 'PollMembership' + + belongs_to :poll, counter_cache: :vote_count + +end diff --git a/app/models/user.rb b/app/models/user.rb index 7d38b2b..87f95df 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,10 @@ class User < ApplicationRecord VALID_EMAIL_REGEX = /\A[\w+]+@[a-z\d]+\.[a-z]+\z/i has_secure_password + has_many :own_polls, dependent: :destroy, class_name: 'Poll' + has_many :memberships, class_name: 'PollMembership', foreign_key: "user_id" + has_many :polls,-> { distinct }, through: :memberships, source: :poll + validates :username, presence: true validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }, uniqueness: true diff --git a/app/policies/application_policy.rb b/app/policies/application_policy.rb index f1e4ed9..e441cdd 100644 --- a/app/policies/application_policy.rb +++ b/app/policies/application_policy.rb @@ -7,4 +7,5 @@ class ApplicationPolicy < ActionPolicy::Base def logged_in? user.present? end + end diff --git a/app/policies/poll_policy.rb b/app/policies/poll_policy.rb new file mode 100644 index 0000000..049b3f5 --- /dev/null +++ b/app/policies/poll_policy.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +class PollPolicy < ApplicationPolicy + def index? + logged_in? + end + + def create? + logged_in? + end + + def show? + invited? + end + + def update? + owner? + end + + def destroy? + owner? + end + + def invite? + owner? + end + def vote? + invited? && !voted? + end + + def owner? + user.id == record.user_id + end + + def invited? + record.memberships.where(user_id: user.id).exists? + end + + def voted? + record.memberships.where(user_id: user.id).and(record.memberships.where.not(poll_option_id: nil)).exists? + end +end diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index 1210a33..daa2ab3 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true class UserPolicy < ApplicationPolicy - def create? !logged_in? end diff --git a/app/views/invites/_invite_form.html.erb b/app/views/invites/_invite_form.html.erb new file mode 100644 index 0000000..01c8a11 --- /dev/null +++ b/app/views/invites/_invite_form.html.erb @@ -0,0 +1,8 @@ +<%= form_tag poll_invites_path, method: :post, class: "border rounded p-4 mt-5" do %> +
+ <%= text_field_tag :query, params[:query], class: "form-control" %> +
+
+ <%= submit_tag "Send invite", class: "btn btn-primary" %> +
+<% end %> \ No newline at end of file diff --git a/app/views/invites/new.html.erb b/app/views/invites/new.html.erb new file mode 100644 index 0000000..0d9d35d --- /dev/null +++ b/app/views/invites/new.html.erb @@ -0,0 +1,9 @@ +
+
+
+

Invite friends to poll.

+

Type username or email

+ <%= render "invite_form"%> +
+
+
\ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 8c4e97f..ed8b624 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -7,7 +7,7 @@ <%= csp_meta_tag %> <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> - <%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %> + <%= javascript_pack_tag 'application' %> diff --git a/app/views/pages/index.html.erb b/app/views/pages/index.html.erb index 74f3a88..aa747cc 100644 --- a/app/views/pages/index.html.erb +++ b/app/views/pages/index.html.erb @@ -1,5 +1,5 @@ -
-
-

Make your Poll easier!!

-
+
+
+

Welcome to Fun Poll App

+
\ No newline at end of file diff --git a/app/views/polls/_active.html.erb b/app/views/polls/_active.html.erb new file mode 100644 index 0000000..4cede93 --- /dev/null +++ b/app/views/polls/_active.html.erb @@ -0,0 +1,18 @@ +
+
+
+
+
Title
+ <%= link_to active.title, active, class: "nav-link" %> +
+
+
Author
+

<%= active.user.username %>

+
+
+
Start date
+

<%= active.start_date %>

+
+
+
+
\ No newline at end of file diff --git a/app/views/polls/_edit_poll_form.html.erb b/app/views/polls/_edit_poll_form.html.erb new file mode 100644 index 0000000..73db8b9 --- /dev/null +++ b/app/views/polls/_edit_poll_form.html.erb @@ -0,0 +1,42 @@ +<%= form_for @poll, method: :patch, class: "border rounded p-4 mt-5" do |f| %> + <% if f.object.errors.any? %> +
+ <%= f.object.errors.full_messages.each do |msg| %> +
<%= msg %>
+ <% end %> +
+ <% end %> + +
+ <%= f.text_field :title, placeholder: "Title", class: "form-control" %> +
+
+ <%= f.text_area :description, placeholder: "Description", class: "form-control" %> +
+
+
+ <%= f.label :start_date, class: "form-label"%> + <%= f.date_field :start_date, class: "form-control"%> +
+
+ <%= f.label :end_date, class: "form-label"%> + <%= f.date_field :end_date, class: "form-control"%> +
+
+
+ <%= f.fields_for :options, method: :patch do |option| %> +
+ <%= option.text_field :vote_option, placeholder: 'Option', class: "form-control" %> + <%= option.check_box :_destroy %> +
+ <% end %> +
+
+
+ <%= f.submit "Edit poll", class: "btn btn-primary" %> +
+
+ <%= f.button "Add option", class: "btn btn-success", style: "width: 100%;" %> +
+
+ <% end %> diff --git a/app/views/polls/_ended.html.erb b/app/views/polls/_ended.html.erb new file mode 100644 index 0000000..655e233 --- /dev/null +++ b/app/views/polls/_ended.html.erb @@ -0,0 +1,18 @@ +
+
+
+
+
Title
+ <%= link_to ended.title, ended, class: "nav-link" %> +
+
+
Author
+

<%= ended.user.username %>

+
+
+
End date
+

<%= ended.start_date %>

+
+
+
+
\ No newline at end of file diff --git a/app/views/polls/_new_poll_form.html.erb b/app/views/polls/_new_poll_form.html.erb new file mode 100644 index 0000000..a326d5d --- /dev/null +++ b/app/views/polls/_new_poll_form.html.erb @@ -0,0 +1,42 @@ + <%= form_for @poll, class: "border rounded p-4 mt-5" do |f| %> + <% if f.object.errors.any? %> +
+ <%= f.object.errors.full_messages.each do |msg| %> +
<%= msg %>
+ <% end %> +
+ <% end %> + +
+ <%= f.text_field :title, placeholder: "Title", class: "form-control" %> +
+
+ <%= f.text_area :description, placeholder: "Description", class: "form-control" %> +
+
+
+ <%= f.label :start_date, class: "form-label"%> + <%= f.date_field :start_date, class: "form-control"%> +
+
+ <%= f.label :end_date, class: "form-label"%> + <%= f.date_field :end_date, class: "form-control"%> +
+
+
+ <%= f.fields_for :options do |option| %> +
+ <%= option.text_field :vote_option, placeholder: 'Option', class: "form-control" %> + <%= option.check_box :_destroy %> +
+ <% end %> +
+
+
+ <%= f.submit "Add poll", class: "btn btn-primary" %> +
+
+ +
+
+ <% end %> diff --git a/app/views/polls/_options.html.erb b/app/views/polls/_options.html.erb new file mode 100644 index 0000000..31872a0 --- /dev/null +++ b/app/views/polls/_options.html.erb @@ -0,0 +1,7 @@ +
+
+ <%= form_for options, url: poll_vote_path(@poll.id), method: :patch do |f|%> + <%= f.submit options.vote_option, class: 'nav-link border mb-1'%> + <% end %> +
+
\ No newline at end of file diff --git a/app/views/polls/_upcoming.html.erb b/app/views/polls/_upcoming.html.erb new file mode 100644 index 0000000..ae32f06 --- /dev/null +++ b/app/views/polls/_upcoming.html.erb @@ -0,0 +1,18 @@ +
+
+
+
+
Title
+ <%= link_to upcoming.title, upcoming, class: "nav-link" %> +
+
+
Author
+

<%= upcoming.user.username %>

+
+
+
Start date
+

<%= upcoming.start_date %>

+
+
+
+
\ No newline at end of file diff --git a/app/views/polls/edit.html.erb b/app/views/polls/edit.html.erb new file mode 100644 index 0000000..ff7df57 --- /dev/null +++ b/app/views/polls/edit.html.erb @@ -0,0 +1,8 @@ +
+
+
+

Edit your poll.

+ <%= render "edit_poll_form" %> +
+
+
\ No newline at end of file diff --git a/app/views/polls/index.html.erb b/app/views/polls/index.html.erb new file mode 100644 index 0000000..1e602ed --- /dev/null +++ b/app/views/polls/index.html.erb @@ -0,0 +1,20 @@ +
+
+

Your poll

+ <%= link_to '+ New Poll', new_poll_path, class: "btn btn-primary"%> +
+
+
+ <% if current_user.polls.empty?%> +

You have no polls yet.

+ <% else %> +

Uppcoming polls

+ <%= render partial: "upcoming", collection: @upcoming %> +

Active polls

+ <%= render partial: "active", collection: @active %> +

Closed pools

+ <%= render partial: "ended", collection: @ended%> + <% end %> +
+
+
\ No newline at end of file diff --git a/app/views/polls/new.html.erb b/app/views/polls/new.html.erb new file mode 100644 index 0000000..1237fde --- /dev/null +++ b/app/views/polls/new.html.erb @@ -0,0 +1,8 @@ +
+
+
+

Create your own poll.

+ <%= render "new_poll_form"%> +
+
+
diff --git a/app/views/polls/show.html.erb b/app/views/polls/show.html.erb new file mode 100644 index 0000000..83ddca8 --- /dev/null +++ b/app/views/polls/show.html.erb @@ -0,0 +1,55 @@ +
> +
+
+

<%= @poll.title %>

+
+
+ <% if allowed_to?(:invite?, @poll ) %> + <%= link_to "Invite user", new_poll_invite_path(@poll.id), class:"btn btn-success pl-1 pr-1 w-50"%> + <% end %> + <% if allowed_to?(:update?, @poll ) %> + <%= link_to "Edit poll", edit_poll_path(@poll.id), class:"btn btn-warning pl-1 pr-1 w-50"%> + <% end %> + <% if allowed_to?(:destroy?, @poll ) %> + <%= link_to "Delete poll", @poll, method: :delete, data: { confirm: "Are you sure?" }, class:"btn btn-danger pl-1 pr-1 w-50"%> + <% end %> +
+
+
+
+
+
+
Details
+

Description

+

<%= @poll.description %>

+
+
+

Start date

+

<%= @poll.start_date %>

+
+
+

End date

+

<%= @poll.end_date %>

+
+
+

Participians count <%= @members%>

+
+
+
+
+
+
+ <%= render partial: 'options', collection: @options%> +
+
+
+
+
+
+
+
+ + +
+
+
\ No newline at end of file diff --git a/app/views/shared/_navigation.html.erb b/app/views/shared/_navigation.html.erb index 9eb20e4..e990e3e 100644 --- a/app/views/shared/_navigation.html.erb +++ b/app/views/shared/_navigation.html.erb @@ -6,6 +6,9 @@ +