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値が取得できないのでエラーになります。
うまい解決方法が見つかったら、別の記事に書きたいと思います。
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-csv
はcsvをモデルクラスから作るためのもの。
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で設定したクラスをもとにして、valueをcsv形式の文字列にしてるのかな
結果として、動きませんでした。
その時のエラーがこちらです。
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"
と書くこともできます。
ですが、上記のように書くと他のパラメータを追加する際に困ったことになります。
よほどのことがない限りは避けましょう。