diadia

興味があることをやってみる。自分のメモを残しておきます。

ダイアログを実装する

ダイアログについて

まずダイアログとは、画面上に出てくるボックスのことで何かを知らせるとともに、ユーザーに複数のボタンを押してもらうことでアクションを起こしてもらうものである。
トーストもユーザーに何かを通知する機能は同じだが、通知するのみでユーザーがアクションを起こせないという点が異なる。

ダイアログを実装する

実装する概要、イメージ

  1. ダイアログのボックスを作る
  2. どこかをタップして表示させる仕組み(リスナ、ハンドラ)を作成する
  3. リストビューにリスナを設置する
ダイアログのボックスを作る

ダイアログのボックスを作るとは、ダイアログのタイトルとかメッセージ、ボタンを作ることを意味する。
ただし、自分自身で直接ダイアログのボックスを作成するわけではない。自分自身で行うことはダイアログビルダを生成し、そのビルダにどのようなダイアログを作りたいのか教え、ビルダのメソッド(create)を通じてダイアログ(ボックス)を作成する。したがって、ダイアログを作成するとは、ダイアログビルダインスタンスを生成し、ビルダのcreateメソッドを実行することを意味する。

具体的なコードは以下のとおりである。

#ダイアログのボックスを作成する方法

#ダイアログビルダを生成する
val builder = AlertDialog.Builder(activity)


#ダイアログビルダを通じてダイアログインスタンスを生成する
val dialog = builder.create()

しかし待ってほしい。どこにこのビルダのコードを記述するのか。それもきちんと決まっているようだ。
ビルダを記述する場所は、DialogFragmentを継承したクラスの中で記述する。そしてビルダでダイアログを記述したあとにはそのダイアログを返すことも決まっている。ということで、以下のようになる。

#ダイアログのボックスを作成する方法(ビルダの記述する場所も含めて)


class MyDialogFragment : DialogFragment(){
    override fun onCreateDialog(savedInstanceState:Bundle?):Dialog{
    
    //ダイアログビルダを生成
    val builder = AlertDialog.Builder(activity)
    
    //ダイアログのタイトルを設定
    builder.setTitle(R.string.dialog_title)
    
    //ダイアログのメッセージを設定
    builder.setMessage(R.string.dialog_message)
    
    //ダイアログのポジティブボタンを設定
    builder.setPositiveButton(R.string.dialog_btn, DialogButtonClickListener())
    
    //ダイアログビルダを通じてダイアログインスタンスを生成する
    val dialog = builder.create()
    return dialog
    }
}

 

リストビューにリスナを設置する

リストビューにリスナを設置するのは、class MainActivity: AppCompatActivity(){}内に以下の内容を追記する。

lv_obj.onItemClickListener = MyListener()    

ButtonとListViewのリスナーについて

ButtonとListViewにつけるリスナーが紛らわしいので整理

Button

ButtonにつけるリスナーはsetOnClickListenerである。
そしてListenerインスタンスはView.OnClickListenerを使えば良い。

使用例


class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
    val btClick = findViewById<Button>(R.id.btClick)
    val listener = TapOnClickListener()
    btClick.setOnClickListener(listener)
    
    }
    
    private inner class TapOnClickListener:View.OnClickListener{
        override fun onClick(view:View){
            val input  = findViewById<EditText>(R.id.etName)
            val inputStr = input.text.toString()
            val output = findViewById<TextView>(R.id.tvOutput)
            }
    }
}
    

ListView

ListViewにつけるリスナーは、onItemClickListenerである。
そしてListenerインスタンスはAdapterView.OnItemClickListenerを使えば良い。

使用例



class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
    val lvMenu = findViewById(R.id.lvMenu)
    lvMenu.onItemClickListener = ListItemClickListener()
    }
    
    private inner class ListItemClickListener: AdapterView.OnItemClickListener{
        override fun onItemClick(parent:AdapterView<*>,
                                 view: View,
                                 position: Int,
                                 id: Long)
        {
            val item = parent.getItemAtPosition(position) as String

            Toast.makeText(applicationContext, item, Toast.LENGTH_LONG).show()

        }
    }
}

メモ

androidのバックエンドとしてdjangoが使える

ソース:https://www.slideshare.net/kaki_k/jaws-ug-geeklab?next_slideshow=1

python-gcmについて

android開発にpython-gcmなるものが必要である可能性が出てきた。

これはpush通知に必要な機能だと考えられる。進んできたらまた調べるかもしれない。

https://pypi.org/project/python-gcm/

 

フッターを画面下に表示させる

参考:https://public-constructor.com/footer-at-bottom-with-flexbox/

結論

    
<body >

<div class="container-fluid mx-0 px-0 test">
{% include 'config/include/navbar.html' %}

{%  block content %}
{%  endblock %}
<div class="espacio">
</div>


{% include 'config/include/bottom.html' %}

</div>
.test {
	display: flex;
	flex-flow: column;
	min-height: 100vh;

}

.espacio{
	flex: 1;
}
	

footer {

    background: #333333;
    color: #ffffff;
    height: 80px;
}

django-allauth環境下でメールアドレスを変更する

条件

メールアドレスでユーザー認証を行う場合に、UserモデルかEmail Addressモデルのどちらのデータを使ってユーザー認証を行っているかわからない。
またEmail Addressインスタンスを変更するとUserモデルインスタンのEメールも自動的に変更されるのかもわからない。この辺を明らかにしていきたい。

検証

Userモデルのemailアドレスをadminから変更する。そして変更後のemailアドレスを使ってログインを試みる。

結果

Userモデルのemailアドレスを変更したところ、変更したアドレスとパスワードを使ってユーザー認証することができた。
また変更前のアドレスとパスワードを使ってもユーザー認証づる事ができた。

結論

Authentication and AuthorizationのUserモデルのemailaddressでもユーザー認証できるし、AccountsのEmail Addressモデルのemailaddressでもユーザー認証できる。したがってemailアドレスを変更する場合には両方とも変更しなければ、セキュリティに問題が生じる。

django Userモデルのusernameを変更させる

各ユーザーがユーザーネームを自由に変更できるようにしたい。時間がかかってしまったので記録しておく。

概要

django-allauthを使っている。ユーザー認証及び登録はemailアドレスで行っている。
そのためusernameはemailアドレスの@以前の部分が自動的に割り当てられる。当初usernameの変更のためにemailアドレスを変更する必要があるかもしれないと思っていたが、django.contrib.auth.models.Userモデルのインスタンスを直接変更すれば済む結論が得られた。

手続き

usernameを変更する際にdjango-allauthまたはビルトインのUserバリデーションを利用したいと考えた。そのためusernameの仕組みを少し調べたが詳しくはわからなかった。ただhttps://github.com/django/django/blob/master/django/contrib/auth/forms.py#L81にUserCreationFormがあり、それを参考に今回はusernameの変更を試みた。

forms.py...

from django import forms
from django.contrib.auth.models import User


class UsernameChangeForm(forms.ModelForm):
    
    class Meta:
        model  = User
        fields = ("username",)
        field_classes = {'username': UsernameField}
        
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        for field in self.fields.values():
            field.widget.attrs['class'] = "form-control"
            field.widget.attrs['placeholder'] = "150 characters or fewer. Letters, digits and @/./+/-/_ only."

views.py...

from django.shortcuts import render, redirect
from django.views.generic import View
from django.contrib.auth.models import User
from django.contrib import messages

from app.forms import UsernameChangeForm



class UsernameChangeView(View):
	def get(self, request, *args, **kwargs):
		context = {}
		form = UsernameChangeForm()
		context["form"] = form
		return render(request, 'app/change_username.html', context)


	def post(self, request, *args, **kwargs):
		context = {}
		form = UsernameChangeForm(request.POST)

		if form.is_valid():
			username = form.cleaned_data["username"]
			user_obj = User.objects.get(username=request.user.username)
			user_obj.username = username
			user_obj.save()
			messages.info(request,"usernameを変更しました。")
		
			return redirect('profiles:profile')
		else:
			context["form"] = form

			return render(request, 'config/change_username.html', context)

django FB, Googleアカウントでユーザー認証を実現する

ユーザー認証を実現するには、いくつかのライブラリがありそれを利用すれば良い事がわかった。

 https://simpleisbetterthancomplex.com/tutorial/2016/10/24/how-to-add-social-login-to-django.html
https://scotch.io/tutorials/django-authentication-with-facebook-instagram-and-linkedin
https://hodalog.com/oauth-authentication-in-dja/

今回はdjango-allauthを使って実装する。実際試してみてかなり簡単だと思った。

django-allauthのドキュメント

https://django-allauth.readthedocs.io/en/latest/providers.html

settings.pyの記述

#INSTALLED_APPSに記述する内容

'allauth',
'allauth.account',
'allauth.socialaccount',
'allauth.socialaccount.providers.google',#googleを使う場合はこれを追加する
'allauth.socialaccount.providers.facebook',#facebookを使う場合はこれを追加する


#social account providerの設定もsettings.pyに記述する
SOCIALACCOUNT_PROVIDERS = {
    'facebook': {
        'METHOD': 'oauth2',
        'SCOPE': ['email', 'public_profile', 'user_friends'],
        'AUTH_PARAMS': {'auth_type': 'reauthenticate'},
        'INIT_PARAMS': {'cookie': True},
        'FIELDS': [
            'id',
            'email',
            'name',
            'first_name',
            'last_name',
            'verified',
            'locale',
            'timezone',
            'link',
            'gender',
            'updated_time',
        ],
        'EXCHANGE_TOKEN': True,
        'LOCALE_FUNC': 'path.to.callable',
        'VERIFIED_EMAIL': False,
        'VERSION': 'v2.12',
    },
     'google': {
        'SCOPE': [
            'profile',
            'email',
        ],
        'AUTH_PARAMS': {
            'access_type': 'online',
        }
    }
}


login機能を実装するテンプレートに記述する内容

#テンプレートタグをhtmlファイル上部に記述する
{% load socialaccount %}
{% providers_media_js %}



<a href="{% provider_login_url "facebook" method="js_sdk" %}">Login Button image</a>

Facebookのログイン機能を実装するときに困ったら

エラーメッセージを表示させることが解決の一歩だが、どのように表示させるかを記録しておく。
まずSNSアカウントログインエラーを表示するテンプレートは、allauth/socialaccount/authentication_error.htmlである。
このテンプレートに{{ auth_error }}を記述すると、テンプレートが表示されたときにエラーメッセージも表示されることになる。
この表示されたエラーメッセージをもとにググれば解決するだろう。
参考:https://stackoverflow.com/questions/36728887/debugging-django-allauth-social-network-login-failure/36728888#36728888

facebook developer サイトでの設定事項について

うまくログインができない場合は以下の設定がなされているか確認すると良い。

Settings Basic
- App Domains: "AnySite.com"
- Privacy policy URL: "https://AnySite.com/myprivacy/"
- Website: "https://AnySite.com/"
Settings Advanced
- Server IP Whitelist: let it blank
- Domain Manager: let it blank

Facebook login Settings
Yes Client OAuth Login
Yes Web OAuth Login
Yes (new: forced) Use strict Mode for redicect URLs
Yes Embeded Browser OAuth Login
Yes Enforce HTTPS
Valid OAuth Redirect URLs: "https://AnySite.com/accounts/facebook/login/callback/" (mandatory)

参考:https://stackoverflow.com/questions/49350221/django-2-0-allauth-facebook-2018?noredirect=1&lq=1

GoogleのOAAUTHを使ったログイン機能を実装する

googleのOAauthログイン機能をdjango-allauthで実装すると割と簡単にできる。それを少しまとめておく。
実装の手続き概要:

  1. デプロイするならドメインを作成する
  2. GoogleAPIConsoleでプロジェクトを作成する
  3. Googleアカウント連携に必要なクライアントIDとシークレットキーを入手します
  4. django-allauthのsettings.pyにgoogleの関連項目を記述する。
  5. adminページでgoogleのクライアントIDとシークレットキーを使用する。またSITEも編集する。

デプロイ環境でgoogleのログイン機能を実現するには、ドメインが必要になる。localhost:8000のまましようとするとエラーが出てしまうので注意する。
ドメインはfreenomで無料で取得できる。

GoogleAPIConsoleへ移動する。

 

f:id:torajirousan:20191217121053p:plain

 

 

テンプレートのログインURLについて

django-allauthをインストールしたallauth/account/login.htmlにはsocialログインの部分が記載されておらず、記載されているところは、allauth/socialaccount/snippets/provider_list.htmlである。これがインクルードタグを通じてhtmlに表示される。

{% for provider in socialaccount_providers %}

<a title="{{provider.name}}" class="socialaccount_provider {{provider.id}} btn btn-primary" 
     href="{% provider_login_url provider.id process=process scope=scope auth_params=auth_params %}">{{provider.name}}</a>

{% endfor %}   

問題はここにある。以下の記事でBootstrap-socialという便利なcssがある事がわかった。これを利用するにはclass属性にgooglefacebook等の内容を記載しなければならない。だからここを直接記載するためにgoogle,Facebookのリンクを表示させるにはいかに書けばいいかという問題が残る。

 

Bootstrap-socialの紹介記事:
https://medium.com/@jinkwon711/django-allauth-facebook-login-b536444cbc6b
Bootstrap-socialの実装手続きの記事:
https://pisuke-code.com/css-how-to-use-bootstrap-social/

 

以下の記事によるとgoogle,Facebookは以下のように書けばいいようだ。

 

<a href="{% provider_login_url "twitter" %}">Twitterでログイン</a>
<a href="{% provider_login_url "google" %}">Googleでログイン</a>    
<a href="{% provider_login_url "facebook" %}">Facebookでログイン</a>

https://tech.torico-corp.com/blog/django-projects-oauth2/

これでうまくいくと思われる

https化してみる

django-allauthを使っても実際に運用するとhttpのためにログインページを表示する事ができないことがあった。そのためhttps化させる必要があるとわかった。

参考:https://certbot.open-code.club/
https://narito.ninja/blog/detail/19/

大雑把な手順

certbotというコマンドでTLSサーバ証明書を取得する事ができるのでcertbotコマンドをインストール。
certbotコマンドを実行しTLSサーバ証明書を取得する。
Webサーバ(ngnx)に証明書を設定する。

参考:https://certbot.open-code.club/certonly-standalone.html#certbot%20certonly%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%81%A7%E8%A8%BC%E6%98%8E%E6%9B%B8%E5%8F%96%E5%BE%97(standalone%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3)

textareaフォームの大きさを設定する

textareaフォームを使う際にそのフォームの大きさを設定したいときがある。自分がよく使っているBootstrapではうまく対応できなかったのでその対応できなかった理由と対処方法をメモとして残す。

Bootstrapで対応できない理由

Bootstrapのsizingのところで様々な要素を変更することができる。
しかしこのサイジングを使うとサイズ変更に伴う空白部分ができてしまう。そこの空白スペースに別要素を入れようとしても入れることができなかった。
したがってwebの少ない画面を有効利用しようとする意図に反する結果となり、採用しなかった。
https://getbootstrap.com/docs/4.4/utilities/sizing/

他の方法

htmlのtextareaフォームの初設定が有る。それがrowsとcolsであった。これを直接設定すれば、空白部分を作らずにフォームのサイズを変更する事ができた。
http://www.htmq.com/html/textarea.shtml

django 独自のドメインを設定する

 

freenomでドメインを取得する

freenomでは無料でドメインを取得することができる。

f:id:torajirousan:20191214084415p:plain

 取得後にfreenomのmydomainsを開き、Manage Domainを選択する。

f:id:torajirousan:20191214085753p:plain

Management ToolsからNameserversを選択する。

f:id:torajirousan:20191214090401p:plain

 nameserver1とnameserver2にさくらVPSの情報を入力する。具体的にはNameserver1に「NS1.DNS.NE.JP」、Nameserver2に「NS2.DNS.NE.JP」を入力する。

さくらVPSで取得したドメインを登録する

 

djangoのsettings.pyを変更する

settings.pyのALLOWED_HOSTにドメインを入力する。

nginxの設定項目を変更する

vi /etc/nginx/conf.d/project.conf
server {
    listen  80;
    #server_name 153.126.216.172;
    server_name 'domainを記入する';

    location /static {
        alias /usr/share/nginx/html/static;
    }

    location /media {
        alias /usr/share/nginx/html/media;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Django:ModelChoiceFieldを使う

ドキュメント:https://docs.djangoproject.com/ja/2.2/ref/forms/fields/#modelchoicefield

現在困っているのは、Formにdjango.contrib.auth.models.Userオブジェクトを渡してフォームから別のオブジェクトを生成しようと試みているが、form.is_valid()でうまく行かない。この状況でmodelchoicefieldなる存在を知った。これが解決の糸口になるかもしれないので少々調べてみることとする。

https://www.subthread.co.jp/blog/20160531/
こちらの記事によるとやはりオブジェクトをそのまま利用するために使われるようだ。

使い方を調べる

基本的にforms.Formでmodelchoicefieldを使う記事が多い。しかしながらforms.ModelFormでも使用することができるようだ。それは以下の記事を参照
https://natsumesouxx.hatenadiary.org/entry/20100523/1274565168

from django.forms.models import ModelChoiceField

class CustomChoiceField(forms.ModelChoiceField):

class BlogForm(ModelForm):
    
    user_obj = CustomChoiceField(queryset=User.objects.all())
    class Meta:
        model = Contact    

ModelChoiceFieldを動的に使う場合の注意点

ModelChoiceFieldを動的に使う場合には、forms.pyにおけるModelFormのクラス定義に注意しなければならない。
動的に使うとは、views.pyでformインスタンスを生成し、views内で呼び出したquerysetをformインスタンスにあてがい動的な仕様に変更する。

views.py
from django.shortcuts import render 
from django.views.generic import View
from products.forms import TestModelForm


class TestView(View):
    def get(self, request, *args, **kwargs):
        context = {}
        form = TestModelForm()
        form.fields['name'] = Products.objects.filter(price=100)
        context["form"] = form
        
        return render(request, "test_model/test_modelchoice.html"

しかしながらforms.pyを以下のようにnoneを使ってしまうと、views.pyでエラーが出てしまう。理由はよくわからない。

forms.py
form django import forms
from django.forms.models import ModelChoiceField
from products.models import Product

class TestModelForm(forms.ModelForm):
    name = ModelChoiceField(queryset=Products.objects.none())
    
    class Meta:
        model  = Product
        fields = ("name", "category", "price")

必ずall()を使うこと。

forms.py
form django import forms
from django.forms.models import ModelChoiceField
from products.models import Product

class TestModelForm(forms.ModelForm):
    name = ModelChoiceField(queryset=Products.objects.all())
    
    class Meta:
        model  = Product
        fields = ("name", "category", "price")