Compare commits

...

6 commits

Author SHA1 Message Date
Christophe Robillard
0a2f26684b fill form mode 2026-03-21 16:17:59 +01:00
Christophe Robillard
a9f72b60af can edit selected day 2026-03-21 16:16:54 +01:00
Christophe Robillard
46e6014fea update day_log 2026-03-20 19:28:54 +01:00
Christophe Robillard
d6fba7e6e8 valid mood unique per day per user 2026-03-20 19:08:26 +01:00
Christophe Robillard
560a9442a7 fix modes factory 2026-03-20 19:08:26 +01:00
Christophe Robillard
93b578c3cf create day_log 2026-03-20 19:08:10 +01:00
15 changed files with 411 additions and 9 deletions

View file

@ -0,0 +1,50 @@
class DayLogsController < ApplicationController
before_action :set_day_log, only: [ :update ]
def edit
@day_log = Current.user.day_logs.find_or_initialize_by(day: params[:day])
@mood = Current.user.moods.find_by(recorded_at: params[:day].to_date.beginning_of_day..params[:day].to_date.end_of_day)
end
def create
@day_log = Current.user.day_logs.build(day_log_params)
if @day_log.save
handle_mood(@day_log.day, params[:day_log][:mode_id])
redirect_to root_path
else
render :new, status: :unprocessable_content
end
end
def update
if @day_log.update(day_log_params)
handle_mood(@day_log.day, params[:day_log][:mode_id])
redirect_to dashboard_path
else
render :edit, status: :unprocessable_content
end
end
private
def set_day_log
@day_log = Current.user.day_logs.find(params[:id])
end
def handle_mood(day, mode_id)
return if mode_id.blank?
mood = Current.user.moods.find_by(recorded_at: day.beginning_of_day..day.end_of_day)
if mood
mood.update(mode_id: mode_id)
else
Current.user.moods.create(mode_id: mode_id, recorded_at: day.to_datetime)
end
end
def day_log_params
params.expect(day_log: [ :day, :info ])
end
end

View file

@ -1,16 +1,19 @@
import { Controller } from '@hotwired/stimulus' import { Controller } from '@hotwired/stimulus'
export default class extends Controller { export default class extends Controller {
static targets = [ "image", "modeDay" ]; static targets = [ "image", "modeDay", "dayLogLink" ];
updateDayInfo(event) { updateDayInfo(event) {
const image = this.imageTarget; const image = this.imageTarget;
const modeDay = this.modeDayTarget; const modeDay = this.modeDayTarget;
const modeDayContent = event.target.dataset.day + ' : ' + event.target.dataset.mode; const day = event.target.dataset.day;
const modeDayContent = day + ' : ' + event.target.dataset.mode;
image.src = event.target.dataset.image; image.src = event.target.dataset.image;
modeDay.textContent = modeDayContent; modeDay.textContent = modeDayContent;
event.target.className = "selected-day " + event.target.dataset.mode; event.target.className = "selected-day " + event.target.dataset.mode;
this.dayLogLinkTarget.href = `/day_logs/edit?day=${day}`
if (this.lastTarget) { if (this.lastTarget) {
this.lastTarget.className = "day " + this.lastTarget.dataset.mode; this.lastTarget.className = "day " + this.lastTarget.dataset.mode;
} }

View file

@ -1,4 +1,19 @@
class Mood < ApplicationRecord class Mood < ApplicationRecord
belongs_to :user belongs_to :user
belongs_to :mode belongs_to :mode
validates :recorded_at, presence: true
validate :unique_per_day_and_user
private
def unique_per_day_and_user
return unless recorded_at && user
existing = user.moods.where.not(id: id).any? do |mood|
mood.recorded_at.to_date == recorded_at.to_date
end
errors.add(:recorded_at, :taken) if existing
end
end end

View file

@ -3,6 +3,7 @@ class User < ApplicationRecord
has_many :sessions, dependent: :destroy has_many :sessions, dependent: :destroy
has_many :moods, -> { order "recorded_at" } has_many :moods, -> { order "recorded_at" }
has_many :modes has_many :modes
has_many :day_logs
normalizes :email_address, with: ->(e) { e.strip.downcase } normalizes :email_address, with: ->(e) { e.strip.downcase }

View file

@ -0,0 +1,20 @@
= form_with model: day_log do |f|
- @day_log.errors.full_messages.each do |msg|
%p= msg
.field
= f.label :day
= f.date_field :day
.field
= f.label :info
= f.text_area :info
.field
= label_tag :mode_id, "Mode"
= select_tag "day_log[mode_id]",
options_from_collection_for_select(Current.user.modes, :id, :label, @mood&.mode_id),
include_blank: true
= f.submit

View file

@ -0,0 +1,3 @@
%h1 Modifier le journal
= render 'form', day_log: @day_log

View file

@ -0,0 +1,3 @@
%h1 Nouveau journal
= render 'form', day_log: @day_log

View file

@ -23,8 +23,10 @@
<div class="title is-4"> <div class="title is-4">
<span class="icon"><i class="fa-regular fa-calendar"></i></span> <span class="icon"><i class="fa-regular fa-calendar"></i></span>
<span data-mood-target="modeDay">Aujourd'hui : <%= @user.current_mode.label %></span> <span data-mood-target="modeDay">Aujourd'hui : <%= @user.current_mode.label %></span>
<%= link_to "Journal", edit_day_log_for_day_path(day: Date.today), "data-mood-target": "dayLogLink" %>
</div> </div>
</div> </div>
<div class="logs"> <div class="logs">
<div class="is-flex is-flex-direction-row is-flex-wrap-wrap mb-3"> <div class="is-flex is-flex-direction-row is-flex-wrap-wrap mb-3">
<% @user.history.each do |month| %> <% @user.history.each do |month| %>

View file

@ -21,4 +21,7 @@ Rails.application.routes.draw do
patch "/invite/:token", to: "invitations#update", as: :invitation patch "/invite/:token", to: "invitations#update", as: :invitation
get "/moods", to: "moods#index", as: :dashboard get "/moods", to: "moods#index", as: :dashboard
get "/day_logs/edit", to: "day_logs#edit", as: :edit_day_log_for_day
resources :day_logs, only: [ :create, :update ]
end end

View file

@ -1,7 +1,6 @@
FactoryBot.define do FactoryBot.define do
factory :mode do factory :mode do
label { "MyString" } label { "MyString" }
slug { "MyString" }
color { "MyString" } color { "MyString" }
user { nil } user { nil }
end end

View file

@ -1,7 +1,7 @@
FactoryBot.define do FactoryBot.define do
factory :mood do factory :mood do
mode { "croisiere" } recorded_at { DateTime.parse("2026-01-15 10:00:00") }
recorded_at { DateTime.now }
association :user association :user
association :mode
end end
end end

View file

@ -2,6 +2,7 @@ FactoryBot.define do
factory :user do factory :user do
sequence(:email_address) { |n| "user#{n}@example.com" } sequence(:email_address) { |n| "user#{n}@example.com" }
sequence(:username) { |n| "user#{n}" } sequence(:username) { |n| "user#{n}" }
password_digest { BCrypt::Password.create('password123') } password { "obanonalors" }
password_digest { BCrypt::Password.create(password) }
end end
end end

View file

@ -1,7 +1,46 @@
require 'rails_helper' require 'rails_helper'
describe Mood, type: :model do describe Mood do
it 'works' do let(:user) { create(:user) }
expect(true) let(:mode) { create(:mode, user: user) }
subject { build(:mood, user: user, mode: mode, recorded_at: DateTime.parse("2026-01-15 10:00:00")) }
it { is_expected.to be_valid }
describe "validations" do
it "is invalid without a user" do
subject.user = nil
expect(subject).not_to be_valid
end
it "is invalid without a mode" do
subject.mode = nil
expect(subject).not_to be_valid
end
it "is invalid without a recorded_at" do
subject.recorded_at = nil
expect(subject).not_to be_valid
end
end
describe "uniqueness per day and user" do
it "is invalid if a mood already exists for the same day and user" do
create(:mood, user: user, mode: mode, recorded_at: DateTime.parse("2026-01-15 18:00:00"))
expect(subject).not_to be_valid
end
it "is valid if the same day exists but for a different user" do
other_user = create(:user)
other_mode = create(:mode, user: other_user)
create(:mood, user: other_user, mode: other_mode, recorded_at: DateTime.parse("2026-01-15 10:00:00"))
expect(subject).to be_valid
end
it "is valid if the same user has a mood on a different day" do
create(:mood, user: user, mode: mode, recorded_at: DateTime.parse("2026-01-14 10:00:00"))
expect(subject).to be_valid
end
end end
end end

View file

@ -0,0 +1,251 @@
require 'rails_helper'
describe "DayLogs", type: :request do
let(:user) { create(:user) }
let(:mode) { create(:mode, user: user) }
let(:day) { Date.parse("2026-01-15") }
describe "GET /day_logs/edit" do
context "when not authenticated" do
it "redirects to the login page" do
get edit_day_log_for_day_path(day: day)
expect(response).to redirect_to(new_session_path)
end
end
context "when authenticated" do
before { login_as(user) }
context "with an existing day_log for the day" do
let!(:day_log) { create(:day_log, user: user, day: day) }
it "returns http success" do
get edit_day_log_for_day_path(day: day)
expect(response).to have_http_status(:success)
end
end
context "without an existing day_log for the day" do
it "returns http success" do
get edit_day_log_for_day_path(day: day)
expect(response).to have_http_status(:success)
end
it "does not persist a day_log" do
expect {
get edit_day_log_for_day_path(day: day)
}.not_to change(DayLog, :count)
end
end
end
end
describe "POST /day_logs" do
context "when not authenticated" do
it "redirects to the login page" do
post day_logs_path, params: { day_log: { day: day, info: "Une info" } }
expect(response).to redirect_to(new_session_path)
end
end
context "when authenticated" do
before { login_as(user) }
context "with valid params" do
context "without mode" do
it "creates a day_log" do
expect {
post day_logs_path, params: { day_log: { day: day, info: "Une info" } }
}.to change(DayLog, :count).by(1)
end
it "does not create a mood" do
expect {
post day_logs_path, params: { day_log: { day: day, info: "Une info" } }
}.not_to change(Mood, :count)
end
end
context "with mode, no existing mood" do
it "creates a day_log" do
expect {
post day_logs_path, params: { day_log: { day: day, info: "Une info", mode_id: mode.id } }
}.to change(DayLog, :count).by(1)
end
it "creates a mood" do
expect {
post day_logs_path, params: { day_log: { day: day, info: "Une info", mode_id: mode.id } }
}.to change(Mood, :count).by(1)
end
end
context "with mode, existing mood" do
let!(:existing_mood) { create(:mood, user: user, mode: mode, recorded_at: DateTime.parse("2026-01-15 08:00:00")) }
let(:other_mode) { create(:mode, user: user) }
it "creates a day_log" do
expect {
post day_logs_path, params: { day_log: { day: day, info: "Une info", mode_id: other_mode.id } }
}.to change(DayLog, :count).by(1)
end
it "does not create a new mood" do
expect {
post day_logs_path, params: { day_log: { day: day, info: "Une info", mode_id: other_mode.id } }
}.not_to change(Mood, :count)
end
it "updates the existing mood mode" do
post day_logs_path, params: { day_log: { day: day, info: "Une info", mode_id: other_mode.id } }
expect(existing_mood.reload.mode).to eq(other_mode)
end
end
it "redirects after creation" do
post day_logs_path, params: { day_log: { day: day, info: "Une info" } }
expect(response).to have_http_status(:redirect)
end
end
context "with invalid params" do
it "does not create a day_log" do
expect {
post day_logs_path, params: { day_log: { day: nil, info: nil } }
}.not_to change(DayLog, :count)
end
it "returns unprocessable content" do
post day_logs_path, params: { day_log: { day: nil, info: nil } }
expect(response).to have_http_status(:unprocessable_content)
end
end
end
end
describe "GET /day_logs/:id/edit" do
let!(:day_log) { create(:day_log, user: user, day: day) }
context "when not authenticated" do
it "redirects to the login page" do
get edit_day_log_path(day_log)
expect(response).to redirect_to(new_session_path)
end
end
context "when authenticated" do
before { login_as(user) }
it "returns http success" do
get edit_day_log_path(day_log)
expect(response).to have_http_status(:success)
end
end
end
describe "GET /day_logs/edit" do
context "when not authenticated" do
it "redirects to the login page" do
get edit_day_log_for_day_path(day: day)
expect(response).to redirect_to(new_session_path)
end
end
context "when authenticated" do
before { login_as(user) }
context "with an existing day_log for the day" do
let!(:day_log) { create(:day_log, user: user, day: day) }
it "returns http success" do
get edit_day_log_for_day_path(day: day)
expect(response).to have_http_status(:success)
end
end
context "without an existing day_log for the day" do
it "returns http success" do
get edit_day_log_for_day_path(day: day)
expect(response).to have_http_status(:success)
end
it "does not persist a day_log" do
expect {
get edit_day_log_for_day_path(day: day)
}.not_to change(DayLog, :count)
end
end
end
end
describe "PATCH /day_logs/:id" do
let!(:day_log) { create(:day_log, user: user, day: day) }
context "when not authenticated" do
it "redirects to the login page" do
patch day_log_path(day_log), params: { day_log: { info: "Nouvelle info" } }
expect(response).to redirect_to(new_session_path)
end
end
context "when authenticated" do
before { login_as(user) }
context "with valid params" do
context "without mode" do
it "updates the day_log" do
patch day_log_path(day_log), params: { day_log: { info: "Nouvelle info" } }
expect(day_log.reload.info).to eq("Nouvelle info")
end
it "does not create a mood" do
expect {
patch day_log_path(day_log), params: { day_log: { info: "Nouvelle info" } }
}.not_to change(Mood, :count)
end
it "redirects to dashboard" do
patch day_log_path(day_log), params: { day_log: { info: "Nouvelle info" } }
expect(response).to redirect_to(dashboard_path)
end
end
context "with mode, no existing mood" do
it "creates a mood" do
expect {
patch day_log_path(day_log), params: { day_log: { info: "Nouvelle info", mode_id: mode.id } }
}.to change(Mood, :count).by(1)
end
end
context "with mode, existing mood" do
let!(:existing_mood) { create(:mood, user: user, mode: mode, recorded_at: DateTime.parse("2026-01-15 08:00:00")) }
let(:other_mode) { create(:mode, user: user) }
it "does not create a new mood" do
expect {
patch day_log_path(day_log), params: { day_log: { info: "Nouvelle info", mode_id: other_mode.id } }
}.not_to change(Mood, :count)
end
it "updates the existing mood mode" do
patch day_log_path(day_log), params: { day_log: { info: "Nouvelle info", mode_id: other_mode.id } }
expect(existing_mood.reload.mode).to eq(other_mode)
end
end
end
context "with invalid params" do
it "does not update the day_log" do
patch day_log_path(day_log), params: { day_log: { info: nil } }
expect(day_log.reload.info).not_to be_nil
end
it "returns unprocessable content" do
patch day_log_path(day_log), params: { day_log: { info: nil } }
expect(response).to have_http_status(:unprocessable_content)
end
end
end
end
end

View file

@ -0,0 +1,12 @@
module AuthenticationHelper
def login_as(user)
post session_path, params: {
email_address: user.email_address,
password: user.password
}
end
end
RSpec.configure do |config|
config.include AuthenticationHelper, type: :request
end