utamaro’s blog

誰かの役に立つ情報を発信するブログ

Djangoのテンプレート内でcloudinaryのディレクトリを指定して画像を表示する

Djangoを使って、cloudinaryで保存している画像を表示する場合、{% cloudinary %}を使用します。

その際に、ファイル名を指定することができるのですが、ファイル名のみだとディレクトリ分けしていた場合表示されません。

そこでなんとかディレクトリを指定して、画像を表示したいと思いました。

そこで取った方法が以下の書き方です。

{% cloudinary public_id crop="fill/dir" class="is-rounded" %}

これで出力されるurlは以下のようになります。

http://res.cloudinary.com/hqarhdoek/image/upload/c_fill/dir/image_name

ポイントはcrop部分です。

もともとはcropの値はfillだけで、その場合のurlは以下のようになります。

http://res.cloudinary.com/hqarhdoek/image/upload/c_fill/imange_name

ちなみに、folder="dir"としてもだめでした。

問題点

widthやheightを指定する場合、以下のようなurlになります。

'https://res.cloudinary.com/demo/image/upload/c_fill,h_150,w_100/sample.jpg' 

すると、fill/dirのようになってもうまく動きません。

あくまでもファイル名の前の区切りがフォルダとしなければなりません。

その場合は、おとなしくサーバーサイドでファイル名をつくりましょう。

herokuに上げて動作を確認してると、imageが404になったりしたので、この方法は間違っているのかもしれません。

その検証はしてなくて、他のバグを直してるときについでに解決していたので未検証です。

ただ、ローカルでは動いていたのでいまいち原因がわかってないです。

Djangoでログイン後に特定の処理を挟む方法

表題の通りですが、Djangoでログイン後に一回だけ処理を入れたいときの方法を考えて見ました。

一つしか思いつかなかったのですが、紹介したいと思います。

やることをは簡単で、ログイン後のリダイレクトに一つだけviewを挟む形です。

まずは、settings.pyに以下の項目を追加します。

LOGIN_REDIRECT_URL = 'before-home'

これを追加すると、ログイン後にbefore-homeにリダイレクトされます。

before-homeというのはurls.pyにnameで設定した名前を入れます。

次にview.pyを修正します。

class GoToHomeView(RedirectView):
    pattern_name = 'home'

    def get_redirect_url(self, *args, **kwargs):
        request = self.request
        if request.user.is_authenticated:
            # ログインしている場合は、なにか処理する
            # is_authenticatedを入れなくても良いですが、ログインしてるとは限らない
            self.execute(request.user)
        return super().get_redirect_url(*args, **kwargs)

    def execute(self, user):
        # なにか処理

RedirectViewについては説明は↓に記載されています。

https://docs.djangoproject.com/ja/2.1/ref/class-based-views/base/#redirectview

コードの例では、pattern_nameにhomeを指定していて、リダイレクトするとhomeが呼ばれます。

get_redirect_urlをオーバーライドしており、ログイン後に処理が実行されるようになります。

if request.user.is_authenticated:でセーフコードを入れる理由としては、ログインしなくてもbefore-homeが実行される可能性があるからです。

@require_loginを入れても同じことができると考えたのですが、以下のコードでつまづきました。

    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            if test_func(request.user):

このコードはfrom django.contrib.auth.decorators内のコードで、login_requiredが実行されたときに呼ばれます。

その際にRedirectViewではuser値が取得できないのでエラーになります。

f:id:miyaji-y26:20181023080759p:plain

エラーの内容

うまい解決方法が見つかったら、別の記事に書きたいと思います。

Djangoでtwitter認証をしたあとにツイートする方法

前提として、social-auth-app-djangoを使ってtwitterでの認証が実装できている状態とします。

class TwitterPost(TemplateView):
    post_api = "https://api.twitter.com/1.1/statuses/update.json"

    def post(self, request, *args, **kwargs):
        social_user = UserSocialAuth.objects.filter(user=request.user).first()
        # ここでバリデーションを入れるべきでしょう。
        twitter_oauth = OAuth1Session(
            SOCIAL_AUTH_TWITTER_KEY,
            SOCIAL_AUTH_TWITTER_SECRET,
            social_user.tokens.get('oauth_token'),
            social_user.tokens.get('oauth_token_secret'),
        )
        params = {"status": 'twitter post api test.', "lang": "ja"}
        result = twitter_oauth.post(self.post_api, params)
        return redirect('home')

バリデーションのコードは省いています。

これが実行されると、ログインしているユーザがtwitter post api test.とツイートします。

次にhtmlのテンプレートを(特別なことはやっていませんが)htmlのどこかに入れます。

{% if user.is_authenticated %}
<form action="{% url 'twitter_post' %}" method="post">
    {% csrf_token %}
    <button type="submit">
        <span>twitterで共有</span>
    </button>
</form>
{% endif %}

settings.pyに以下のpathを追加します。

    path('twitter_post', TwitterPost.as_view(), name='twitter_post'),

これでOK

Djangoのtemplate内でクエリパラメータを設定する方法

Djangoのtemplate内で以下のように書いているときに、リンク先をクエリパラメータで渡すときの方法です。

<li><a href="{% url 'home' %}">next</a></li>

渡したいパラメータはpとします。値は整数です。

まず、一番簡単な方法を紹介します。

<li><a href="{% url 'home' %}?p=1">next</a></li>

この方法はシンプルですが、パラメータが増えたときに大変なことになります。

具体的には以下のようにどこからどこまでがパラメータとして設定しているのか分かりづらくなります。

{% url 'home' %}?p=1&s=23&d=201810&f=90

この問題を解決するための少し、面倒な方法を紹介します。

以下のようなディレクトリ構成でtemplate_tags/tags.pyというファイルを作成します。

.
├── manage.py
├── project
│   ├── __init__.py
│   ├── assets
│   ├── django.log
│   ├── settings
│   ├── templates
│   ├── urls.py
│   └── wsgi.py
└── template_tags
    ├── __init__.py
    └── tags.py

tags.pyの内容は以下のように書きます。

from django import template

register = template.Library()


@register.simple_tag
def query_transform(request, **kwargs):
    updated = request.GET.copy()
    for k, v in kwargs.items():
        updated[k] = v

    return updated.urlencode()

これはDjangoのcustom templateという機能で使えるものです。

https://docs.djangoproject.com/en/2.1/howto/custom-template-tags/

次に、settings.pyを修正します

librariesのところから追加します。

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR,  'templates')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
            'libraries': {  # ← これを追加する
                'tags': 'template_tags.tags',
            }
        },
    },
]

最後に、template(html)を修正します。

どこでも良いのですが、以下の記述を書きます。

私の場合は<body>直下に書いています。

<body>
    {% load tags %}
    ...

tagsはlibraries.tagsの名称です。つまり、template_tags.tagsで登録しているタグを読み込むということです。

ではタグを使ってリンクを書き直します。

<li><a href="{% url 'home' %}?{%query_transform request p=1%}">next</a></li>

だいぶスッキリしました。

複数のパラメータを渡したい場合は{%query_transform request p=1 q=2%}となります。

SpringBootでLocalDateTimeを含んだデータをCSV形式で出力する方法

SpringBootを使って、データ内にLocalDateTime型のフィールドがある場合のCSVファイル作成方法です。

CSVファイルを作成して、そのファイルをダウンロードするのではなく、CSV文字列を返すイメージです。

ブラウザでGETリクエストをするとダウンロードできるようにしてみました。

依存関係を追加する

依存関係をgradleで追加します。

dependencies {
    compile(
        //https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-csv
        'com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.2.3',

        //https://mvnrepository.com/artifact/com.fasterxml.jackson.datatype/jackson-datatype-jsr310
        // jacksonでjava8のdate型を使うため
        'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.7',
    )

jackson-dataformat-csvcsvをモデルクラスから作るためのもの。

jackson-datatype-jsr310はjava8のDateがモデル内にあった場合に必要になるもの。

資料参照しつつ試したこと

csvデータの作成

github(jackson-dateformats-csv)の使用方法を見ました。 https://github.com/FasterXML/jackson-dataformats-text/tree/master/csv

ドキュメントの中間あたりに以下のコードがあったのでこれを参考にしてます。

CsvMapper mapper = new CsvMapper();
Pojo value = ...;
CsvSchema schema = mapper.schemaFor(Pojo.class); // schema from 'Pojo' definition
String csv = mapper.writer(schema).writeValueAsString(value);

こんなことを考えました。

  • PojoクラスはModelクラスみたいなものだろう。たぶんgetterとかsetterがあれば良いはず。
  • CsvMapperはObjectMapperみたいなものだろう。継承してるし、configureの設定とかできそう
  • 最後にschemaで設定したクラスをもとにして、valuecsv形式の文字列にしてるのかな

結果として、動きませんでした。

その時のエラーがこちらです。

com.fasterxml.jackson.core.JsonGenerationException: CSV generator does not support Object values for properties
    at com.fasterxml.jackson.core.JsonGenerator._reportError(JsonGenerator.java:1961)
    at com.fasterxml.jackson.dataformat.csv.CsvGenerator.writeStartObject(CsvGenerator.java:327)
    at com.fasterxml.jackson.core.base.GeneratorBase.writeStartObject(GeneratorBase.java:286)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:151)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719)

なにかのパラメータが原因でcsvが作れてないのかな?と考えましたが、わからないので片っ端から調べました。

どうやらLocalDateTimeが原因らしいというのがわかったので、解決方法を調べました。

以下のコードを追加することで解消できました。

このコードはstackoverflowから見つけたものです。(タブを消してしまって、もう一度検索して見つけることができませんでした。orz)

    JavaTimeModule javaTimeModule = new JavaTimeModule();
    javaTimeModule.addDeserializer(
        LocalDateTime.class, 
        new LocalDateTimeDeserializer(DateTimeFormatter.ISO_DATE_TIME)
    );
    csvMapper.registerModule(javaTimeModule);

おそらく、マッピングするさいにjava8のLocalDateTimeに対応していないマッパーを、ISO_DATE_TIMEで文字列に変換できるようにしているのだと思います。

上記のコードではなく、Modelクラスに@DateTimeFormatを追加して対応してみましたが、結果は変わりませんでした。

ダウンロードの設定

headerと、ファイル名を指定していご、レスポンスを返しています。

HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "text/csv;");
String  filename = "hoge";
headers.setContentDispositionFormData("filename", filename + ".csv");
return new ResponseEntity<byte[]>(csv.getBytes(), headers, HttpStatus.OK);

完成コード

※ 一つのControllerに処理を書いていますが、一般的にはServiceクラスに分けた方が良いです。

@ResponseBody
@RequestMapping(value = "/download/csv", method = RequestMethod.GET)
public Object downloadCsv() {
    CsvMapper csvMapper = new CsvMapper();
    csvMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
    CsvSchema schema = csvMapper.schemaFor(DataModel.class).withHeader();
    JavaTimeModule javaTimeModule = new JavaTimeModule();
    // Hack time module to allow 'Z' at the end of string (i.e. javascript json's)
    javaTimeModule.addDeserializer(
            LocalDateTime.class,
            new LocalDateTimeDeserializer(DateTimeFormatter.ISO_DATE_TIME)
    );
    csvMapper.registerModule(javaTimeModule);
    // ↓DBからデータをセレクト
    List<DataModel> dataList = service.selectAll();
    String csv = null;
    try {
        csv = csvMapper.writer(schema).writeValueAsString(dataList);
    } catch (JsonProcessingException e) {
        // 本来はExceptionとか出します。
        e.printStackTrace();
    }
    HttpHeaders headers = new HttpHeaders();
    headers.add("Content-Type", "text/csv;");
    String  filename = "hoge";
    headers.setContentDispositionFormData("filename", filename + ".csv");
    return new ResponseEntity<>(csv.getBytes(), headers, HttpStatus.OK);
}

SpringBootでクライアントからの日付をLocalDateTimeで受け取る方法

クライアントから2018-10-19T10:10という文字列を受け取ったときに、LocalDateTimeで受け取る方法を紹介します。

LocalDateTimeで受け取れると何かと便利です。Stringで受け取ると、それをDateに直したりするのが面倒なので、結構使える方法なのではないでしょうか。

それでは、javaのプログラムを紹介します。

プログラム

import lombok.Data;

@Data
public class SampleForm {
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private LocalDateTime sampleDateTime;  // '2018-10-19T10:10'
}

controllerの実装の一部です

    // /xxxx?sampleDateTime=yyyy-MM-ddTHH:mm みたいなリクエストを受け取る
    @RequestMapping(method = RequestMethod.GET)
    public String sampleView(SampleForm form) {
        return "sample";
    }

@DateTimeFormatで指定しているDateTimeFormat.ISO.DATE_TIMEのパターンは、yyyy-MM-dd'T'HH:mm:ss.SSSXXXと設定されています。

クライアントからのリクエスト方法は様々だと思いますが、大抵はjsでデータを作ったり、datetime-localを使ったり、datepickerを使ったりしていると思います。

それぞれのフォーマットに合わせてDateTimeFormatを指定しても良いです。

そこらへんは仕様書を作って、フロント側との調整が必要になると思います。

独自のパターンを指定する場合は以下のように指定します。

@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")

SpringBootでPathVariable付きのリダイレクトをする方法

spring bootを使って、@PathVariableがついているurlにリダイレクトする際の方法について紹介します。

設定は、/redirect/fromにリクエストがあった際に、/redirect/to/{id}へリダイレクトするときの書き方です。

それぞれのurlの仕様について説明します。

/redirect/from

  • postでリクエストを受け付ける。
  • /redirect/to/777へリダイレクトする。

/redirect/to/{id}

  • getでリクエストを受け付ける。
  • redirect/to.htmlを表示する。(サンプルではhtmlまでは載せていません。)
  • idを@PathVariableで取得する。

サンプル

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

@Controller
@RequestMapping(value = "/redirect/")
public class ServicePaymentMethodStatusApiController {

    @RequestMapping(value="/from", method = RequestMethod.POST)
    public String redirectFrom(UriComponentsBuilder uriBuilder) {
        UriComponents uriComponent = uriBuilder
                .path("/redirect/to")
                .pathSegment("777")
                .build();
        // ↓ /redirect/to/777 が作成される。
        String redirectPath = uriComponent.toUri().getPath();
        return "redirect:" + redirectPath;
    }

    @RequestMapping(value="/to/{id}", method = RequestMethod.GET)
    public String redirectTo(@PathVariable("id") int id, UriComponentsBuilder uriBuilder) {
        // thymeleafやjspでレンダリング
        return "redirect/to";
    }
}

UriComponentsBuilderを使ってpathを組み立てています。

このbuilderを使わなくてもreturn "redirect:/redirect/to" + "/777"と書くこともできます。

ですが、上記のように書くと他のパラメータを追加する際に困ったことになります。

よほどのことがない限りは避けましょう。