Walther Diechmann

Joined

1,070 Experience
2 Lessons Completed
1 Question Solved

Activity

Yeah - turns out I was overthinking it - the browser/device apparently does the house-keeping themselves - so I took it in another direction. Scold me for littering if you must - here is the Stimulus controller that emerged from the thick of the fight

import { Controller } from "@hotwired/stimulus"
import { enter, leave } from "el-transition"

// Connects to data-controller="profile"
export default class extends Controller {

  static targets = [
    "buttonForm",
    "enableNotifications",
    "disableNotifications"
  ]

  registration = null

  connect() {
    navigator.serviceWorker.register("/service-worker.js").then(
      (reg) => {
        this.registration = reg;
        if (!this.registration) return;
        this.showPromptForNotifications();
      },
      (err) => {
        console.error("Service Worker registration failed: ", err);
      }
    );
  }

  enable(event) {
    event.preventDefault()
    if (!this.registration) return;
    this.buttonFormTarget.ariaBusy = true;
    Notification.requestPermission()
      .then((permission) => {
        if (permission === "granted") {
          this.setupSubscription();
        } else {
          console.log("Notifications declined");
        }
      })
      .catch((error) => console.log("Notifications error", error));
  }

  disable(event) {
    event.preventDefault()
    console.log("Disable notifications")
    if (!this.registration) return;

    let r = this.registration
    let en = this.enableNotificationsTarget
    let dn = this.disableNotificationsTarget

    this.registration.pushManager.getSubscription().then(
      async function (subscription) {
        if (!subscription) return;

        await fetch("/web_push_subscriptions?unsubscribe=true", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
          },
          body: JSON.stringify(subscription),
        }).then((response) => {
          if (response.ok) {
            subscription.unsubscribe()
            r = null
            leave(dn).then(() => {
              enter(en)
            })
          }
        }).catch(function (e) {
          console.log("Server unsubscription setup failed", e);
        })
      })
  }

  // Show a prompt to the user to subscribe to notifications
  showPromptForNotifications() {
    this.registration.pushManager.getSubscription().then(
      (subscription) => {
        if (subscription) {
          this.setupSubscription(subscription);
        } else {
          enter(this.enableNotificationsTarget)
        }
      },
      (err) => {
        console.log("Error getting subscription: ", err);
      }
    );
  }

  //
  // Setup the subscription with the push server - provided the user has granted permission
  async setupSubscription(subscription = null) {
    if (Notification.permission !== "granted" && !subscription) return;

    if (!subscription) {
      let vapid = new Uint8Array(
        JSON.parse(document.querySelector("meta[name=web_push_public]")?.content)
      );

      subscription = await this.registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: vapid,
      });
    }

    await fetch("/web_push_subscriptions", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(subscription),
    }).then((response) => {
      if (response.ok) {
        leave(this.enableNotificationsTarget).then(() => {
          enter(this.disableNotificationsTarget);
        });
      } else {
        console.log("Subscription setup failed");
      }
    });
  }
}

I use the web_push strategy in Noticed V2 - and it's a kicker - my +1 on that, really!

But I must be doing something wrong b/c everytime a user logs into the app hem is requested (by the service-worker) to accept receiving notifications. IMHO the user should only be facing that decision once (and then on a dashboard or profile have an unsubscribe button)

My question boils down to: how do I persist the user's decision to subscribing to notifications on the client - as it apparently is not enough to

  await fetch("/web_push_subscriptions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(subscription),
  });

thx again for a real gem Chris!

Posted in Setup MacOS 11.0 Big Sur Discussion

this is ruining my day totally :(

dyld: lazy symbol binding failed: Symbol not found: _mysql_server_init
  Referenced from: /Users/walther/.asdf/installs/ruby/3.0.1/lib/ruby/gems/3.0.0/gems/mysql2-0.5.3/lib/mysql2/mysql2.bundle
  Expected in: flat namespace

I've single-handedly restored the russian economy (judged by the amount of energy/gas I've spent googling, reinstalling everything but the macOS)

has anyone had success installing the arm64 mysql server using brew on Big Sur 11.6.5 (I did) followed by installing the mysql2 gem (I didn't) ?

I've collected quite a set of settings that you may browse here

Last update: getting my associations straight!

Well - like I more or less anticipated - the devil was lurking if not in the detail then in the assignment :big_smile:

Going back to the drawing board (and with a kind push from https://twitter.com/kaspth) I realized that I had been too generous with the delegated_typ'ing

class Assignment < AbstractResource

  belongs_to :event, inverse_of: :assignments
  belongs_to :assignable, polymorphic: true, required: true

  accepts_nested_attributes_for :assignable

when in fact assignments are nothing but mere (polymorphic) mortals :smile:

Now I could finally -- hunting this down for the better part of a week (jeez I'm getting too old for this game) -- have my abstracted_away controller (which will allow me to focus on all the crazy exciting methods and just c/p the "standard" controller stuff from inherited controllers like this (and I don't think it gets any thinner)

class TasksController < EventsController

  def new_resource 
    resource_class= Task
    super
  end

  private 

    # Never trust parameters from the scary internet, only allow the white list through.
    def resource_params
      params.require(:event).permit(:name, :account_id, :eventable_type, eventable_attributes: [ :duration ], assignments_attributes: [ :id, :assignable_id, :assignable_type, :assignable_role, :_destroy ]  )
    end

    #
    # implement on every controller where search makes sense
    # geet's called from resource_control.rb 
    #
    def find_resources_queried options
      Task.search Task.all, params[:q]
    end
end

Happy to report that all is - again - quiet on the West front (as opposed to the East front at the moment I'm afraid -- slava Ukraine btw)

Update: right about the ugliest method - where's the sugar?

I knew that no primary_key had this "wrong arguments" odour so it was a no-brainer to feed the beast some proper arguments

    def create
      r = resource_params.tap {|ary| ary.delete :eventable_attributes }
      r = r.tap {|ary| ary.delete :assignments_attributes}
      r = Event.new r
      r.eventable = Task.new resource_params[:eventable_attributes]
      if r.valid?
        r.save
        resource_params[:assignments_attributes].each do |k,a|
          r.assignments << Assignment.create( event: r, assignable_type: a["assignable_type"], assignable_id: a["assignable_id"], assignable_role: a["assignable_role"])
        end
      end
    end

so - it's a "done deal" (except it's nothing of the kind) but this was totally not what I expected from delegated_type (and I'm fully aware that I'm getting punished by my own sword)

Update: every little step -- closer and yet so distant

Working on the issue had me redo the #create

    def create
      r = resource_params.tap {|ary| ary.delete :eventable_attributes }
      r = r.tap {|ary| ary.delete :assignments_attributes}
      r = Event.new r
      r.eventable = Task.new resource_params[:eventable_attributes]
      if r.valid?
        r.save
        resource_params[:assignments_attributes].each do |k,a|
          r.assignments << Assignment.create( event: r, assignable: a)
        end
      end
      @resource= r
    end

not in any way near my expectations - and it still won't let me off the hook

 r.assignments << Assignment.create( event: r, assignable: a)

(telling me that there is undefined method `primary_key' for ActionController::Parameters:Class)

Update: inching my way through this - -

I decided to let the code rest while I finished Drive To Survive Season 4 which to a certain degree proved beneficial!

Tearing it all apart, I did:

    def create_resource
      r = resource_params.tap {|ary| ary.delete :eventable_attributes }
      r = r.tap {|ary| ary.delete :assignments_attributes}
      r = Event.new r, eventable: resource_params[:event][:eventable_attributes]
      r.assignments.build resource_params[:event][:assignments_attributes]
    end

which left me with this beauty:

hours-hours-1  | 22:58:01 web.1  | NoMethodError (undefined method `[]' for nil:NilClass):
hours-hours-1  | 22:58:01 web.1  |   
hours-hours-1  | 22:58:01 web.1  | app/controllers/tasks_controller.rb:33:in `create_resource'

() which at least left me with something to chase!

Update: adding 'events' in the CLI

I "adjusted" the models somewhat (and added the eventable module for completeness, now looking like this:

class Event < ApplicationRecord
  belongs_to :calendar, optional: true
  has_many :assignments, inverse_of: :event

  delegated_type :eventable, types: %w[ Call Task Meeting], dependent: :destroy

  accepts_nested_attributes_for :eventable, reject_if: :all_blank, allow_destroy: true
  accepts_nested_attributes_for :assignments, reject_if: :all_blank, allow_destroy: true

end

class Task < Event
  include Eventable

  has_many :assignments, through: :event, inverse_of: :assignable
end 

module Eventable
  extend ActiveSupport::Concern

  included do
    has_one :event, as: :eventable, touch: true, dependent: :destroy

  end

end

class Assignment < ApplicationRecord
  belongs_to :event, inverse_of: :assignments
  delegated_type :assignable, types: %w[ Participant Asset ]
end


class TasksController < EventsController

  def new_resource 
    @event = Event.new eventable: Task.new
    @event.assignments.new( assignable: (Employee.find(params[:employee_id]) rescue nil))
  end

  def create
    @event = Event.new resource_params
  end


  private 


    # Never trust parameters from the scary internet, only allow the white list through.
    def resource_params
      params.require(:event).permit(:name, :account_id, eventable_attributes: [ :id, :duration ], assignments_attributes: [ :id, :assignable_id, :assignable_type, :assignable_role, :_destroy ]  )
    end

end


Further, I "recalibrated" the form to now produce this kind of params:

Started POST "/tasks" for 172.25.0.1 at 2022-03-29 17:40:30 +0000
hours-hours-1  | 17:40:30 web.1  | Processing by TasksController#create as TURBO_STREAM
hours-hours-1  | 17:40:30 web.1  |   Parameters: {"authenticity_token"=>"[FILTERED]", "event"=>{"account_id"=>"1", "name"=>"dfghjklæ", "eventable_attributes"=>{"duration"=>"110"}, "assignments_attributes"=>{"0"=>{"assignable_type"=>"Employee", "assignable_id"=>"54", "assignable_role"=>"owner"}}}}

(the somewhat 'funny' log is due to me running this all off a set of Docker containers)

While having the tasks_controller#create method doing the heavy-lifting still has quite a while to go - I've managed to fiddle the CLI into persisting a task by doing this:

irb(main):041:0> event = Event.new account_id: Account.first.id, name: "test", eventable: Task.new(duration: 100)
irb(main):044:0> event.assignments << Assignment.create( event: event, assignable: Employee.last, assignable_role: "owner")

Now - if only I could make the

irb(main):031:0> Event.new params["event"]
/usr/local/bundle/gems/activemodel-7.0.0/lib/active_model/attribute_assignment.rb:51:in `_assign_attribute': unknown attribute 'eventable_attributes' for Event. (ActiveModel::UnknownAttributeError)

go away - and take the log barfing up

hours-hours-1  | 17:40:30 web.1  | NoMethodError (undefined method `constantize' for nil:NilClass):
hours-hours-1  | 17:40:30 web.1  |   
hours-hours-1  | 17:40:30 web.1  | app/controllers/tasks_controller.rb:30:in `create_resource'

with it - I'd be home free :big_smile:

I have this marvelous complicated DB design that goes along these lines: events ( like calls, tasks, and meetings ) can have a number of assignees (Participants, and Assets) - like a Team Meeting can have a meeting room and the entire team of employees in team A assigned.

That one/two line 'use case' really has me in the ropes :cry:

class Event < ApplicationRecord
  belongs_to :calendar, optional: true
  has_many :assignments, inverse_of: :event

  delegated_type :eventable, types: %w[ Call Task Meeting], dependent: :destroy
end

class Task < Event
  include Eventable

  has_many :assignments, through: :event, inverse_of: :assignable
  accepts_nested_attributes_for :assignments
end 

class Assignment < ApplicationRecord
  belongs_to :event, inverse_of: :assignments
  delegated_type :assignable, types: %w[ Participant Asset ]
end

# routes.rb

  resources :employees do
     resources :tasks
  end

class TasksController < EventsController

  def new 
    @task= Task.new( event: Event.new)
    @task.assignments.new( assignable: (Employee.find(params[:employee_id]) rescue nil))
  end

  def create
   ??
  end


  private 
    def resource_params
      params.require(:task).permit(:duration, event_attributes: [ :name, :account_id ], assignments_attributes: [ :id, :assignable_id, :assignable_type, :assignable_role, :_destroy ]  )
    end

end

I am able to get a params back from the form with

#
# Parameters: {"authenticity_token"=>"[FILTERED]", 
# "task"=>{
#   "event_attributes"=>{"account_id"=>"1", "name"=>"meeting"}, 
#   "duration"=>"100", 
#   "assignments_attributes"=>{
#     "0"=>{"assignable_type"=>"Employee", "assignable_id"=>"54", "assignable_role"=>"owner"}
#     }
#   }
# }

I just don't get how I persist the thing :cry: (again)

Posted in Hotwire Modal Forms Discussion

Solid lesson, Chris! I do miss one tiny issue though :(

When saving the form and it has errors all is good - but when the form is good!?

Remember - you're on the index.html.erb already and opening a modal will afford you to prepend new posts on /posts across all tabs/connected users, and all is great.

I can format.turbo_stream { render turbo_stream: turbo_stream.remove( "post_form" ) } but that does not close the modal and will leave me with "an empty" DIV if I somehow manages to get the modal.hide() working. A "how to attach to the webhooks of Turbo really would come in handy <3

And then there is the 'edit' action on the modal too - same song more or less.