Merge branch 'main' into column-perma
* main: (47 commits) Remove overflow from columns Only reverse Watching button if icon_only Move video captions below content Account#slug uses the virtual "tenant" attribute Rename LOCAL_AUTHENTICATION to OSS_CONFIG Add a script to generate unique passwords per email address Visually hide the pagination indicator Use hide avatars from tabindex on preview Render a helpful menu of tenants temporarily Remove code related to Launchpad auth or 37id Users Formatting Fix missing close bracket from merge Reverse involvement buttons if watching Only render the manifest link for tenanted URLs Move load-balancer to fizzy-lb-101 Add otel_collector accessory for load balancer Fix: this was getting idiomorph confused as it was being invoked on many subtree elements Keep column popups during page refreshes Don't render next link unless there is more activity Live updates for the collection perma ...
This commit is contained in:
@@ -71,7 +71,7 @@ group :test do
|
||||
end
|
||||
|
||||
require_relative "lib/bootstrap"
|
||||
unless Bootstrap.local_authentication?
|
||||
unless Bootstrap.oss_config?
|
||||
eval_gemfile "gems/fizzy-saas/Gemfile"
|
||||
gem "fizzy-saas", path: "gems/fizzy-saas"
|
||||
end
|
||||
|
||||
+1
-1
@@ -359,7 +359,7 @@ GEM
|
||||
logger (~> 1.6)
|
||||
letter_opener (1.10.0)
|
||||
launchy (>= 2.2, < 4)
|
||||
lexxy (0.1.9.beta)
|
||||
lexxy (0.1.10.beta)
|
||||
rails (>= 8.0.2)
|
||||
lint_roller (1.1.0)
|
||||
logger (1.7.0)
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m3 5h8v2h-8zm10 0h8v2h-8zm-10 4h8v2h-8zm10 0h8v2h-8zm-10 4h8v2h-8zm10 0h8v2h-8zm-10 4h8v2h-8z"/></svg>
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m21.8 0h-19.6c-1.2 0-2.2 1-2.2 2.2v19.5c0 1.2 1 2.2 2.2 2.2h19.5c1.2 0 2.2-1 2.2-2.2v-19.5c0-1.2-1-2.2-2.2-2.2zm-10.8 19h-8v-2h8zm0-4h-8v-2h8zm0-4h-8v-2h8zm0-4h-8v-2h8zm10 8h-8v-2h8zm0-4h-8v-2h8zm0-4h-8v-2h8z"/></svg>
|
||||
|
Before Width: | Height: | Size: 171 B After Width: | Height: | Size: 286 B |
@@ -73,10 +73,6 @@
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&:not(.cards--considering, .is-collapsed, .cards--grid) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
&.is-collapsed {
|
||||
border-radius: calc(var(--column-width-collapsed) / 2);
|
||||
flex: 0 0 var(--column-width-collapsed);
|
||||
|
||||
@@ -97,6 +97,17 @@
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.events__filter-select {
|
||||
font-weight: inherit;
|
||||
text-decoration: underline;
|
||||
|
||||
@media (any-hover: hover) {
|
||||
&:hover {
|
||||
--btn-color: var(--color-link);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.events__day {
|
||||
position: relative;
|
||||
|
||||
|
||||
@@ -31,12 +31,6 @@
|
||||
position: absolute;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
/* Disable the submit button when Lexical is empty */
|
||||
+ .comment__submit {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -248,7 +248,7 @@
|
||||
}
|
||||
|
||||
.attachment--file {
|
||||
--attachment-icon-color: var(--lexxy-color-text-subtle);
|
||||
--attachment-icon-color: var(--color-ink-medium);
|
||||
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@@ -266,6 +266,15 @@
|
||||
color: var(--color-ink);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Video attachments don't have an identifiable class, but we need to
|
||||
* make sure the caption is always below the video */
|
||||
&:has(video) {
|
||||
.attachment__caption {
|
||||
flex: none;
|
||||
inline-size: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.attachment--psd,
|
||||
|
||||
@@ -27,7 +27,9 @@ class CardsController < ApplicationController
|
||||
end
|
||||
|
||||
def update
|
||||
@card.update! card_params
|
||||
suppressing_broadcasts_unless_published(@card) do
|
||||
@card.update! card_params
|
||||
end
|
||||
|
||||
if @card.published?
|
||||
render_card_replacement
|
||||
@@ -50,6 +52,14 @@ class CardsController < ApplicationController
|
||||
@card = Current.user.accessible_cards.find params[:id]
|
||||
end
|
||||
|
||||
def suppressing_broadcasts_unless_published(card, &block)
|
||||
if card.published?
|
||||
yield
|
||||
else
|
||||
Collection.suppressing_turbo_broadcasts(&block)
|
||||
end
|
||||
end
|
||||
|
||||
def card_params
|
||||
params.expect(card: [ :status, :title, :description, :image, tag_ids: [] ])
|
||||
end
|
||||
|
||||
@@ -37,7 +37,9 @@ module Authentication
|
||||
end
|
||||
|
||||
def require_tenant
|
||||
ApplicationRecord.current_tenant.present? || request_authentication
|
||||
unless ApplicationRecord.current_tenant.present?
|
||||
render "sessions/login_menu"
|
||||
end
|
||||
end
|
||||
|
||||
def require_authentication
|
||||
|
||||
@@ -24,7 +24,7 @@ class UsersController < ApplicationController
|
||||
|
||||
def show
|
||||
@filter = Current.user.filters.new(creator_ids: [ @user.id ])
|
||||
@day_timeline = Current.user.timeline_for(Time.current, filter: @filter)
|
||||
@day_timeline = Current.user.timeline_for(day_param, filter: @filter)
|
||||
end
|
||||
|
||||
def update
|
||||
@@ -50,6 +50,14 @@ class UsersController < ApplicationController
|
||||
head :forbidden unless Current.user.can_change?(@user)
|
||||
end
|
||||
|
||||
def day_param
|
||||
if params[:day].present?
|
||||
Time.zone.parse(params[:day])
|
||||
else
|
||||
Time.current
|
||||
end
|
||||
end
|
||||
|
||||
def user_params
|
||||
params.expect(user: [ :name, :email_address, :password, :avatar ])
|
||||
end
|
||||
|
||||
@@ -50,7 +50,7 @@ module AccessesHelper
|
||||
collection_involvement_path(collection),
|
||||
method: :put,
|
||||
aria: { labelledby: involvement_label_id },
|
||||
class: "btn",
|
||||
class: class_names("btn", { "btn--reversed": access.involvement == "watching" && icon_only }),
|
||||
params: { show_watchers: show_watchers, involvement: next_involvement(access.involvement), icon_only: icon_only }
|
||||
) do
|
||||
safe_join([
|
||||
|
||||
@@ -52,17 +52,6 @@ module EventsHelper
|
||||
tag.div class: "events__time-block", style: "grid-area: #{row}/#{col}", &
|
||||
end
|
||||
|
||||
def event_next_page_link(next_day)
|
||||
if next_day
|
||||
tag.div id: "next_page",
|
||||
data: { controller: "fetch-on-visible",
|
||||
fetch_on_visible_url_value: events_days_path(
|
||||
day: next_day.strftime("%Y-%m-%d"),
|
||||
**@filter.as_params
|
||||
) }
|
||||
end
|
||||
end
|
||||
|
||||
def event_action_sentence(event)
|
||||
if event.action.comment_created?
|
||||
comment_event_action_sentence(event)
|
||||
|
||||
@@ -35,6 +35,30 @@ module PaginationHelper
|
||||
end
|
||||
end
|
||||
|
||||
def with_automatic_pagination(name, page, **properties)
|
||||
pagination_list name, paginate_on_scroll: true, **properties do
|
||||
concat(pagination_frame_tag(name, page) do
|
||||
yield
|
||||
concat link_to_next_page(name, page, activate_when_observed: true)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
def day_timeline_pagination_frame_tag(day_timeline, &)
|
||||
turbo_frame_tag day_timeline_pagination_frame_id_for(day_timeline.day), data: { timeline_target: "frame" }, role: "presentation", refresh: :morph, &
|
||||
end
|
||||
|
||||
def day_timeline_pagination_frame_id_for(day)
|
||||
"day-timeline-pagination-contents-#{day.strftime("%Y-%m-%d")}"
|
||||
end
|
||||
|
||||
def day_timeline_pagination_link(day_timeline, filter)
|
||||
if day_timeline.next_day
|
||||
link_to "Load more…", events_days_path(day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params),
|
||||
class: "txt-reversed", data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" }
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def pagination_list(name, tag_element: :div, paginate_on_scroll: false, **properties, &block)
|
||||
classes = properties.delete(:class)
|
||||
|
||||
@@ -14,7 +14,7 @@ export default class extends Controller {
|
||||
}
|
||||
|
||||
preventToggle(event) {
|
||||
if (event.detail.attributeName === "class") {
|
||||
if (event.target.hasAttribute("data-collapsible-columns-target") && event.detail.attributeName === "class") {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { post } from "@rails/request.js"
|
||||
export default class extends Controller {
|
||||
static classes = ["filtersSet"]
|
||||
static targets = ["field", "form"]
|
||||
static values = { refreshUrl: String, noFilteringUrl: String }
|
||||
static values = { refreshUrl: String, noFilteringUrl: String, cardsUrl: String }
|
||||
|
||||
initialize() {
|
||||
this.debouncedToggle = debounce(this.#toggle.bind(this), 50)
|
||||
@@ -31,6 +31,12 @@ export default class extends Controller {
|
||||
this.debouncedToggle()
|
||||
}
|
||||
|
||||
submitToGenericCardsView() {
|
||||
this.formTarget.action = this.cardsUrlValue
|
||||
this.formTarget.dataset.turboFrame = "top"
|
||||
this.formTarget.requestSubmit()
|
||||
}
|
||||
|
||||
#toggle() {
|
||||
this.element.classList.toggle(this.filtersSetClass, this.#hasFiltersSet)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Controller } from "@hotwired/stimulus"
|
||||
import { debounce } from "helpers/timing_helpers";
|
||||
import { debounce, nextFrame } from "helpers/timing_helpers";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = [ "cancel" ]
|
||||
static targets = [ "cancel", "submit" ]
|
||||
|
||||
static values = {
|
||||
debounceTimeout: { type: Number, default: 300 }
|
||||
@@ -37,6 +37,16 @@ export default class extends Controller {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
async disableSubmitWhenInvalid(event) {
|
||||
await nextFrame()
|
||||
|
||||
if (this.element.checkValidity()) {
|
||||
this.submitTarget.removeAttribute("disabled")
|
||||
} else {
|
||||
this.submitTarget.toggleAttribute("disabled", true)
|
||||
}
|
||||
}
|
||||
|
||||
select(event) {
|
||||
event.target.select()
|
||||
}
|
||||
|
||||
@@ -82,15 +82,14 @@ export default class extends Controller {
|
||||
|
||||
#clearHiddenFields() {
|
||||
this.element.querySelectorAll('input[type="hidden"]').forEach(field => {
|
||||
if (field !== this.hiddenField) {
|
||||
field.remove()
|
||||
}
|
||||
field.remove()
|
||||
})
|
||||
}
|
||||
|
||||
#addHiddenFields() {
|
||||
this.#selectedValues().forEach(value => {
|
||||
const [field] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children
|
||||
const [ field ] = this.hiddenFieldTemplateTarget.content.cloneNode(true).children
|
||||
field.removeAttribute("id")
|
||||
field.value = value
|
||||
this.element.appendChild(field)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ class Account < ApplicationRecord
|
||||
has_many_attached :uploads
|
||||
|
||||
def slug
|
||||
"/#{tenant_id}"
|
||||
"/#{tenant}"
|
||||
end
|
||||
|
||||
def setup_basic_template
|
||||
|
||||
@@ -22,7 +22,6 @@ class Card < ApplicationRecord
|
||||
case index
|
||||
when "stalled" then stalled
|
||||
when "postponing_soon" then postponing_soon
|
||||
when "falling_back_soon" then falling_back_soon
|
||||
when "closed" then closed.recently_closed_first
|
||||
when "golden" then golden
|
||||
when "draft" then drafted
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module Filter::Fields
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
INDEXES = %w[ all stalled postponing_soon falling_back_soon golden draft ]
|
||||
INDEXES = %w[ all stalled postponing_soon golden draft ]
|
||||
SORTED_BY = %w[ newest oldest latest ]
|
||||
|
||||
delegate :default_value?, to: :class
|
||||
|
||||
@@ -67,6 +67,10 @@ class User::Filtering
|
||||
filter.closers.any?
|
||||
end
|
||||
|
||||
def show_collections?
|
||||
filter.collections.any?
|
||||
end
|
||||
|
||||
def enable_collection_filtering(&block)
|
||||
@collection_filtering_route_resolver = block
|
||||
end
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
local_save_key_value: "comment-#{card.id}",
|
||||
action: "turbo:submit-end->local-save#submit keydown.ctrl+enter->form#submit:prevent keydown.meta+enter->form#submit:prevent keydown.esc->form#cancel:stop" } do |form| %>
|
||||
<%= form.rich_textarea :body, required: true, placeholder: new_comment_placeholder(card),
|
||||
data: { local_save_target: "input", action: "lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
|
||||
data: { local_save_target: "input", action: "lexxy:change->form#disableSubmitWhenInvalid lexxy:change->local-save#save turbo:morph-element->local-save#restoreContent" } do %>
|
||||
<%= general_prompts(@card.collection) %>
|
||||
<% end %>
|
||||
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start" do %>
|
||||
<%= form.button class: "comment__submit btn btn--reversed flex-item-justify-start", data: { form_target: "submit" }, disabled: true do %>
|
||||
<span>Post this comment</span>
|
||||
<% end %>
|
||||
<span data-form-target="cancel" hidden></span>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
</div>
|
||||
|
||||
<footer class="card__footer">
|
||||
<%= render "cards/display/preview/meta", card: card %>
|
||||
<%= render "cards/display/preview/meta", card: card, preview: true %>
|
||||
<%= render "cards/display/common/background", card: card %>
|
||||
</footer>
|
||||
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
|
||||
<%= turbo_frame_tag :cards_container do %>
|
||||
<section class="cards cards--grid">
|
||||
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
|
||||
<%= with_automatic_pagination :cards_paginated_container, @page do %>
|
||||
<%= render partial: "cards/display/preview", collection: @page.records, as: :card, locals: { draggable: true }, cached: ->(card) { cacheable_preview_parts_for(card) } %>
|
||||
<% end %>
|
||||
</section>
|
||||
<% end %>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<% content_for :header do %>
|
||||
<%= render "filters/menu", user_filtering: @user_filtering %>
|
||||
<div class="header__actions header__actions--start">
|
||||
<%= link_to cards_path(collection_ids: [ @collection ]), class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
|
||||
<%= link_to @collection, class: "btn borderless txt-medium", data: { controller: "hotkey", action: "keydown.esc@document->hotkey#click" } do %>
|
||||
<span class="overflow-ellipsis">
|
||||
←
|
||||
<strong><%= @collection.name %></strong>
|
||||
@@ -38,7 +38,6 @@
|
||||
</div>
|
||||
|
||||
<div class="settings__panel panel shadow center">
|
||||
<%# render "collections/edit/workflows", collection: @collection %>
|
||||
<%= render "collections/edit/auto_close", collection: @collection %>
|
||||
<%= render "collections/edit/publication", collection: @collection %>
|
||||
<%= render "collections/edit/delete", collection: @collection %>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<% @page_title = @collection.name %>
|
||||
<% turbo_exempts_page_from_cache %>
|
||||
|
||||
<%= turbo_stream_from @collection %>
|
||||
|
||||
<% content_for :head do %>
|
||||
<%= tag.meta property: "og:title", content: @page_title %>
|
||||
<%= tag.meta property: "og:description", content: Account.sole.name %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<div class="cards__expander-menu" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
|
||||
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open">
|
||||
<%= icon_tag "menu-dots-horizontal", class: "translucent" %>
|
||||
<span class="for-screen-reader">Column options</span>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<div class="position-relative" data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
|
||||
<button class="btn btn--circle txt-x-small borderless" data-action="click->dialog#open:stop" aria-label="Add a new column, etc.">
|
||||
<%= icon_tag "add", class: "translucent", "aria-hidden": true %>
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
<%= day_timeline_pagination_frame_tag @day_timeline do %>
|
||||
<%= render "events/day", day_timeline: @day_timeline %>
|
||||
<%= day_timeline_pagination_link(@day_timeline, @filter) %>
|
||||
<% end %>
|
||||
@@ -1,7 +0,0 @@
|
||||
<%= turbo_stream.append :activity do %>
|
||||
<%= render "events/day", day_timeline: @day_timeline %>
|
||||
<% end %>
|
||||
|
||||
<%= turbo_stream.replace :next_page do %>
|
||||
<%= event_next_page_link(@day_timeline.next_day) %>
|
||||
<% end %>
|
||||
@@ -1,9 +1,11 @@
|
||||
<%= link_to event.notifiable_target,
|
||||
class: "event event--#{ event.action } #{ "golden-effect" if event.card.golden? } center-block flex flex-column full-width align-start justify-start position-relative",
|
||||
style: "--card-color: #{ card.closed? ? "var(--color-card-complete)" : card.color };",
|
||||
data: { related_element_target: "related",
|
||||
related_element_group_value: card.id,
|
||||
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
|
||||
target: "_top",
|
||||
data: {
|
||||
related_element_target: "related",
|
||||
related_element_group_value: card.id,
|
||||
action: "mouseover->related-element#highlight mouseout->related-element#unhighlight" } do %>
|
||||
<div class="card__header flex align-start gap full-width">
|
||||
<h4 class="card__collection flex-inline align-start">
|
||||
<span class="card__id"><%= card.id %></span>
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
<% @page_title = "Home" %>
|
||||
<% @header_class = "header--events" %>
|
||||
|
||||
<%= turbo_stream_from :activity_summaries %>
|
||||
<%= render "filters/broadcasts", filter: @filter %>
|
||||
|
||||
<% content_for :header do %>
|
||||
<%= render "filters/menu", user_filtering: @user_filtering %>
|
||||
<%= render "events/index/add_card_button", user_filtering: @user_filtering %>
|
||||
|
||||
<h1 class="header__title overflow-ellipsis">
|
||||
<div class="overflow-ellipsis">
|
||||
Activity in <%= @user_filtering.selected_collections_label %>
|
||||
</div>
|
||||
<h1 class="header__title">
|
||||
<span>Activity in</span>
|
||||
<%= render "events/index/filter", user_filtering: @user_filtering %>
|
||||
</h1>
|
||||
<% end %>
|
||||
|
||||
<div class="events" id="activity">
|
||||
<%= render "events/day", day_timeline: @day_timeline %>
|
||||
</div>
|
||||
<%= event_next_page_link(@day_timeline.next_day) %>
|
||||
<%= tag.div id: "activity", class: "events", data: { controller: "pagination", pagination_paginate_on_intersection_value: true } do %>
|
||||
<%= day_timeline_pagination_frame_tag @day_timeline do %>
|
||||
<%= render "events/day", day_timeline: @day_timeline %>
|
||||
|
||||
<%= day_timeline_pagination_link(@day_timeline, @filter) %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
<% filter = user_filtering.filter %>
|
||||
|
||||
<%= form_with url: events_path,
|
||||
method: :get, class: "flex-inline position-relative",
|
||||
data: { controller: "form", turbo_frame: "cards_container", turbo_action: "advance" } do |form| %>
|
||||
<%= tag.div class: "flex-inline position-relative quick-filter",
|
||||
data: {
|
||||
controller: "dialog multi-selection-combobox",
|
||||
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
|
||||
filter_show: user_filtering.show_collections?,
|
||||
multi_selection_combobox_no_selection_label_value: user_filtering.selected_collections_label } do %>
|
||||
<button type="button" class="btn borderless btn--plain events__filter-select" data-action="click->dialog#toggle:stop">
|
||||
<span class="overflow-ellipsis" data-multi-selection-combobox-target="label">
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<template data-multi-selection-combobox-target="hiddenFieldTemplate">
|
||||
<%= hidden_field_tag "collection_ids[]", nil, data: { filter_settings_target: "field" } %>
|
||||
</template>
|
||||
|
||||
<%= filter_dialog "Collection…" do %>
|
||||
<strong class="popup__title">Collection…</strong>
|
||||
|
||||
<% if user_filtering.collections.many? %>
|
||||
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small font-weight-normal", autofocus: true,
|
||||
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
|
||||
<% end %>
|
||||
|
||||
<ul class="popup__list" data-filter-target="list" role="listbox">
|
||||
<% user_filtering.collections.each do |collection| %>
|
||||
<%= tag.li class: "popup__item", data: {
|
||||
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: collection.id, multi_selection_combobox_label: collection.name },
|
||||
role: "checkbox", aria: { checked: filter.collections.include?(collection) } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change form#submit">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= collection.name %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,4 +1,4 @@
|
||||
<nav data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside">
|
||||
<nav data-controller="dialog" data-action="keydown.esc->dialog#close click@document->dialog#closeOnClickOutside" data-turbo-permanent>
|
||||
<%= render "filters/menu/button" %>
|
||||
|
||||
<%= tag.dialog class: "fizzy-menu filter popup popup--animated panel margin-block-start-half",
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<% cache user_filtering do %>
|
||||
<%= tag.aside id: "filter-settings",
|
||||
<%= tag.aside \
|
||||
class: class_names("filters flex align-center gap-half justify-center center margin-block-end", { "filters--expanded": user_filtering.expanded? }),
|
||||
data: {
|
||||
controller: "toggle-class filter-settings",
|
||||
toggle_class_toggle_class: "filters--expanded",
|
||||
filter_settings_filters_set_class: "filters--has-filters-set",
|
||||
filter_settings_no_filtering_url_value: no_filtering_url,
|
||||
filter_settings_refresh_url_value: settings_refresh_path } do %>
|
||||
filter_settings_refresh_url_value: settings_refresh_path,
|
||||
filter_settings_cards_url_value: cards_path,
|
||||
turbo_permanent: true } do %>
|
||||
<%= form_with url: filter_url, method: :get, class: "", data: {
|
||||
controller: "form",
|
||||
turbo_frame: "cards_container",
|
||||
@@ -14,9 +16,6 @@
|
||||
action: "turbo:submit-end->filter-settings#resetIfNoFiltering",
|
||||
turbo_action: "advance" } do |form| %>
|
||||
<%= hidden_field_tag :expand_all, true, disabled: !user_filtering.expanded?, data: { toggle_enable_target: "element"} %>
|
||||
<% user_filtering.filter.collections.each do |collection| %>
|
||||
<%= filter_hidden_field_tag "collection_ids[]", collection.id %>
|
||||
<% end %>
|
||||
|
||||
<%= yield form if block_given? %>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<%= tag.button class:"fizzy-menu-trigger input input--select center flex-inline align-center txt-normal",
|
||||
data: {
|
||||
action: "click->dialog#open:stop keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
|
||||
action: "click->dialog#open:stop keydown.j@document->hotkey#click keydown.meta+j@document->hotkey#click keydown.ctrl+j@document->hotkey#click",
|
||||
controller: "hotkey" } do %>
|
||||
<svg fill="none" height="282" viewBox="0 0 178 282" width="178" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<path d="m14.1582 85.2218s66.3075-9.7823 75.9322-9.7823c9.6242 0 73.9516 9.7823 73.9516 9.7823-6.535 65.4602-17.171 132.0422-21.661 160.5982 0 0-16.941 11.257-51.3278 11.257s-55.9319-12.654-55.9319-12.654c-4.4901-28.557-14.4184-93.741-20.9631-159.2012z" fill="#d2edf8"/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<% if platform.desktop? %>
|
||||
<footer class="popup__footer">
|
||||
Press <kbd class="kbd"><%= hotkey_label(["ctrl", "J"]) -%></kbd> anytime to open this, <kbd class="kbd">esc</kbd> to close, <kbd class="kbd">↑</kbd><kbd class="kbd">↓</kbd> to move, <kbd class="kbd">enter</kbd> to navigate, <kbd class="kbd">SHIFT+ENTER</kbd> to select.
|
||||
Press <kbd class="kbd">J</kbd> anytime to open this, <kbd class="kbd">esc</kbd> to close, <kbd class="kbd">↑</kbd><kbd class="kbd">↓</kbd> to move, <kbd class="kbd">enter</kbd> to navigate.
|
||||
</footer>
|
||||
<% end %>
|
||||
|
||||
@@ -2,15 +2,8 @@
|
||||
|
||||
<% if Current.user.collections.many? %>
|
||||
<li class="popup__item" data-filter-target="item" data-navigable-list-target="item" id="filter-collection-all">
|
||||
<%= link_to clear_url_path, class: "popup__radio btn" do %>
|
||||
<%= check_box_tag "filter_collections", nil, user_filtering.collections.blank?, class: "form-checkbox" %>
|
||||
<span class="for-screen-reader">
|
||||
See everything in all collections
|
||||
</span>
|
||||
<%= icon_tag "check", size: 18, class: "checked" %>
|
||||
<% end %>
|
||||
|
||||
<%= link_to clear_url_path, class: "popup__btn btn" do %>
|
||||
<%= icon_tag "collection", class: "popup__icon" %>
|
||||
<%= link_to cards_path, class: "popup__btn btn" do %>
|
||||
<span class="overflow-ellipsis">All collections</span>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -1,17 +1,5 @@
|
||||
<li class="popup__item" data-filter-target="item" data-navigable-list-target="item" id="filter-collection-<%= collection.id %>">
|
||||
<label class="popup__radio btn">
|
||||
<%= form.check_box "collection_ids[]", {
|
||||
checked: user_filtering.filter.collections.include?(collection),
|
||||
data: { action: "change->form#submit" },
|
||||
include_hidden: false,
|
||||
}, collection.id %>
|
||||
|
||||
<span class="for-screen-reader">
|
||||
Toggle filter for <%= collection.name %>
|
||||
</span>
|
||||
<%= icon_tag "check", size: 18, class: "checked" %>
|
||||
</label>
|
||||
|
||||
<%= icon_tag "collection", class: "popup__icon" %>
|
||||
<%= link_to collection_path(collection), class: "popup__btn btn" do %>
|
||||
<span class="overflow-ellipsis"><%= collection.name %></span>
|
||||
<% end %>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<%= icon_tag "bookmark", class: "popup__icon" %>
|
||||
<%= link_to cards_path(filter_id: filter.id), class: "popup__btn btn" do %>
|
||||
<div class="txt-tight-lines min-width txt-small overflow-ellipsis">
|
||||
<div><%= filter.collections_label %></div>
|
||||
<div><%= filter.summary %></div>
|
||||
<div><strong><%= filter.collections_label %></strong></div>
|
||||
<div class="txt-capitalize"><%= filter.summary %></div>
|
||||
</div>
|
||||
<% end %>
|
||||
</li>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<% filter = user_filtering.filter %>
|
||||
|
||||
<%= tag.div class: "quick-filter position-relative",
|
||||
data: {
|
||||
controller: "dialog multi-selection-combobox",
|
||||
action: "keydown.esc->dialog#close click@document->dialog#closeOnClickOutside",
|
||||
filter_show: user_filtering.show_collections?,
|
||||
multi_selection_combobox_no_selection_label_value: "Collection…",
|
||||
multi_selection_combobox_label_prefix_value: "" } do %>
|
||||
<button type="button" class="btn input input--select flex-inline txt-x-small" data-action="click->dialog#toggle:stop">
|
||||
<span class="overflow-ellipsis" data-multi-selection-combobox-target="label">
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<template data-multi-selection-combobox-target="hiddenFieldTemplate">
|
||||
<%= hidden_field_tag "collection_ids[]", nil, data: { filter_settings_target: "field" } %>
|
||||
</template>
|
||||
|
||||
<%= filter_dialog "Collection…" do %>
|
||||
<strong class="popup__title">Collection…</strong>
|
||||
|
||||
<% if user_filtering.collections.many? %>
|
||||
<%= text_field_tag nil, nil, placeholder: "Filter…", class: "input input--transparent txt-small", autofocus: true,
|
||||
type: "search", autocorrect: "off", autocomplete: "off", data: { "1p-ignore": "true", filter_target: "input", action: "input->filter#filter" } %>
|
||||
<% end %>
|
||||
|
||||
<ul class="popup__list" data-filter-target="list" role="listbox">
|
||||
<% user_filtering.collections.each do |collection| %>
|
||||
<%= tag.li class: "popup__item", data: {
|
||||
filter_target: "item", navigable_list_target: "item", multi_selection_combobox_target: "item", multi_selection_combobox_value: collection.id, multi_selection_combobox_label: collection.name },
|
||||
role: "checkbox", aria: { checked: filter.collections.include?(collection) } do %>
|
||||
<button type="button" class="btn popup__btn" data-action="dialog#close multi-selection-combobox#change filter-settings#change filter-settings#submitToGenericCardsView:stop">
|
||||
<span class="overflow-ellipsis flex-item-grow"><%= collection.name %></span>
|
||||
<%= icon_tag "check", class: "checked flex-item-justify-end", "aria-hidden": true %>
|
||||
</button>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
<% end %>
|
||||
@@ -1,3 +1,4 @@
|
||||
<%= render "filters/settings/collections", user_filtering: user_filtering %>
|
||||
<%= render "filters/settings/sorted_by", user_filtering: user_filtering %>
|
||||
<%= render "filters/settings/indexed_by", user_filtering: user_filtering %>
|
||||
<%= render "filters/settings/tags", user_filtering: user_filtering %>
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
<%= tenanted_action_cable_meta_tag %>
|
||||
<%= yield :head %>
|
||||
|
||||
<link rel="manifest" href="<%= pwa_manifest_path(format: :json) %>">
|
||||
<% if ApplicationRecord.current_tenant %>
|
||||
<link rel="manifest" href="<%= pwa_manifest_path(format: :json) %>">
|
||||
<% end %>
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="/favicon.png" type="image/png">
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<% cache ApplicationRecord.tenants do %>
|
||||
<h2>We're migrating Fizzy authentication</h2>
|
||||
<p>
|
||||
While we migrate Fizzy away from Launchpad and 37id, please login at one of the following URLs:
|
||||
|
||||
<ul style="list-style-type: none;">
|
||||
<% ApplicationRecord.with_each_tenant do |tenant| %>
|
||||
<li><%= link_to Account.sole.name, root_url(script_name: Account.sole.slug) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</p>
|
||||
<p>
|
||||
Mike should have sent you your temporary password by now. If you haven't received it, please ping him!
|
||||
</p>
|
||||
<% end %>
|
||||
@@ -0,0 +1,13 @@
|
||||
<div class="events margin-block-double" id="activity" data-controller="pagination" data-pagination-paginate-on-intersection-value="true">
|
||||
<%= day_timeline_pagination_frame_tag day_timeline do %>
|
||||
<%= render "events/day", day_timeline: day_timeline %>
|
||||
|
||||
<% if day_timeline.next_day %>
|
||||
<%= link_to "Load more…", user_path(user, day: day_timeline.next_day.strftime("%Y-%m-%d"), **filter.as_params),
|
||||
class: "txt-reversed", data: { frame: day_timeline_pagination_frame_id_for(day_timeline.next_day), pagination_target: "paginationLink" } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
@@ -50,7 +50,4 @@
|
||||
|
||||
<h1 class="font-weight-black txt-large margin-block-double"><%= "What #{ Current.user == @user ? "have you" : "has #{ @user.first_name }" } been up to?" %></h1>
|
||||
|
||||
<div class="events margin-block-double" id="activity">
|
||||
<%= render "events/day", day_timeline: @day_timeline %>
|
||||
</div>
|
||||
<%= event_next_page_link(@day_timeline.next_day) %>
|
||||
<%= render "users/activity_timeline", user: @user, day_timeline: @day_timeline, filter: @filter %>
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
echo "Login with david@37signals.com / secret123456 to:"
|
||||
bin/rails runner - <<EOF
|
||||
puts "Login with david@37signals.com / secret123456 to:"
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
next unless tenant =~ /\A\d+\z/
|
||||
if Account.sole.tenant_id
|
||||
puts " - #{Account.sole.name}: http://fizzy.localhost:3006/#{Account.sole.tenant_id}"
|
||||
end
|
||||
puts " - #{Account.sole.name}: http://fizzy.localhost:3006#{Account.sole.slug}"
|
||||
end
|
||||
EOF
|
||||
|
||||
@@ -14,8 +12,8 @@ if [ -f tmp/solid-queue.txt ]; then
|
||||
export SOLID_QUEUE_IN_PUMA=1
|
||||
fi
|
||||
|
||||
if [ -f tmp/local-auth.txt ]; then
|
||||
export LOCAL_AUTHENTICATION=1
|
||||
if [ -f tmp/oss-config.txt ]; then
|
||||
export OSS_CONFIG=1
|
||||
fi
|
||||
|
||||
exec ./bin/rails server -p 3006
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env ruby
|
||||
require_relative "../lib/bootstrap"
|
||||
if !Bootstrap.local_authentication?
|
||||
if !Bootstrap.oss_config?
|
||||
# default from rails/test_unit/runner.rb but adding the saas gem test files
|
||||
ENV["DEFAULT_TEST"] = "{gems/fizzy-saas/,}test/**/*_test.rb"
|
||||
ENV["DEFAULT_TEST_EXCLUDE"] = "{gems/fizzy-saas/,}test/{system,dummy,fixtures}/**/*_test.rb"
|
||||
|
||||
@@ -11,7 +11,7 @@ system("bundle config set --local auto_install true")
|
||||
system("bundle check") || system!("bundle install")
|
||||
|
||||
require_relative "../lib/bootstrap"
|
||||
load File.expand_path("../gems/fizzy-saas/bin/setup", __dir__) unless Bootstrap.local_authentication?
|
||||
load File.expand_path("../gems/fizzy-saas/bin/setup", __dir__) unless Bootstrap.oss_config?
|
||||
|
||||
puts "\n== Preparing database =="
|
||||
if ARGV.include?("--reset")
|
||||
|
||||
+4
-4
@@ -9,10 +9,10 @@ CI.run do
|
||||
step "Security: Importmap audit", "bin/importmap audit"
|
||||
step "Security: Brakeman audit", "bin/brakeman --quiet --no-pager --exit-on-warn --exit-on-error"
|
||||
|
||||
step "Tests: Rails with 37id auth", "bin/rails test"
|
||||
step "Tests: Rails with local auth", "LOCAL_AUTHENTICATION=1 bin/rails test"
|
||||
step "Tests: 37id", "bin/rails 37id:test:units"
|
||||
step "Tests: System", "bin/rails test:system"
|
||||
step "Tests: Rails: SaaS config", "bin/rails test"
|
||||
step "Tests: Rails: OSS config", "OSS_CONFIG=1 bin/rails test"
|
||||
step "Tests: 37id", "bin/rails 37id:test:units"
|
||||
step "Tests: System", "bin/rails test:system"
|
||||
|
||||
if success?
|
||||
step "Signoff: All systems go. Ready for merge and deploy.", "gh signoff"
|
||||
|
||||
+12
-2
@@ -39,11 +39,21 @@ accessories:
|
||||
|
||||
load-balancer:
|
||||
image: basecamp/kamal-proxy:lb
|
||||
roles:
|
||||
- web
|
||||
host: fizzy-beta-lb-01
|
||||
options:
|
||||
publish:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
|
||||
|
||||
otel_collector-load-balancer:
|
||||
image: otel/opentelemetry-collector-contrib:0.126.0
|
||||
port: 9394
|
||||
files:
|
||||
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
options:
|
||||
user: 0
|
||||
host: fizzy-beta-lb-01
|
||||
|
||||
@@ -39,11 +39,21 @@ accessories:
|
||||
|
||||
load-balancer:
|
||||
image: basecamp/kamal-proxy:lb
|
||||
roles:
|
||||
- web
|
||||
host: fizzy-lb-101
|
||||
options:
|
||||
publish:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
|
||||
|
||||
otel_collector-load-balancer:
|
||||
image: otel/opentelemetry-collector-contrib:0.126.0
|
||||
port: 9394
|
||||
files:
|
||||
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
options:
|
||||
user: 0
|
||||
host: fizzy-lb-101
|
||||
|
||||
@@ -39,11 +39,21 @@ accessories:
|
||||
|
||||
load-balancer:
|
||||
image: basecamp/kamal-proxy:lb
|
||||
roles:
|
||||
- web
|
||||
host: fizzy-staging-lb-01
|
||||
options:
|
||||
publish:
|
||||
- 80:80
|
||||
- 443:443
|
||||
volumes:
|
||||
- load-balancer:/home/kamal-proxy/.config/kamal-proxy
|
||||
|
||||
otel_collector-load-balancer:
|
||||
image: otel/opentelemetry-collector-contrib:0.126.0
|
||||
port: 9394
|
||||
files:
|
||||
- config/otel_collector.yml:/etc/otelcol-contrib/config.yaml
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
options:
|
||||
user: 0
|
||||
host: fizzy-staging-lb-01
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
require "bootstrap"
|
||||
Rails.application.config.x.local_authentication = Bootstrap.local_authentication?
|
||||
Rails.application.config.x.oss_config = Bootstrap.oss_config?
|
||||
|
||||
+1
-1
@@ -221,7 +221,7 @@ Rails.application.routes.draw do
|
||||
|
||||
root "events#index"
|
||||
|
||||
unless Rails.application.config.x.local_authentication
|
||||
unless Rails.application.config.x.oss_config
|
||||
mount Fizzy::Saas::Engine, at: "/", as: "saas"
|
||||
end
|
||||
|
||||
|
||||
+11
-40
@@ -13,7 +13,7 @@ end
|
||||
def create_tenant(signal_account_name, bare: false)
|
||||
if bare
|
||||
tenant_id = Digest::SHA256.hexdigest(signal_account_name)[0..8].to_i(16)
|
||||
elsif Rails.application.config.x.local_authentication
|
||||
elsif Rails.application.config.x.oss_config
|
||||
tenant_id = ActiveRecord::FixtureSet.identify signal_account_name
|
||||
else
|
||||
signal_account = SignalId::Account.find_by_product_and_name!("fizzy", signal_account_name)
|
||||
@@ -22,7 +22,7 @@ def create_tenant(signal_account_name, bare: false)
|
||||
|
||||
ApplicationRecord.destroy_tenant tenant_id
|
||||
ApplicationRecord.create_tenant(tenant_id) do
|
||||
account = if bare || Rails.application.config.x.local_authentication
|
||||
account = if bare || Rails.application.config.x.oss_config
|
||||
Account.create(name: signal_account_name, tenant_id: tenant_id).tap do
|
||||
User.create!(
|
||||
name: "David Heinemeier Hansson",
|
||||
@@ -40,45 +40,16 @@ def create_tenant(signal_account_name, bare: false)
|
||||
end
|
||||
|
||||
def find_or_create_user(full_name, email_address)
|
||||
if Rails.application.config.x.local_authentication
|
||||
if user = User.find_by(email_address: email_address)
|
||||
user.password = "secret123456"
|
||||
user.save!
|
||||
user
|
||||
else
|
||||
User.create!(
|
||||
name: full_name,
|
||||
email_address: email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
end
|
||||
if user = User.find_by(email_address: email_address)
|
||||
user.password = "secret123456"
|
||||
user.save!
|
||||
user
|
||||
else
|
||||
SignalId::Database.on_master do
|
||||
unless signal_identity = SignalId::Identity.find_by_email_address(email_address)
|
||||
signal_identity = SignalId::Identity.create!(
|
||||
name: full_name,
|
||||
email_address: email_address,
|
||||
username: email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
end
|
||||
|
||||
signal_account = Account.sole.external_account
|
||||
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
|
||||
|
||||
if user = User.find_by(external_user_id: signal_user.id)
|
||||
user.password = "secret123456"
|
||||
user.save!
|
||||
user
|
||||
else
|
||||
User.create!(
|
||||
external_user_id: signal_user.id,
|
||||
name: signal_identity.name,
|
||||
email_address: signal_identity.email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
end
|
||||
end
|
||||
User.create!(
|
||||
name: full_name,
|
||||
email_address: email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
module Sessions
|
||||
module SignalSessions
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
before_action :require_local_auth, only: %i[ new create ]
|
||||
end
|
||||
|
||||
private
|
||||
def require_local_auth
|
||||
head :forbidden
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,23 +0,0 @@
|
||||
class Sessions::LaunchpadController < ApplicationController
|
||||
require_unauthenticated_access
|
||||
|
||||
before_action :require_sig
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def update
|
||||
user = Account.sole.external_account.authenticate(sig: @sig).try(:peer)
|
||||
if user.present? && user.active?
|
||||
start_new_session_for user
|
||||
redirect_to after_authentication_url
|
||||
else
|
||||
render plain: "Authentication failed. This is probably a bug.", status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def require_sig
|
||||
@sig = params.expect(:sig)
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
module LoginHelper
|
||||
module SignalLogin
|
||||
def login_url
|
||||
if ApplicationRecord.current_tenant
|
||||
Launchpad.login_url(product: true, account: Account.sole)
|
||||
else
|
||||
Launchpad.login_url(product: true)
|
||||
end
|
||||
end
|
||||
|
||||
def logout_url
|
||||
Launchpad.logout_url
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -20,9 +20,8 @@ module Account::SignalAccount
|
||||
User.create!(
|
||||
name: account.external_account.owner.name,
|
||||
email_address: account.external_account.owner.email_address,
|
||||
external_user_id: account.external_account.owner.id,
|
||||
role: "admin",
|
||||
password: SecureRandom.hex(36) # TODO: remove password column?
|
||||
password: SecureRandom.hex(16)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
module User::SignalUser
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
prepended do
|
||||
# the external_user_id is the SignalId::User id
|
||||
belongs_to :external_user, dependent: :destroy, class_name: "SignalId::User", optional: true
|
||||
end
|
||||
|
||||
class_methods do
|
||||
def find_by_signal_user_id(signal_user_id)
|
||||
find_by(external_user_id: signal_user_id)
|
||||
end
|
||||
end
|
||||
|
||||
def deactivate
|
||||
super
|
||||
SignalId::Database.on_master { external_user&.destroy }
|
||||
end
|
||||
end
|
||||
@@ -1,10 +1,4 @@
|
||||
Fizzy::Saas::Engine.routes.draw do
|
||||
resource :session do
|
||||
scope module: "sessions" do
|
||||
resource :launchpad, only: %i[ show update ], controller: "launchpad"
|
||||
end
|
||||
end
|
||||
|
||||
namespace :signup do
|
||||
get "/" => "accounts#new"
|
||||
resources :accounts, only: %i[ new create ]
|
||||
|
||||
@@ -5,10 +5,7 @@ module Fizzy
|
||||
class Engine < ::Rails::Engine
|
||||
# extend application models
|
||||
config.to_prepare do
|
||||
User.prepend User::SignalUser
|
||||
Account.prepend Account::SignalAccount
|
||||
LoginHelper.prepend LoginHelper::SignalLogin
|
||||
SessionsController.include Sessions::SignalSessions
|
||||
end
|
||||
|
||||
# moved from config/initializers/queenbee.rb
|
||||
@@ -39,7 +36,6 @@ module Fizzy
|
||||
|
||||
silence_warnings do
|
||||
SignalId::Account::Peer = Account
|
||||
SignalId::User::Peer = User
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class ControllerAuthenticationTest < ActionDispatch::IntegrationTest
|
||||
test "access without an account slug redirects to launchpad" do
|
||||
integration_session.default_url_options[:script_name] = "" # no tenant
|
||||
|
||||
get cards_path
|
||||
|
||||
assert_redirected_to Launchpad.login_url(product: true)
|
||||
end
|
||||
|
||||
test "access with an account slug but no session redirects to launchpad" do
|
||||
get cards_path
|
||||
|
||||
assert_redirected_to Launchpad.login_url(product: true, account: Account.sole)
|
||||
end
|
||||
end
|
||||
@@ -1,38 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class Sessions::LaunchpadControllerTest < ActionDispatch::IntegrationTest
|
||||
test "show renders when not signed in" do
|
||||
get saas.session_launchpad_path(params: { sig: "test-sig" })
|
||||
|
||||
assert_response :success
|
||||
|
||||
assert_select "form input#sig" do |node|
|
||||
assert_equal node.length, 1
|
||||
assert_equal node.first["value"], "test-sig"
|
||||
end
|
||||
end
|
||||
|
||||
test "create establishes a session when the sig is valid" do
|
||||
user = users(:david)
|
||||
|
||||
put saas.session_launchpad_path(params: { sig: user.external_user.perishable_signature })
|
||||
|
||||
assert_redirected_to root_url
|
||||
assert parsed_cookies.signed[:session_token]
|
||||
end
|
||||
|
||||
test "create checks user.active?" do
|
||||
user = users(:david)
|
||||
user.update! active: false
|
||||
|
||||
put saas.session_launchpad_path(params: { sig: user.external_user.perishable_signature })
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
|
||||
test "returns 401 when the sig is invalid" do
|
||||
put saas.session_launchpad_path(params: { sig: "invalid" })
|
||||
|
||||
assert_response :unauthorized
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
test "destroy" do
|
||||
sign_in_as :kevin
|
||||
|
||||
delete session_path
|
||||
|
||||
assert_redirected_to Launchpad.logout_url
|
||||
assert_not cookies[:session_token].present?
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_session_path
|
||||
|
||||
assert_response :forbidden
|
||||
end
|
||||
|
||||
test "create" do
|
||||
post session_path, params: { email_address: "david@37signals.com", password: "secret123456" }
|
||||
|
||||
assert_response :forbidden
|
||||
end
|
||||
end
|
||||
@@ -18,7 +18,7 @@ class Account::SignalAccountTest < ActiveSupport::TestCase
|
||||
assert_equal @account, @account.external_account.peer
|
||||
end
|
||||
|
||||
test ".create_with_admin_user creates a new local account and user peers" do
|
||||
test ".create_with_admin_user creates a new local account" do
|
||||
ApplicationRecord.create_tenant("account-create-with-dependents") do
|
||||
signal_account = signal_accounts(:honcho_fizzy)
|
||||
account = Account.create_with_admin_user(tenant_id: signal_account.queenbee_id)
|
||||
@@ -32,11 +32,9 @@ class Account::SignalAccountTest < ActiveSupport::TestCase
|
||||
|
||||
assert_equal 1, User.count
|
||||
User.first.tap do |user|
|
||||
assert signal_account.owner.name, user.name
|
||||
assert signal_account.owner.email_address, user.email_address
|
||||
assert signal_account.owner.id, user.external_user_id
|
||||
assert_equal signal_account.owner.name, user.name
|
||||
assert_equal signal_account.owner.email_address, user.email_address
|
||||
assert_equal "admin", user.role
|
||||
assert_equal user, signal_account.owner.peer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,7 +42,6 @@ class SignupTest < ActiveSupport::TestCase
|
||||
|
||||
assert @signup.user
|
||||
assert @signup.user.persisted?
|
||||
assert_equal @signup.user.external_user, @signup.signal_account.owner
|
||||
|
||||
assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name
|
||||
assert_includes ApplicationRecord.tenants, @signup.tenant_name
|
||||
@@ -86,7 +85,6 @@ class SignupTest < ActiveSupport::TestCase
|
||||
|
||||
assert @signup.user
|
||||
assert @signup.user.persisted?
|
||||
assert_equal @signup.user.external_user, @signup.signal_account.owner
|
||||
|
||||
assert_equal @signup.queenbee_account.id.to_s, @signup.tenant_name
|
||||
assert_includes ApplicationRecord.tenants, @signup.tenant_name
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
require "test_helper"
|
||||
|
||||
class User::SignalUserTest < ActiveSupport::TestCase
|
||||
setup do
|
||||
@user = users(:david)
|
||||
end
|
||||
|
||||
test "belongs to a Signal::User" do
|
||||
assert_not_nil @user.external_user_id
|
||||
assert_equal signal_users("37s_fizzy_david"), @user.external_user
|
||||
end
|
||||
|
||||
test "peering" do
|
||||
assert_equal @user, @user.external_user.peer
|
||||
end
|
||||
|
||||
test "deactivate clears signal user" do
|
||||
users(:jz).deactivate
|
||||
|
||||
assert_nil users(:jz).reload.external_user
|
||||
end
|
||||
end
|
||||
@@ -4,10 +4,6 @@ require "queenbee/testing/mocks"
|
||||
module ActiveSupport
|
||||
class TestCase
|
||||
include SignalId::Testing
|
||||
|
||||
def saas_extension_sign_in_as(user)
|
||||
put saas.session_launchpad_path, params: { sig: user.external_user.perishable_signature }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
module Bootstrap
|
||||
def self.local_authentication?
|
||||
ENV.fetch("LOCAL_AUTHENTICATION", "") != "" || !File.directory?(File.expand_path("../gems/fizzy-saas", __dir__))
|
||||
def self.oss_config?
|
||||
ENV.fetch("OSS_CONFIG", "") != "" || !File.directory?(File.expand_path("../gems/fizzy-saas", __dir__))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
#
|
||||
# This script creates a new user that can be logged into via LOCAL_AUTHENTICATION, avoiding Launchpad/37id integration.
|
||||
# In order to login as this user, you must set the `LOCAL_AUTHENTICATION` environment variable when running the Rails server:
|
||||
#
|
||||
# LOCAL_AUTHENTICATION=1 bin/dev
|
||||
#
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
unless Rails.env.development?
|
||||
puts "ERROR: This script is intended to be run in development mode only."
|
||||
exit 1
|
||||
end
|
||||
|
||||
if ARGV.length < 2
|
||||
puts "Usage: #{$0} <email> <tenant>"
|
||||
exit 1
|
||||
end
|
||||
|
||||
email_address = ARGV[0]
|
||||
tenant = ARGV[1]
|
||||
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
user = User.create!(
|
||||
name: email_address.split("@").first,
|
||||
email_address: email_address,
|
||||
password: "secret123456"
|
||||
)
|
||||
|
||||
puts "Created: "
|
||||
pp user
|
||||
end
|
||||
+14
-30
@@ -2,41 +2,25 @@
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
if ARGV.length < 2
|
||||
puts "Usage: #{$0} <email> <tenant>"
|
||||
if ARGV.length < 3
|
||||
puts "Usage: #{$0} <tenant> <email> <fullname>"
|
||||
exit 1
|
||||
end
|
||||
|
||||
email_address = ARGV[0]
|
||||
tenant = ARGV[1]
|
||||
tenant = ARGV.shift
|
||||
email_address = ARGV.shift
|
||||
name = ARGV.join(" ")
|
||||
|
||||
def confirm(noun)
|
||||
print "Is this the correct #{noun}? (y/n) "
|
||||
response = $stdin.gets.chomp.downcase
|
||||
exit 0 unless response == "y"
|
||||
puts
|
||||
end
|
||||
|
||||
signal_identity = SignalId::Identity.find_by!(email_address: email_address)
|
||||
pp signal_identity
|
||||
confirm "identity"
|
||||
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
signal_account = Account.sole.external_account
|
||||
pp signal_account
|
||||
confirm "account"
|
||||
|
||||
SignalId::Database.on_master do
|
||||
signal_user = SignalId::User.create!(identity: signal_identity, account: signal_account)
|
||||
|
||||
user = User.create!(
|
||||
name: signal_user.name,
|
||||
email_address: signal_user.email_address,
|
||||
external_user_id: signal_user.id,
|
||||
password: SecureRandom.hex(36) # TODO: remove password column?
|
||||
)
|
||||
begin
|
||||
ApplicationRecord.with_tenant(tenant) do
|
||||
password = SecureRandom.hex(16)
|
||||
|
||||
user = User.create!(name:, email_address:, password:)
|
||||
puts "Created: "
|
||||
pp [ user, signal_user ]
|
||||
pp user
|
||||
|
||||
puts "Password is: #{password.inspect}"
|
||||
end
|
||||
rescue Exception => e
|
||||
puts "Failed with error: #{e.inspect}"
|
||||
end
|
||||
|
||||
@@ -34,7 +34,6 @@ ActiveRecord::Tenanted::DatabaseTasks.migrate_all
|
||||
|
||||
ApplicationRecord.with_tenant(signup.tenant_name) do |tenant|
|
||||
Account.sole.update! external_account: signup.signal_account
|
||||
User.first.update! external_user: signup.signal_account.owner
|
||||
|
||||
puts "\n\nLogin to http://launchpad.localhost:3011/ as #{signup.email_address} / #{signup.password}"
|
||||
end
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
#
|
||||
# set up a temporary password for all users
|
||||
# while we migrate away from Launchpad and 37id.
|
||||
#
|
||||
# the password is unguessable but generated based on the email address and a seed,
|
||||
# so each user will have the same password across all tenants.
|
||||
#
|
||||
require_relative "../../config/environment"
|
||||
|
||||
seed = ARGV[0] || "temporary-password-seed"
|
||||
puts "Using seed: #{seed.inspect}"
|
||||
|
||||
TENANTS = Hash.new
|
||||
USERS = Hash.new { |h, k| h[k] = Hash.new }
|
||||
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
TENANTS[tenant] = Account.sole.name
|
||||
|
||||
User.find_each do |user|
|
||||
putc "."
|
||||
next if user.system?
|
||||
|
||||
if user.external_user_id
|
||||
suser = SignalId::User.find_by_id(user.external_user_id)
|
||||
if suser && suser.email_address != user.email_address
|
||||
puts "\nWarning: fixing email address for user #{user.id} in tenant #{tenant}:"
|
||||
puts " local: #{user.email_address}"
|
||||
puts " signal: #{suser.email_address}"
|
||||
user.update! email_address: suser.email_address
|
||||
end
|
||||
end
|
||||
|
||||
password = Digest::SHA256.hexdigest("#{seed}-#{user.email_address}")[0..16]
|
||||
user.update! password: password
|
||||
|
||||
USERS[user.email_address][tenant] = password
|
||||
end
|
||||
end
|
||||
|
||||
puts
|
||||
|
||||
USERS.each do |email, hash|
|
||||
puts "\n#{email}:"
|
||||
puts " password: #{hash.first.last}"
|
||||
puts " fizzies:"
|
||||
hash.each do |tenant, _|
|
||||
url = Rails.application.routes.url_helpers.root_url(Rails.application.config.action_controller.default_url_options.merge(script_name: "/#{tenant}"))
|
||||
|
||||
puts " #{TENANTS[tenant]}: #{url}"
|
||||
end
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
account = Account.sole
|
||||
signal_account = account.signal_account
|
||||
|
||||
signal_users = SignalId::User.where(account_id: signal_account.id)
|
||||
|
||||
signal_users.each do |signal_user|
|
||||
unless User.find_by(signal_user_id: signal_user.id)
|
||||
User.create!(
|
||||
name: signal_user.identity.name,
|
||||
email_address: signal_user.identity.email_address,
|
||||
signal_user_id: signal_user.id,
|
||||
password: SecureRandom.hex(36) # TODO: remove password column?
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/usr/bin/env ruby
|
||||
|
||||
#
|
||||
# this is intended to copy a production database into beta, change the account id, and stitch the
|
||||
# user accounts together properly.
|
||||
#
|
||||
|
||||
require_relative "../config/environment"
|
||||
|
||||
ActiveRecord::Base.logger = Logger.new(File::NULL)
|
||||
|
||||
ApplicationRecord.with_each_tenant do |tenant|
|
||||
puts "\n# tenant: #{tenant}"
|
||||
|
||||
signal_account = SignalId::Account.find_by!(queenbee_id: tenant)
|
||||
puts "Found signal account #{signal_account.inspect}"
|
||||
|
||||
account = Account.sole
|
||||
if account.tenant_id != tenant
|
||||
puts "setting account tenant_id to #{tenant}"
|
||||
account.update!(tenant_id: tenant, name: account.name + " (Beta)")
|
||||
end
|
||||
|
||||
User.find_each do |user|
|
||||
next if user.system? || user.external_user_id.nil?
|
||||
|
||||
signal_user = user.external_user
|
||||
next if signal_user.nil?
|
||||
next if signal_user.account == account.external_account
|
||||
|
||||
signal_identity = signal_user.identity
|
||||
pp signal_identity
|
||||
|
||||
SignalId::Database.on_master do
|
||||
signal_user = SignalId::User.find_or_create_by!(identity: signal_identity, account: signal_account)
|
||||
puts "Created signal user #{signal_user.inspect} for identity #{signal_identity.inspect}"
|
||||
user.external_user_id = signal_user.id
|
||||
user.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,20 +1,19 @@
|
||||
require "test_helper"
|
||||
|
||||
class ControllerAuthenticationTest < ActionDispatch::IntegrationTest
|
||||
if Rails.application.config.x.local_authentication
|
||||
test "access without an account slug redirects to new session" do
|
||||
integration_session.default_url_options[:script_name] = "" # no tenant
|
||||
test "access without an account slug redirects to new session" do
|
||||
integration_session.default_url_options[:script_name] = "" # no tenant
|
||||
|
||||
get cards_path
|
||||
get cards_path
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
end
|
||||
assert_response :success
|
||||
assert_dom "h2", text: "We're migrating Fizzy authentication"
|
||||
end
|
||||
|
||||
test "access with an account slug but no session redirects to new session" do
|
||||
get cards_path
|
||||
test "access with an account slug but no session redirects to new session" do
|
||||
get cards_path
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
end
|
||||
assert_redirected_to new_session_path
|
||||
end
|
||||
|
||||
test "access with an account slug and a session allows functional access" do
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
require "test_helper"
|
||||
|
||||
class SessionsControllerTest < ActionDispatch::IntegrationTest
|
||||
if Rails.application.config.x.local_authentication
|
||||
test "destroy" do
|
||||
sign_in_as :kevin
|
||||
test "destroy" do
|
||||
sign_in_as :kevin
|
||||
|
||||
delete session_path
|
||||
delete session_path
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
assert_not cookies[:session_token].present?
|
||||
end
|
||||
assert_redirected_to new_session_path
|
||||
assert_not cookies[:session_token].present?
|
||||
end
|
||||
|
||||
test "new" do
|
||||
get new_session_path
|
||||
test "new" do
|
||||
get new_session_path
|
||||
|
||||
assert_response :success
|
||||
end
|
||||
assert_response :success
|
||||
end
|
||||
|
||||
test "create with valid credentials" do
|
||||
post session_path, params: { email_address: "david@37signals.com", password: "secret123456" }
|
||||
test "create with valid credentials" do
|
||||
post session_path, params: { email_address: "david@37signals.com", password: "secret123456" }
|
||||
|
||||
assert_redirected_to root_path
|
||||
assert cookies[:session_token].present?
|
||||
end
|
||||
assert_redirected_to root_path
|
||||
assert cookies[:session_token].present?
|
||||
end
|
||||
|
||||
test "create with invalid credentials" do
|
||||
post session_path, params: { email_address: "david@37signals.com", password: "wrong" }
|
||||
test "create with invalid credentials" do
|
||||
post session_path, params: { email_address: "david@37signals.com", password: "wrong" }
|
||||
|
||||
assert_redirected_to new_session_path
|
||||
assert_not cookies[:session_token].present?
|
||||
end
|
||||
assert_redirected_to new_session_path
|
||||
assert_not cookies[:session_token].present?
|
||||
end
|
||||
end
|
||||
|
||||
Vendored
-9
@@ -5,27 +5,18 @@ david:
|
||||
email_address: david@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: member
|
||||
<% if User.reflect_on_association :external_user %>
|
||||
external_user: 37s_fizzy_david
|
||||
<% end %>
|
||||
|
||||
jz:
|
||||
name: JZ
|
||||
email_address: jz@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: member
|
||||
<% if User.reflect_on_association :external_user %>
|
||||
external_user: 37s_fizzy_jzimdars
|
||||
<% end %>
|
||||
|
||||
kevin:
|
||||
name: Kevin
|
||||
email_address: kevin@37signals.com
|
||||
password_digest: <%= digest %>
|
||||
role: admin
|
||||
<% if User.reflect_on_association :external_user %>
|
||||
external_user: 37s_fizzy_kevin
|
||||
<% end %>
|
||||
|
||||
system:
|
||||
name: System
|
||||
|
||||
@@ -3,6 +3,6 @@ require "test_helper"
|
||||
class AccountTest < ActiveSupport::TestCase
|
||||
test "slug" do
|
||||
account = Account.sole
|
||||
assert_equal "/#{account.tenant_id}", account.slug
|
||||
assert_equal "/#{ApplicationRecord.current_tenant}", account.slug
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,30 +1,27 @@
|
||||
require "test_helper"
|
||||
|
||||
class PeriodHighlightTest < ActiveSupport::TestCase
|
||||
# Skipping when locally authenticating because the VCR cassettes would need to be re-recorded.
|
||||
unless Rails.application.config.x.local_authentication
|
||||
include VcrTestHelper
|
||||
include VcrTestHelper
|
||||
|
||||
setup do
|
||||
@user = users(:david)
|
||||
setup do
|
||||
@user = users(:david)
|
||||
end
|
||||
|
||||
test "generate period highlights" do
|
||||
period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do
|
||||
PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months)
|
||||
end
|
||||
|
||||
test "generate period highlights" do
|
||||
period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do
|
||||
PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months)
|
||||
end
|
||||
assert_match /logo/i, period_highlights.to_html
|
||||
end
|
||||
|
||||
assert_match /logo/i, period_highlights.to_html
|
||||
test "don't generate highlights for existing periods" do
|
||||
new_period_highlights = PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months)
|
||||
|
||||
existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do
|
||||
PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months)
|
||||
end
|
||||
|
||||
test "don't generate highlights for existing periods" do
|
||||
new_period_highlights = PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months)
|
||||
|
||||
existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do
|
||||
PeriodHighlights.create_or_find_for(@user.collections, starts_at: 1.month.ago, duration: 2.months)
|
||||
end
|
||||
|
||||
assert_equal new_period_highlights, existing_period_highlights
|
||||
end
|
||||
assert_equal new_period_highlights, existing_period_highlights
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,67 +1,64 @@
|
||||
require "test_helper"
|
||||
|
||||
class User::HighlightsTest < ActiveSupport::TestCase
|
||||
# Skipping when locally authenticating because the VCR cassettes would need to be re-recorded.
|
||||
unless Rails.application.config.x.local_authentication
|
||||
include VcrTestHelper
|
||||
include VcrTestHelper
|
||||
|
||||
setup do
|
||||
@user = users(:david)
|
||||
travel_to 1.week.ago + 2.days
|
||||
Current.session = sessions(:david)
|
||||
end
|
||||
setup do
|
||||
@user = users(:david)
|
||||
travel_to 1.week.ago + 2.days
|
||||
Current.session = sessions(:david)
|
||||
end
|
||||
|
||||
test "generate weekly highlights" do
|
||||
stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do
|
||||
period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do
|
||||
@user.generate_weekly_highlights
|
||||
end
|
||||
|
||||
assert_match /logo/i, period_highlights.to_html
|
||||
end
|
||||
end
|
||||
|
||||
test "don't generate highlights for existing periods" do
|
||||
stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do
|
||||
new_period_highlights = @user.generate_weekly_highlights
|
||||
assert_not_nil new_period_highlights
|
||||
|
||||
existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do
|
||||
@user.generate_weekly_highlights
|
||||
end
|
||||
|
||||
assert_equal new_period_highlights, existing_period_highlights
|
||||
end
|
||||
end
|
||||
|
||||
test "periods respect user timezone for week boundaries" do
|
||||
@user.settings.update!(timezone_name: "America/New_York")
|
||||
|
||||
# Sunday Jan 7, 2024 at 2am EST (7am UTC) - this is Sunday in NYC
|
||||
sunday_in_nyc = Time.zone.parse("2024-01-07 07:00:00 UTC")
|
||||
|
||||
# Saturday Jan 6, 2024 at 11pm EST (Jan 7 4am UTC) - still Saturday in NYC but Sunday in UTC
|
||||
saturday_in_nyc = Time.zone.parse("2024-01-07 04:00:00 UTC")
|
||||
|
||||
# Event on Saturday evening in NYC (but Sunday in UTC)
|
||||
saturday_event = travel_to(saturday_in_nyc) { cards(:logo).track_event("Saturday event") }
|
||||
|
||||
# Events throughout the week starting Sunday in NYC
|
||||
7.times do |i|
|
||||
travel_to(sunday_in_nyc + i.days) { cards(:logo).track_event("Event #{i}") }
|
||||
test "generate weekly highlights" do
|
||||
stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do
|
||||
period_highlights = assert_difference -> { PeriodHighlights.count }, 1 do
|
||||
@user.generate_weekly_highlights
|
||||
end
|
||||
|
||||
wednesday = sunday_in_nyc + 3.days
|
||||
|
||||
# The period should start at Sunday in NYC timezone
|
||||
period_start = wednesday.in_time_zone("America/New_York").beginning_of_week(:sunday)
|
||||
period = PeriodHighlights::Period.new(@user.collections, starts_at: period_start, duration: 1.week)
|
||||
|
||||
# The Saturday event should NOT be included (it's in the previous week in NYC time)
|
||||
assert_not_includes period.events, saturday_event
|
||||
|
||||
# Should include 7 events from the current week
|
||||
assert_equal 7, period.events.count
|
||||
assert_match /logo/i, period_highlights.to_html
|
||||
end
|
||||
end
|
||||
|
||||
test "don't generate highlights for existing periods" do
|
||||
stub_const(PeriodHighlights::Period, :MIN_EVENTS_TO_BE_INTERESTING, 3) do
|
||||
new_period_highlights = @user.generate_weekly_highlights
|
||||
assert_not_nil new_period_highlights
|
||||
|
||||
existing_period_highlights = assert_no_difference -> { PeriodHighlights.count } do
|
||||
@user.generate_weekly_highlights
|
||||
end
|
||||
|
||||
assert_equal new_period_highlights, existing_period_highlights
|
||||
end
|
||||
end
|
||||
|
||||
test "periods respect user timezone for week boundaries" do
|
||||
@user.settings.update!(timezone_name: "America/New_York")
|
||||
|
||||
# Sunday Jan 7, 2024 at 2am EST (7am UTC) - this is Sunday in NYC
|
||||
sunday_in_nyc = Time.zone.parse("2024-01-07 07:00:00 UTC")
|
||||
|
||||
# Saturday Jan 6, 2024 at 11pm EST (Jan 7 4am UTC) - still Saturday in NYC but Sunday in UTC
|
||||
saturday_in_nyc = Time.zone.parse("2024-01-07 04:00:00 UTC")
|
||||
|
||||
# Event on Saturday evening in NYC (but Sunday in UTC)
|
||||
saturday_event = travel_to(saturday_in_nyc) { cards(:logo).track_event("Saturday event") }
|
||||
|
||||
# Events throughout the week starting Sunday in NYC
|
||||
7.times do |i|
|
||||
travel_to(sunday_in_nyc + i.days) { cards(:logo).track_event("Event #{i}") }
|
||||
end
|
||||
|
||||
wednesday = sunday_in_nyc + 3.days
|
||||
|
||||
# The period should start at Sunday in NYC timezone
|
||||
period_start = wednesday.in_time_zone("America/New_York").beginning_of_week(:sunday)
|
||||
period = PeriodHighlights::Period.new(@user.collections, starts_at: period_start, duration: 1.week)
|
||||
|
||||
# The Saturday event should NOT be included (it's in the previous week in NYC time)
|
||||
assert_not_includes period.events, saturday_event
|
||||
|
||||
# Should include 7 events from the current week
|
||||
assert_equal 7, period.events.count
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -65,6 +65,6 @@ RubyLLM.configure do |config|
|
||||
config.openai_api_key ||= "DUMMY-TEST-KEY" # Run tests with VCR without having to configure OpenAI API key locally.
|
||||
end
|
||||
|
||||
unless Rails.application.config.x.local_authentication
|
||||
unless Rails.application.config.x.oss_config
|
||||
load File.expand_path("../gems/fizzy-saas/test/test_helper.rb", __dir__)
|
||||
end
|
||||
|
||||
@@ -7,11 +7,7 @@ module SessionTestHelper
|
||||
cookies.delete :session_token
|
||||
user = users(user) unless user.is_a? User
|
||||
|
||||
if Rails.application.config.x.local_authentication
|
||||
post session_path, params: { email_address: user.email_address, password: "secret123456" }
|
||||
else
|
||||
saas_extension_sign_in_as(user)
|
||||
end
|
||||
post session_path, params: { email_address: user.email_address, password: "secret123456" }
|
||||
|
||||
cookie = cookies.get_cookie "session_token"
|
||||
assert_not_nil cookie, "Expected session_token cookie to be set after sign in"
|
||||
|
||||
Reference in New Issue
Block a user