feat: add way to create animal type when editing animal

This commit is contained in:
Lance Edgar 2026-02-27 16:35:56 -06:00
parent 1c0286eda0
commit ec67340e66
7 changed files with 237 additions and 47 deletions

View file

@ -40,6 +40,15 @@ def main(global_config, **settings):
"wuttaweb:templates", "wuttaweb:templates",
], ],
) )
settings.setdefault(
"pyramid_deform.template_search_path",
" ".join(
[
"wuttafarm.web:templates/deform",
"wuttaweb:templates/deform",
]
),
)
# make config objects # make config objects
wutta_config = base.make_wutta_config(settings) wutta_config = base.make_wutta_config(settings)

View file

@ -55,6 +55,12 @@ class AnimalTypeRef(ObjectRef):
animal_type = obj animal_type = obj
return self.request.route_url("animal_types.view", uuid=animal_type.uuid) return self.request.route_url("animal_types.view", uuid=animal_type.uuid)
def widget_maker(self, **kwargs):
from wuttafarm.web.forms.widgets import AnimalTypeRefWidget
kwargs["factory"] = AnimalTypeRefWidget
return super().widget_maker(**kwargs)
class LogQuick(WuttaSet): class LogQuick(WuttaSet):
@ -185,25 +191,6 @@ class FarmOSQuantityRefs(WuttaSet):
return FarmOSQuantityRefsWidget(**kwargs) return FarmOSQuantityRefsWidget(**kwargs)
class AnimalTypeType(colander.SchemaType):
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
def serialize(self, node, appstruct):
if appstruct is colander.null:
return colander.null
return json.dumps(appstruct)
def widget_maker(self, **kwargs): # pylint: disable=empty-docstring
""" """
from wuttafarm.web.forms.widgets import AnimalTypeWidget
return AnimalTypeWidget(self.request, **kwargs)
class FarmOSPlantTypes(colander.SchemaType): class FarmOSPlantTypes(colander.SchemaType):
def __init__(self, request, *args, **kwargs): def __init__(self, request, *args, **kwargs):

View file

@ -29,7 +29,7 @@ import colander
from deform.widget import Widget, SelectWidget from deform.widget import Widget, SelectWidget
from webhelpers2.html import HTML, tags from webhelpers2.html import HTML, tags
from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget from wuttaweb.forms.widgets import WuttaCheckboxChoiceWidget, ObjectRefWidget
from wuttaweb.db import Session from wuttaweb.db import Session
from wuttafarm.web.util import render_quantity_objects from wuttafarm.web.util import render_quantity_objects
@ -228,33 +228,6 @@ class FarmOSUnitRefWidget(Widget):
return super().serialize(field, cstruct, **kw) return super().serialize(field, cstruct, **kw)
class AnimalTypeWidget(Widget):
"""
Widget to display an "animal type" field.
"""
def __init__(self, request, *args, **kwargs):
super().__init__(*args, **kwargs)
self.request = request
def serialize(self, field, cstruct, **kw):
""" """
readonly = kw.get("readonly", self.readonly)
if readonly:
if cstruct in (colander.null, None):
return HTML.tag("span")
animal_type = json.loads(cstruct)
return tags.link_to(
animal_type["name"],
self.request.route_url(
"farmos_animal_types.view", uuid=animal_type["uuid"]
),
)
return super().serialize(field, cstruct, **kw)
class FarmOSPlantTypesWidget(Widget): class FarmOSPlantTypesWidget(Widget):
""" """
Widget to display a farmOS "plant types" field. Widget to display a farmOS "plant types" field.
@ -372,6 +345,11 @@ class UsersWidget(Widget):
return super().serialize(field, cstruct, **kw) return super().serialize(field, cstruct, **kw)
##############################
# native data widgets
##############################
class AssetParentRefsWidget(WuttaCheckboxChoiceWidget): class AssetParentRefsWidget(WuttaCheckboxChoiceWidget):
""" """
Widget for Parents field which references assets. Widget for Parents field which references assets.
@ -432,3 +410,22 @@ class LogAssetRefsWidget(WuttaCheckboxChoiceWidget):
return HTML.tag("ul", c=assets) return HTML.tag("ul", c=assets)
return super().serialize(field, cstruct, **kw) return super().serialize(field, cstruct, **kw)
class AnimalTypeRefWidget(ObjectRefWidget):
"""
Custom widget which uses the ``<animal-type-picker>`` component.
"""
template = "animaltyperef"
def get_template_values(self, field, cstruct, kw):
""" """
values = super().get_template_values(field, cstruct, kw)
values["js_values"] = json.dumps(values["values"])
if self.request.has_perm("animal_types.create"):
values["can_create"] = True
return values

View file

@ -1,4 +1,5 @@
<%inherit file="wuttaweb:templates/base.mako" /> <%inherit file="wuttaweb:templates/base.mako" />
<%namespace file="/wuttafarm-components.mako" import="make_wuttafarm_components" />
<%def name="index_title_controls()"> <%def name="index_title_controls()">
${parent.index_title_controls()} ${parent.index_title_controls()}
@ -14,3 +15,8 @@
% endif % endif
</%def> </%def>
<%def name="render_vue_templates()">
${parent.render_vue_templates()}
${make_wuttafarm_components()}
</%def>

View file

@ -0,0 +1,13 @@
<div tal:define="
name name|field.name;
oid oid|field.oid;
vmodel vmodel|'modelData.'+oid;
can_create can_create|False;"
tal:omit-tag="">
<animal-type-picker tal:attributes="name name;
v-model vmodel;
:animal-types js_values;
:can-create str(can_create).lower();" />
</div>

View file

@ -0,0 +1,128 @@
<%def name="make_wuttafarm_components()">
${self.make_animal_type_picker_component()}
</%def>
<%def name="make_animal_type_picker_component()">
<script type="text/x-template" id="animal-type-picker-template">
<div>
<div style="display: flex; gap: 0.5rem;">
<b-select :name="name"
:value="internalValue"
@input="val => $emit('input', val)"
style="flex-grow: 1;">
<option v-for="atype in internalAnimalTypes"
:value="atype[0]">
{{ atype[1] }}
</option>
</b-select>
<b-button v-if="canCreate"
type="is-primary"
icon-pack="fas"
icon-left="plus"
@click="createInit()">
New
</b-button>
</div>
<${b}-modal v-if="canCreate"
has-modal-card
% if request.use_oruga:
v-model:active="createShowDialog"
% else:
:active.sync="createShowDialog"
% endif
>
<div class="modal-card">
<header class="modal-card-head">
<p class="modal-card-title">New Animal Type</p>
</header>
<section class="modal-card-body">
<b-field label="Name" horizontal>
<b-input v-model="createName"
ref="createName"
expanded
@keydown.native="createNameKeydown" />
</b-field>
</section>
<footer class="modal-card-foot">
<b-button type="is-primary"
@click="createSave()"
:disabled="createSaving || !createName"
icon-pack="fas"
icon-left="save">
{{ createSaving ? "Working, please wait..." : "Save" }}
</b-button>
<b-button @click="createShowDialog = false">
Cancel
</b-button>
</footer>
</div>
</${b}-modal>
</div>
</script>
<script>
const AnimalTypePicker = {
template: '#animal-type-picker-template',
mixins: [WuttaRequestMixin],
props: {
name: String,
value: String,
animalTypes: Array,
canCreate: Boolean,
},
data() {
return {
internalAnimalTypes: this.animalTypes,
internalValue: this.value,
createShowDialog: false,
createName: null,
createSaving: false,
}
},
methods: {
createInit(name) {
this.createName = name || null
this.createShowDialog = true
this.$nextTick(() => {
this.$refs.createName.focus()
})
},
createNameKeydown(event) {
// nb. must prevent main form submit on ENTER
// (since ultimately this lives within an outer form)
// but also we can submit the modal pseudo-form
if (event.which == 13) {
event.preventDefault()
this.createSave()
}
},
createSave() {
this.createSaving = true
const url = "${url('animal_types.ajax_create')}"
const params = {name: this.createName}
this.wuttaPOST(url, params, response => {
this.internalAnimalTypes.push([response.data.uuid, response.data.name])
this.$nextTick(() => {
this.internalValue = response.data.uuid
this.createSaving = false
this.createShowDialog = false
})
}, response => {
this.createSaving = false
})
},
},
}
Vue.component('animal-type-picker', AnimalTypePicker)
<% request.register_component('animal-type-picker', 'AnimalTypePicker') %>
</script>
</%def>

View file

@ -26,6 +26,7 @@ Master view for Animals
from webhelpers2.html import tags from webhelpers2.html import tags
from wuttaweb.forms.schema import WuttaDictEnum from wuttaweb.forms.schema import WuttaDictEnum
from wuttaweb.util import get_form_data
from wuttafarm.db.model import AnimalType, AnimalAsset from wuttafarm.db.model import AnimalType, AnimalAsset
from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView from wuttafarm.web.views.assets import AssetTypeMasterView, AssetMasterView
@ -137,6 +138,55 @@ class AnimalTypeView(AssetTypeMasterView):
def get_row_action_url_view(self, animal, i): def get_row_action_url_view(self, animal, i):
return self.request.route_url("animal_assets.view", uuid=animal.uuid) return self.request.route_url("animal_assets.view", uuid=animal.uuid)
def ajax_create(self):
"""
AJAX view to create a new animal type.
"""
model = self.app.model
session = self.Session()
data = get_form_data(self.request)
name = data.get("name")
if not name:
return {"error": "Name is required"}
animal_type = model.AnimalType(name=name)
session.add(animal_type)
session.flush()
if self.app.is_farmos_mirror():
token = self.request.session.get("farmos.oauth2.token")
self.app.auto_sync_to_farmos(animal_type, token=token)
return {
"uuid": animal_type.uuid.hex,
"name": animal_type.name,
"farmos_uuid": animal_type.farmos_uuid.hex,
"drupal_id": animal_type.drupal_id,
}
@classmethod
def defaults(cls, config):
""" """
cls._defaults(config)
cls._animal_type_defaults(config)
@classmethod
def _animal_type_defaults(cls, config):
route_prefix = cls.get_route_prefix()
permission_prefix = cls.get_permission_prefix()
url_prefix = cls.get_url_prefix()
# ajax_create
config.add_route(f"{route_prefix}.ajax_create", f"{url_prefix}/ajax/new")
config.add_view(
cls,
attr="ajax_create",
route_name=f"{route_prefix}.ajax_create",
permission=f"{permission_prefix}.create",
renderer="json",
)
class AnimalAssetView(AssetMasterView): class AnimalAssetView(AssetMasterView):
""" """