くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

Django Admin Siteが便利すぎてつらい

admin-site便利。

以下、公式サイトより。

Djangoのパワフルな特徴の1つは、自動的に生成されるadminインタフェースです。あなたのモデルクラスからメタデータを読み取り、モデル中心のインタフェースを提供します。このインタフェースのおかげで、ユーザはあなたのサイトのコンテンツを操作することができます。adminサイトのオススメの使い方は、組織内で利用する管理ツールに利用を制限することです。adminサイトは、あなたのサイトのフロントエンドやその周辺を含んだ全体を作成することを意図していません。

  • Modelを作って、
  • Admin Siteに登録すると、
  • Modelの参照・更新・削除の画面を自動生成してくれる 管理画面用のサイト。これがデフォルトの機能なのがすごい。。

urls.py

python manage.py startprojectの雛形で以下のような感じになっている

from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
]

admin.py

python manage.py startappの雛形でadmin.pyがあるので、そこにいろいろ設定 ちなみに、models.pyはこんな感じ。

# models.py
from django.db import models
from django.utils.translation import ugettext_lazy

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)
  
  def __str__(self):
    return "{}".format(self.title)

  class Meta: # 表示クラス名を変更する
    verbose_name = ugettext_lazy("TODO")
    verbose_name_plural = ugettext_lazy("TODO")

admin.ModelAdminを継承したクラスを作って、もろもろを設定

# admin.py
from django.contrib import admin
from .models import Todo

@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
  ordering = ['id'] # idの昇順でソート
  list_display = ('id', 'name') # リストで表示するフィールド

小ネタ

ManyToManyFieldを連結した表示にしたい

# models.py
from django.db import models
from django.utils.translation import ugettext_lazy

class Category(models.Model):
  name = models.CharField('カテゴリ名', max_length=20)

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)
  categories = models.ManyToManyField(Category, verbose_name='カテゴリ')
# admin.py
from django.contrib import admin
from .models import Todo

@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
  ordering = ['id'] # idの昇順でソート
  list_display = ('id', 'name', 'category_list') # リストで表示するフィールド
  
  # list_displayの独自カラムは関数に置き換えれるので、よい感じの表示に変更
  def category_list(self, obj):
      return ", ".join([cat.name for cat in obj.categories.all()])

ImageFieldを画像で表示したい

# models.py
from django.db import models

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)
  image = models.ImageField()
# admin.py
from django.contrib import admin
from django.utils.html import format_html
from .models import Todo

@admin.register(Todo)
class TodoAdmin(admin.ModelAdmin):
  ordering = ['id'] # idの昇順でソート
  list_display = ('id', 'name', 'thumbnail') # リストで表示するフィールド
  
  # list_displayの独自カラムは関数に置き換えれるので、よい感じの表示に変更
  def thumbnail(self, obj):
      if obj.image and hasattr(obj.image, 'url'):
          return format_html('<img src="{}" width="50" height="50" />', obj.image.url)
      else:
          return 'No Image'

Apacheでデプロイする

設定せずにデプロイすると、adminサイトのcssが読み込まれない。。

1. settings.pyに以下を追加。
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static") # プロジェクトルート直下のstaticディレクトリ
2. 利用されているcssをプロジェクトルート直下のstaticディレクトリに配置
$ python manage.py collectstatic
3. /etc/httpd/con.d/の設定にプロジェクトルート直下のstaticディレクトリを参照するようAlias
Alias /static/ /var/www/my_project/my_app/static/
<Directory /var/www/my_project/my_app/static>
    Require all granted
</Directory>

以上!!

参考にしたサイト様

AndroidのAlarmManagerに再度入門する

いつもAlarmMangerには悩まされるのので、再度調べてみたときの備忘録。。version追従辛い。。

AlarmManagerで設定するサンプル

Context context = ....;

// AlarmManagerを取得する
AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

// Alarmのタイプ
int type = AlarmManager.RTC_WAKEUP;

// Alarmの開始日時
long triggerAtMills = Calendar.getInstance().getTimeInMillis();

// Alarmの繰り返し間隔
long intervalMills = AlarmManager.INTERVAL_DAY;

// Alarmの発火時に発行するIntent
int requestCode = 0;
Intent intent = new Intent(context, MyService.class);
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
PendingIntent operation = PendingIntent.getService(context, requestCode, intent, flags);

// AlarmManagerに登録する
alarmManager.setRepeating(type, triggerAtMillis, intervalMillis, operation);

// AlarmManagerから削除する
alarmManager.cancel(operation);

Alarmのタイプ

// 時刻
int type = AlarmManager.RTC;

// RTC + 実機スリープ中にはWAKE UP
int type = AlarmManager.RTC_WAKEUP;

// スリープ時間を含んだブートアップからの経過時間
int type = AlarmManager.ELAPSED_REALTIME;

// ELAPSED_REALTIME + 実機スリープ中にはWAKE UP
int type = AlarmManager.ELAPSED_REALTIME_WAKEUP;

Alarmの繰り返し間隔

// あらかじめ定数がいろいろ用意されているので、好きにつかう
long intervalMillis = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
long intervalMillis = AlarmManager.INTERVAL_HALF_HOUR;
long intervalMillis = AlarmManager.INTERVAL_HOUR;
long intervalMillis = AlarmManager.INTERVAL_HALF_DAY;
long intervalMillis = AlarmManager.INTERVAL_DAY;

PendingIntentのflags

//    PendingIntent存在しない場合, 新規作成せず、NULLを返す。
int flags = PendingIntent.FLAG_NO_CREATE;
//     PendingIntentが存在する場合, IntentのExtra Dataを更新する。
int flags = PendingIntent.FLAG_UPDATE_CURRENT;
//     PendingIntentが存在している場合, 一度キャンセルしてから新規作成する。
int flags = PendingIntent.FLAG_CANCEL_CURRENT;
//     PendingIntentがただ一度のみ使われるときに利用する.
int flags = PendingIntent.FLAG_ONE_SHOT;

PendingIntentの作成方法

// Service
Intent intent = new Intent(context, MyService.class);
PendingIntent operation = PendingIntent.getService(context, requestCode, intent, flags);

// Broadcast Reciever
Intent intent = new Intent(context, MyReciever.class);
PendingIntent operation = PendingIntent.getBroadcast(context, requestCode, intent, flags);

// Activiy
Intent intent = new Intent(context, MyActivity.class);
PendingIntent operation = PendingIntent.getActivity(context, requestCode, intent, flags);

AlarmManagerへの登録

API 18(Android 4.3)

// *** ~ API 18(Android 4.3)
// 単発アラーム
alarmManager.set(type, triggerAtMillis, operation);
// 繰り返しアラーム
alarmManager.setRepeating(type, triggerAtMillis, intervalMillis, pendingIntent); 

API 19(Android 4.4) ~ API 22(Android 5.1)

// *** API 19(Android 4.4) ~ API 22(Android 5.1)
// 単発アラーム
alarmManager.set(type, triggerAtMillis, operation); // おおまかなアラーム
alarmManager.setExact(type, triggerAtMillis, operation); // 正確なアラーム

// 繰り返しアラーム
alarmManager.setRepeating(type, triggerAtMillis, intervalMillis, pendingIntent); // 正確なアラーム
alarmManager.setInexactRepeating(type, triggerAtMillis, intervalMillis, operation); // おおまかなアラーム
alarmManager.setWindow(type, triggerAtMillis, intervalMillis, operation); //設定したWindow内の正確なアラーム

API 23(Android 6.0) ~

// *** API 23(Android 6.0) ~
// 単発アラーム
alarmManager.set(type, triggerAtMillis, operation);
alarmManager.setAndAllowWhileIdle(type, triggerAtMillis, operation); // Doze中でも発火

alarmManager.setExact(type, triggerAtMillis, operation);
alarmManager.setExactAndAllowWhileIdle(type, triggerAtMillis, operation); // Doze中でも発火

// 繰り返しアラーム
alarmManager.setRepeating(type, triggerAtMillis, intervalMillis, pendingIntent);
alarmManager.setInexactRepeating(type, triggerAtMillis, intervalMillis, operation);
alarmManager.setWindow(type, triggerAtMillis, intervalMillis, operation);

AlarmManagerの登録情報を確認

$ adb shell dumpsys alarm

以上!!

参考にしたサイト様

Android7.xで電話・通話の自動応答できるようにするまで

Androidはバージョンが上がるごとにセキュリティ対策が進み、利用できなくなるAPIがある。
電話の自動応答もその一つで。数少ない情報も使えない場合が多い。

調べてやってみたこと

  1. ITelephonyを使って、acceptCall()を実行する(The internal ITelephony.aidl method)
  2. HeadSetの応答ボタンをエミュレート(The Headset KeyCode Intent Method)
  3. 内部のshellからinput keyeventを送信(Run input keyevent Method)

    • Android7.xから使えないらしい。Android6.x(Marshmallow)まではOK
  4. Telecom Frameworkを使う

Telecom Frameworkとは

公式の説明では、こんな感じ。

Android Telecom Frameworkは、Androidバイスでの音声通話とビデオ通話を管理します。 これには、ConnectionService APIの実装者が提供するVOIPコールだけでなく、 SIMベースのコール(テレフォニーフレームワークなど)も含まれます。

なるほど、通話の管理に対するフレームワークらしい。 更に読み進めると。

Telecomが扱う主な2つのコンポーネントは、ConnectionServicesとInCallServicesです。

ConnectionService実装は、何らかの手段(例えば、VOIP)を使用して別のパーティにコールを接続する役割を担います。 電話で最も一般的なConnectionServiceの実装は、キャリア呼び出しの接続を担当するTelephony ConnectionServiceです。

InCallService実装は、Telecomが管理する呼び出しにユーザーインターフェイスを提供し、ユーザーにこれらの呼び出しを制御したり操作したりする手段を提供します。 デバイスにバンドルされている電話アプリは、InCallServiceの実装の最も一般的な例です。

Telecomは、ConnectionService実装によって提供される呼び出しを、InCallService実装によって提供される呼び出し側ユーザーインターフェイスにルーティングする、スイッチボードとして機能します。

なにやら、2パターンあるらしい。

1, システムの電話アプリの代替品を作成したい 2. Androidの通話体験に統合された呼び出しソリューションを作成したい

うん。むずかしい。。。

1. Creating a Replacement Phone App(電話アプリを作りたい)

電話アプリを作りたいときは、InCallServiceを使えということらしい。

Android端末で標準のの電話アプリの代わりを作成する場合は、InCallService APIを実装します。

InCallServiceには呼び出し機能がなく、呼び出しのためのユーザーインターフェイスのみで構成されている必要があります。InCallServiceは、テレコムフレームワークが認識しているすべてのコールを処理する必要があります。

通話の性質(例えば、通話がSIMベースの電話通話であると仮定して)を想定してはいけません。また、ConnectionServiceに基づいて通話制限を実装してはいけません。

詳細については、「InCallService」を参照してください。

2. Integrating a Calling Solution(通話ソリューションをインテグレートしたい)

電話アプリではなく、通話をむにゃむにゃしたいときには、ConnectionServicesを使うらしい

呼び出し元のソリューションをAndroidに統合する場合は、次のオプションがあります。

2-1 Implement the self-managed ConnectionService API(全部自分で管理する)

これは、既定の電話アプリ内で自分の通話を表示したくないスタンドアロン通話アプリケーションの開発者にとって、またユーザーインターフェイスに他の通話を表示したくない場合に最適です。

従来、スタンドアロンの通話アプリケーションは、電話状態を聞いて他の通話がいつ行われているかを判断しようとしていました。

これは、電話機の状態が、ユーザがインストールした可能性のある他の通話アプリを考慮に入れていないため、問題である。

自己管理されたConnectionServiceを使用すると、デバイスでのネイティブテレフォニーコールだけでなく、このAPIを実装する他のスタンドアロンのコールアプリとの相互運用性を保証することができます。

セルフマネージドConnectionService APIは、オーディオルーティングとフォーカスも管理します。 詳細については、「自己管理接続サービス」を参照してください。

2-2 Implement the managed ConnectionService API(管理されたConnectionService APIを使う)

既存のデバイス電話アプリケーション(getDefaultDialerPackage()を参照)を使用して呼び出しのためのユーザーインターフェイスを提供する呼び出しソリューションの開発を容易にします。

例として、SIP通話の第三者実装、またはVOIP通話サービスがあります。

ConnectionServiceだけでは、コールの接続方法のみが提供されますが、関連付けられたユーザーインターフェイスはありません。

詳細については、ConnectionServiceを参照してください。

2-3 Implement both the InCallService and ConnectionService API(どっちも使う)

独自のConnectionServiceベースの呼び出しソリューションを独自のユーザーインターフェイスで作成し、同じユーザーインターフェイスで他のすべてのAndroid呼び出しを表示する場合に最適です。

この方法を使用する場合でも、InCallServiceが表示する呼び出しのソースについては想定しないでください。

カスタムのInCallServiceにデフォルトの電話アプリが設定されていなくても、ConnectionServiceの実装が機能していることを確認する必要があります。

InCallServiceを使ってみる

InCallService | Android Developersを見てみた。

このサービスは、電話を管理するためのユーザーインターフェイスを提供したいすべてのアプリによって実装されています。 Telecomは、ライブ(アクティブまたは着信)通話が存在している間にこのサービスにバインドし、通話中のアプリと最近切断された通話をインコールアプリに通知するために使用します。 テレコムサービスがInCallService実装にバインドされる前に、まずアプリケーションをデフォルトの電話アプリとして設定する必要があります(getDefaultDialerPackage()を参照)。

以下は、InCallServiceのマニフェスト登録の例です。 metadata(METADATA_IN_CALL_SERVICE_UI)は、この特定のInCallService実装が、組み込みのコールインUIを置き換えることを示しています。

UIは標準の電話アプリに任せて、それ以外の部分を操作できるよう。

参考にしたサイト様

昔の解決策の参考サイト様

公式サイトのReferencesたち

Telecom関連の参考サイト様

ConnectionService関連の参考サイト様

InCallService関連の参考サイト様

電話アプリ関連の参考サイト様

【小ネタ】取得したファイルをURIをRetrofitで送信する

表題まま。取得したファイルをURIをRetrofitで送信したときの備忘録。
そのままだとうまくいかないので、RetrofitをRequestBodyを継承して、
writeTo()時にファイル読み込むようにするといいっぽい

 private fun createRequestBody(uri: Uri, context: Context): RequestBody {
        val contentResolver = context.contentResolver
        val contentType = MediaType.parse(contentResolver.getType(uri))

        val projection = arrayOf(MediaStore.MediaColumns.SIZE)
        var fileName: String? = null
        val length = contentResolver
                .query(uri, projection, null, null, null)
                ?.use {
                    if (it.moveToFirst()) {
                        // URIから拡張子を取得
                        val ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(context.contentResolver.getType(uri))
                        // ファイル名を取得
                        fileName = "${it.getString(0)}.$ext"
                        it.getLong(0)
                    } else null
                } ?: throw IllegalArgumentException("uriからファイルサイズを取得できません")

        return object : RequestBody() {

            override fun contentLength(): Long = length

            override fun contentType(): MediaType? = contentType

            override fun writeTo(sink: BufferedSink) {
                Okio.source(context.contentResolver.openInputStream(uri)).use {
                    sink.writeAll(it)
                }
            }

            override fun toString(): String {
                return fileName ?: super.toString()
            }
        }
    }

参考にしたサイト様

AndroidでRetrofit2をつかってみた

AndroidAPIクライアントライブラリのRetrofit2を使ってみたときの備忘録。

build.gradle
// Retrofit
compile 'com.squareup.retrofit2:retrofit:2.3.0'

// OkHttpのログを見たいとき
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'

// AdapterにRxJavaを使うとき
compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0' // RxJava2はこっち。RxJava1用もあるので注意
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
compile 'io.reactivex.rxjava2:rxjava:2.1.0'

// ConverterにGsonを使うとき
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
compile 'com.google.code.gson:gson:2.8.1'

How To Use

InterfaceでAPIを定義する

public interface ApiClient {
  @POST("login")
  Observable<LoginBean> login(@Query("user") String user, @Query("pass") String pass);
}

InterfaceからAPIクライアントを生成する

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.BASIC);

 ApiClient apiClient = new Retrofit.Builder()
        .client(new OkHttpClient.Builder().addInterceptor(interceptor).build()) // ロガーを追加したOkHttpClientを設定
        .baseUrl("https://sample.com/")
        .addConverterFactory(GsonConverterFactory.create()) // ConverterにGsonを使う
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) // CallAdapterにRxJava2を使う
        .build() // ビルダーを作って
        .create(ApiClient.class); // APIクライントを生成

How To Test

RxJava2のテストは非同期のため、通常通りではうまくいかない
テスト用のTestObserverがあるので、それを使ってテストする
※RxJava1ではTestSubscriberだったが、RxJava2ではTestObserverに置き換わった

@RunWith(AndroidJUnit4.class)
public class TestLoginUseCase {
    private static final String TAG = TestLoginUseCase.class.getSimpleName();

    @Test
    public void test_login_success() throws CtiException {
        // Dagger2を使っているので、そこからインスタンスを取得
        AppModule module = new AppModule(getApplication());
        ServerApi serverApi = module.provideServerApi();
        PrefHandler prefHandler = module.providePrefHandler(module.provideContext());
        prefHandler.setCCode("dev_test");
        LoginUseCase loginUseCase = module.provideLoginUseCase(serverApi, prefHandler);

        String uid = "admin001";
        String pwd = "123456";
        // テスト用のObserverを用意
        TestObserver<LoginBean> subscriber = TestObserver.create();
        loginUseCase.login(uid, pwd).subscribe(subscriber);

        subscriber.awaitTerminalEvent();  // CompleteかErrorになるまで待つ
        subscriber.assertComplete()         // Completeかどうかのアサート
                .assertNoErrors();                  // Errorがないかのアサート
        LoginBean loginBean = subscriber.values().get(0); // Completeになった結果を取得
        assertEquals("200", loginBean.getCode());
        assertEquals("success", loginBean.getStatus());
        assertEquals("success", loginBean.getMessage());
        assertEquals("success", loginBean.getExpiration_datetime());
    }

    AppApplication getApplication() {
        return (AppApplication) InstrumentationRegistry.getTargetContext().getApplicationContext();
    }
}

参考にしたサイト様

Windows10のWSLで、Python3+Djangoしたい

やっと、Windows10のWSLを触るように。。Python環境を整えてた時の備忘録。

やったこと

$ python -V
# => Python 3.6.5
$ pip -V
# => pip 9.0.1 from /usr/lib/python3/dist-packages (python 3.6)
$ virtualenv --version
# => 16.0.0

1. pipのインストール

Ubuntu18.04のdefaultでは、python3がインストールされているので、python3-pipをインストール

$ sudo apt install -y python3-pip

2. virtualenv/virtualenvwrapper のインストール

$ sudo pip instal virtualenv virtualenvwrapper 

# 設定を追加
$ cat << EOS >> ${HOME}/.bashrc
export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3
export WORKON_HOME=$HOME/.virtualenvs
export PROJECT_HOME=$HOME/py_projects
source `which virtualenvwrapper.sh`
EOS

$ source ~/.bashrc

$ chmod 777 ~/.virtualenvs
$ find ~/.virtualenvs -maxdepth 1 -type f | xargs -I{} chmod 755 {}

3. 仮想環境の作成・削除

# 作成
$ virtualenv <仮想環境名>
$ mkvirtualenv --no-site-package <仮想環境名>

# 作成:pythonのバージョン指定
$ virtualenv -p python3.6 <仮想環境名>
$ mkvirtualenv --no-site-package -p /usr/bin/python3 <仮想環境名>

# 削除
$ rm -rf <仮想環境名>
$ rmvirtualenv <仮想環境名>
  • mkvirtualenv--no-site-packageは、ベースとなるpythonのsite-packageを受け継がないようにする指定

4. 仮想環境の有効化・無効化

# 有効化
$ source <仮想環境名>/bin/activate
$ workon <仮想環境名>
# 無効化
$ deactivate
5. Djangoのインストール
$ pip install django
6. Djangoプロジェクトの作成・起動
$ django-admin.py startproject <サイト名>

$ cd <サイト名>
$ python manage.py migrate
$ python manage.py runserver

以上!!

参考にしたサイト様

DjangoでRESTfulなAPIしてみた

DjangoREST APIを簡単に作れるフレームワーク(djangorestframework)を使ってみた。 サクッと作れるので、いい感じ!

インストール

とりあえず、pipでインストール

$ pip install django djangorestframework

インストールしてたら、rest_frameworkを追加

INSTALLED_APPS = (
    ...
    'rest_framework',
)

概要

構成要素

  1. Model ・・・ DjangoのModel
  2. Serializer ・・・Modelをどのようにシリアライズ(・デシリアライズ)するかを決めるためのもの
  3. ViewSet ・・・ APIのクエリーをどう解釈するかを決めるためのもの
  4. URL Pattern ・・・ DjangoにURLのパターンを教えるためのもの

依存関係は、「Model ⇐ Serializer ⇐ ViewSet ⇐ URL Pattern」という感じ。

Model(models.py)

from django.db import models

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)

Serializer(serializer.py)

from rest_framework import serializers
from .models import Todo

class TodoSerializer(serializers.ModelSerializer):
    class Meta:
        model = Todo
        fields = '__all__'

ViewSet(views.py)

from rest_framework import viewsets

from .models import Todo
from .serializer import TodoSerializer

class TodoViewSet(viewsets.ModelViewSet):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer

URL pattern(urls.py)

from rest_framework import routers
from .views import TodoViewSet

router = routers.DefaultRouter()
router.register(r'todos', TodoViewSet)
from django.conf.urls import url, include
from api.urls import router as api_router

urlpatterns = [
    path('api/', include(api_router.urls)), # api.urlsをincludeする
]

これで、「 http://api/todos/ 」にアクセスすると、todoの一覧が取得できるようになる!

小ネタ

MenyToManyFieldやForeignKeyを展開する

デフォルトのだと、外部キーや多対多のModelは展開されず、pkで表現される。 参照しているRelationは展開してほしい。。

展開する場合は、Serializerを変更するとよい感じに。

models.py
from django.db import models

class Category(models.Model):
  name = models.CharField('カテゴリ名', max_length=20)

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)
  categories = models.ManyToManyField(Category, verbose_name='カテゴリ')
serializer.py
from rest_framework import serializers
from .models import Todo, Category

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = '__all__'

class TodoSerializer(serializers.ModelSerializer):
    # 対象のフィールドのSerializerを置き換えると、CategorySerializerを使って展開される
    # ManyToManyのように複数の場合は「many=True」をつける
    # contextを設定すると、URLの展開などをしてくれる
    categories = CategorySerializer(many=True, context=self.context) 
    
    class Meta:
        model = Todo
        fields = '__all__'

MenyToManyFieldごとCreate/Updateしたい

デフォルトのままだと、Todoを追加/更新する場合に、同時にCategoryも追加されてしまう。。 というか、その前のvalidateで、「Categoryがすでに存在します」みたいになる。。 Categoryが存在したらcreateして、存在しなかったらgetしてほしい。。

既存のcreate/update処理をカスタマイズする場合も、Serializerを変更する

models.py
from django.db import models

class Category(models.Model):
  name = models.CharField('カテゴリ名', max_length=20)

class Todo(models.Model):
  title = models.CharField('タイトル', max_length=20)
  done = models.BooleanField('完了', default=False)
  categories = models.ManyToManyField(Category, verbose_name='カテゴリ')
serializer.py
from rest_framework import serializers
from .models import Todo, Category

class CategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = Category
        fields = ['id', 'name']
        extra_kwargs = { # validationを無効にする
            'id': {'validators': []}, 
            'name': {'validators': []},
        }
  
    def validate_name(self, data): # 独自のvalidationを追加。なにもしない
    return data

class TodoSerializer(serializers.ModelSerializer):
    categories = CategorySerializer(many=True) 
    
    class Meta:
        model = Todo
        fields = '__all__'
    
    # create時の処理をカスタマイズ
    def create(self, validated_data):
     # categoriesの部分をpopして退避
        categories_data = validated_data.pop('categories')
        
        # 残りのvalidated_dataからTodoをcreate
        todo = Todo.objects.create(**validated_data)
        todo.save()
        
        # 退避したcategories_dataからcategoriesを追加
        # idのcategoryがあれば、取得して追加。なければ、createして追加
        for category_data in categories_data:
          category = Category.objects.filter(id=category_data['id']).first()
          if category is None:
            category = Category.objects.create(**category_data)
          todo.categories.add(category)
    return todo
    
    # update時の処理をカスタマイズ
    def update(self, instance, validated_data):
        # categoriesの部分をpopして退避
        categories_data = validated_data.pop('categories')
        
        # Todo部分の更新処理
        instance.name = validated_data.get('name', instance.name)
        instance.done = validated_data.get('done', instance.name)

        # 一旦、タグを全削除
        instance.tags.clear() 
        # 追加時と同様に、退避したcategories_dataからcategoriesを追加
        for category_data in categories_data:
           category = Category.objects.filter(id=category_data['id']).first()
           if category is None:
               category = Category.objects.create(**category_data)
           instance.categories.add(category)
    
        instance.save()
    return instance
参考サイト

RESTっぽくない、Web APIを追加したい

RESTだとModelと対応したAPIの形になるので、ちょっとロジックを入れ込んだAPIとかを用意しにくい。。 これも、「ViewSetにactionを追加する」という形で独自のエントリポイントを追加できる。

from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

from .models import Todo
from .serializer import TodoSerializer

class TodoViewSet(viewsets.ModelViewSet):
    queryset = Todo.objects.all()
    serializer_class = TodoSerializer
    
    @action(detail=False) # pkを取らないactionの場合は、「detail=False」にする
    def is_done(self, request):
        try:
            name = request.query_params.get('name') # GETパラメタを取得
            todo = Todo.objects.filter(name=name).first()
            
            # 独自の形式のresponse
            response = {
              'data': TodoSerializer(todo, context={'request': request}).data, # serializeしたdictを取得・設定
              'is_done': todo.is_done
            }
            return Response(response)
        except:
            return Response({'message': 'error'}, status=status.HTTP_400_BAD_REQUEST)

これで「 http://api/todos/is_done/?name=AAA 」にアクセスすると、is_doneが呼び出せるようになる!

参考サイト

以上!!

参考にしたサイト様