Redmineのフィールドの値を他のDBの値と紐づけたいが自由入力でデータが揺らいで困るというケースがよくあると思います!そんな時、他のDBというのがREST-APIなんかで値が取ってこれるならばこんな感じで作ってみては?という事でメモを残しておこうと思います。
環境
環境は以下の通りです。
- Redmine 6.0.4.stable
- View Customize plugin 3.5.2
- Django 5.0.4
- djangorestframework 3.15.1
Redmineの設定
「障害対応」というプロジェクトを作って、「ホスト名」というカスタムフィールドを作ります。この「ホスト名」を設備管理のDBをDjangoのREST-APIを使って取得してきてDATALIST的に選択させるわけですが、まずカスタムフィールドを作成します。ここでカスタムフィールド番号はこの例では「1」で作られていますので、皆様の環境に合わせて修正をお願いします。
カスタムフィールドの作成
http://localhost/redmine/custom_fields/1/edit

チケットのイメージ
以下のようなチケットを作成するとします。

チケットを編集画面にした際のホスト名を入力するINPUT TEXTな部分は、カスタムフィールドの番号が「1」なので…

という事で、「id=”issue_custom_field_values_1″」となりますので、これを記憶しておいて、表示のカスタマイズを作成します。
表示のカスタマイズ
ここで、どこをカスタマイズするのかは、上記の通りですが、REST-APIからどんな値をどうやってとってくるかについては、後述することとして、host.name でホスト名が取得できるとして進めます。

コードの部分
$(document).ready(function () {
const $input = $('#issue_custom_field_values_1');
const $popup = $('<div id="host-search-popup"></div>').css({
display: 'none',
position: 'absolute',
background: '#fff',
border: '1px solid #ccc',
'z-index': 9999,
'max-height': '200px',
overflow: 'auto',
padding: '5px',
});
$('body').append($popup);
let debounceTimer = null;
$input.on('input', function () {
const query = $(this).val().trim();
// ✅ 4文字未満ならポップアップを閉じ、検索もしない
if (query.length < 4) {
$popup.hide();
return;
}
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
$.ajax({
url: `/testapp/api/host/search/?q=${encodeURIComponent(query)}`,
method: 'GET',
success: function (data) {
$popup.empty();
if (!data || data.length === 0) {
$popup.append('<div>一致するホストがありません</div>');
} else {
data.forEach(host => {
const $item = $('<div></div>').text(host.name).css({
padding: '4px',
cursor: 'pointer'
}).hover(function () {
$(this).css('background', '#f0f0f0');
}, function () {
$(this).css('background', '');
}).click(function () {
$input.val(host.name);
$popup.hide();
});
$popup.append($item);
});
}
const offset = $input.offset();
$popup.css({
top: offset.top + $input.outerHeight(),
left: offset.left,
width: $input.outerWidth()
}).show();
},
error: function () {
$popup.text('検索エラー').show();
}
});
}, 300); // debounce
});
$(document).on('click', function (e) {
if (!$(e.target).closest('#host-search-popup, #issue_custom_field_values_1').length) {
$popup.hide();
}
});
});
query.lengthとして5文字以上入力してからREST-APIに聞きに行くようにしています。1文字目からだとマッチしすぎて大量に引っかかるので…
url: /testapp/api/host/search/?q=${encodeURIComponent(query)},
の箇所はREST-APIのリスエスト用URLで、queryは入力された文字が入る。後述するDjangoのviews.pyで部分マッチさせるとリストが返ってくるのでforeachで回してホスト名だけを取得してDATALIST的にDIVで並べる。1つも待っちしない場合は、「一致するホストがありません」と表示させる。
URLにホスト名(FQDN)の記載がありませんが、CORS(Cross-origin resource sharing)制約に引っかかるので他のホスト名ではエラーになるのでお気を付けください。なので実はRedmineのルートパスを/redmine/ に変更しております!変更方法はこちら
DjangoのREST-API設定
Djangoの設定は、こちらのページで作ったものを利用することとします。その際には、REST-APIは設定していなかったため追加の手順を以下に記します。
パッケージのインストール
$ docker exec -it ap /bin/ash
/ # cd /code
/code # source venv/bin/activate
(venv) /code # pip install djangorestframework
各種ファイルの設定
/code/testprj/urls.py
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('testapp/', include('testapp.urls', namespace='api')),
]
/code/testapp/urls.py
from django.urls import path, include
from .views import *
from . import views
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('host', HostViewSet)
app_name = 'testapp'
urlpatterns = [
path('api/', include(router.urls), name='api'),
]
/code/testapp/views.py
from django.shortcuts import render
# Create your views here.
from .models import *
from rest_framework import viewsets
from rest_framework.pagination import PageNumberPagination
from rest_framework.decorators import action
from rest_framework.response import Response
from .serializers import *
# ページネーション用
class PostPagination(PageNumberPagination):
page_size=15
class HostViewSet(viewsets.ModelViewSet):
queryset = Host.objects.all().order_by('-id')
serializer_class = HostSerializer
pagination_class = PostPagination
@action(detail=False, methods=['get'], url_path='search')
def search(self, request):
query = request.GET.get('q', '')
queryset = Host.objects.filter(name__icontains=query)[:50]
data = [{'name': s.name} for s in queryset]
return Response(data)
/code/testapp/serializers.py
from rest_framework import serializers
from .models import *
class HostSerializer(serializers.ModelSerializer):
class Meta:
model = Host
fields = '__all__'
/code/testprj/settings.py
INSTALLED_APPS = [
'rest_framework',
http://localhost:8010/testapp/api/host/search/?q=

実行イメージ
