smpallen99 / ex_admin

ExAdmin is an auto administration package for Elixir and the Phoenix Framework
MIT License
1.21k stars 273 forks source link

Can't create/edit relations to models with non-id/non-string primary keys #311

Open bglusman opened 7 years ago

bglusman commented 7 years ago

I've been putting off figuring this out for a few weeks, and though I can partially work around it by making a custom input form that skips the non-standard relationship, I can't figure out if there is a built in way to tell ExAdmin that a particular belongs_to relation (which may be named relation_id) refers to a a table that uses a non-integer primary key not called "id"... it always errors with "key :id not found in ", whether building automatically or manually... It seems like ExAdmin needs some ability to reflect on Ecto's @primary_key attribute, or an equivalent option to set on it's modules or relations, but if it exists I can't find it in the source or docs. Hypothetically I'd love to try and contribute this, but I'm not sure my meta-programming skills are up to the task yet :-)

For reference, the code erroring/using the non-standard primary keys is here: https://github.com/MasbiaSoupKitchenNetwork/open_pantry, notably on the stock and credit_type_membership modules relating to food and food_group respectively. Only using this structure so we can use existing USDA food database, so we don't have any control over the keys they use. Otherwise ExAdmin is working out great in the project though, really appreciate all the work!

bglusman commented 7 years ago

@praveenperera or @smpallen99 either of you have a minute to discuss/help me think about this in the next day or two?

smpallen99 commented 7 years ago

@bglusman can you post the error please. I'd like to know which module is throwing the error.

bglusman commented 7 years ago

Of course! Thanks @smpallen99 ! Here's one of the errors:

Request: GET /admin/credit_type_memberships/new
** (exit) an exception was raised:
    ** (KeyError) key :id not found in: %OpenPantry.FoodGroup{__meta__: #Ecto.Schema.Metadata<:loaded, "food_groups">, credit_types: #Ecto.Association.NotLoaded<association :credit_types is not loaded>, foodgroup_code: "0100", foodgroup_desc: "Dairy and Egg Products", foods: #Ecto.Association.NotLoaded<association :foods is not loaded>}
        (ex_admin) lib/ex_admin/form/fields.ex:32: anonymous fn/5 in ExAdmin.Form.Fields.do_input_collection/8
        (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
        (ex_admin) lib/ex_admin/form/fields.ex:30: ExAdmin.Form.Fields.do_input_collection/8
        (ex_admin) lib/ex_admin/form.ex:724: anonymous fn/10 in ExAdmin.Form.build_item/5
        (ex_admin) lib/ex_admin/themes/admin_lte2.ex:43: ExAdmin.Theme.AdminLte2.wrap_item_type/6
        (ex_admin) lib/ex_admin/themes/admin_lte2/form.ex:92: ExAdmin.Theme.AdminLte2.Form.theme_wrap_item/9
        (ex_admin) lib/ex_admin/form.ex:612: ExAdmin.Form.wrap_item/9
        (ex_admin) lib/ex_admin/form.ex:717: ExAdmin.Form.build_item/5
        (ex_admin) lib/ex_admin/form.ex:905: anonymous fn/6 in ExAdmin.Form.build_item/5
        (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
        (ex_admin) lib/ex_admin/form.ex:902: anonymous fn/5 in ExAdmin.Form.build_item/5
        (ex_admin) lib/ex_admin/themes/admin_lte2/form.ex:145: ExAdmin.Theme.AdminLte2.Form.form_box/3
        (ex_admin) lib/ex_admin/form.ex:552: anonymous fn/6 in ExAdmin.Form.build_main_block/4
        (elixir) lib/enum.ex:1755: Enum."-reduce/3-lists^foldl/2-0-"/3
        (ex_admin) lib/ex_admin/form.ex:550: ExAdmin.Form.build_main_block/4
        (ex_admin) lib/ex_admin/themes/admin_lte2/form.ex:24: ExAdmin.Theme.AdminLte2.Form.build_form/6
        (ex_admin) web/controllers/admin_resource_controller.ex:73: ExAdmin.AdminResourceController.new/3
        (ex_admin) web/controllers/admin_resource_controller.ex:1: ExAdmin.AdminResourceController.action/2
        (ex_admin) web/controllers/admin_resource_controller.ex:1: ExAdmin.AdminResourceController.phoenix_controller_pipeline/2
        (open_pantry) lib/open_pantry/endpoint.ex:1: OpenPantry.Endpoint.instrument/4
bglusman commented 7 years ago

(FWIW, I'm also considering fixing this a few other ways, but though I only currently care about these 2 tables, especially in the admin, there are numerous other tables referencing these foreign keys, and I'd like to make updating to newer versions of this data in the future easy-ish... I started just copying to strings to a new id column and casting to int, which works, but making that the primary key is a little trickier... may keep playing with some version of that though, but I'm not a SQL guru and it's working ok with strings for everything else as is...)

smpallen99 commented 7 years ago

@bglusman Give the following a try in lib/ex_admin/form/fields.ex

  def do_input_collection(resource, collection, model_name, field_name, item, %{cardinality: :one, related_key: related_key} = assoc, _params, _errors) do
    Adminlog.debug "1st _input_collection... #{field_name}"

    ext_name = ext_name model_name, field_name
    assoc_fields = get_association_fields(item[:opts])

    # err = if errors == [], do: false, else: true
    select(class: "form-control", id: "#{ext_name}_id", name: "#{model_name}[#{assoc.owner_key}]") do
      handle_prompt(field_name, item)
      for item <- collection do
        item_id = Map.get(item, related_key)
        selected = cond do
          Map.get(resource, assoc.owner_key) == item_id ->
            [selected: :selected]
          # not(params[model_name][Atom.to_string(assoc.owner_key)] in [nil, ""]) ->
          #   Logger.warn "foo"
          #   [selected: :selected]
          true ->
            []
        end
        map_relationship_fields(item, assoc_fields)
        |> option([value: "#{item_id}"] ++ selected)
      end
    end
  end

Note the changes around releated_key and item_id

BTW, to figure this stuff out, in iex -S mix, take your model and to this:

OpenPantry.FoodGroup.__schema__(:associations) 

to find the associations, then pick the association your interested in and do

OpenPantry.FoodGroup.__schema__(:association, :some_assoc)

and it will list the meta data for the association.

smpallen99 commented 7 years ago

@bglusman I have support for non-standard primary keys in other parts of the code, but must have missed this one.

bglusman commented 7 years ago

Thanks Steve! That fixes the proximate cause (running locally via path: "../ex_admin" with edited fork, so the page renders with a valid belongs_to drop down, but it's not actually setting the relation correctly on form submit now. The one relation with a proper constraint insists the string key is a required field that can't be empty, and the other one I posted before on save errors as so: ERROR 23502 (not_null_violation): null value in column "food_group_id" violates not-null constraint

Possibly a little confused about the OpenPantry.FoodGroup.__schema__ methods/suggestion, would you like me to pass any of that data on, or did you figure out by cloning and looking at that stuff vs ExAdmin code? Thanks man!

smpallen99 commented 7 years ago

I'm in the process of reviewing a large PR that changes the way ex_admin handles associations and changesets. It takes away some of the ex_admin 'magic' and allows you to control with your own changesets.

I hope to have the PR merged this weekend.

bglusman commented 7 years ago

Awesome! Thanks Steve, let me know if I can help in any way!