What is a JSON feed? Learn more

JSON Feed Viewer

Browse through the showcased feeds, or enter a feed URL below.

Now supporting RSS and Atom feeds thanks to Andrew Chilton's feed2json.org service

CURRENT FEED

Dev Junior

JSON


Day17 - Markdown DIY (prevent XSS attack)

Permalink -

In Django, there is CSRF protection against the Cross-Site Request Forgery. It is so handy which reduces our workloads on security issues. 0. [TL;DR] However, XSS attack is also a problem. So what is XSS? Well, simply speaking, it is about some sneaky attackers who found out your site contains fields that enable scripts, e.g. comments with scripts running. These sneaky guys then post some scripts such as html or javascript. When a visitor come in and load the comment page, the scripts are invoked. That innocent visitor is now vulnerable to privacy exposure which may be further sent to attackers in background. This picture from Incapsula briefly sums up.


1. [Django safe filter] So, what exactly does it deal with Django site? While rendering fields in a template, we use {{ field }}. To enable scripts on certain field, a template filter safe can be used, e.g. {{ field | safe }}. Hence, any scripts in this field will not be autoescaped but able to run smoothly. Consequently, the page containing this field is now susceptible to XSS attacks. Say you have a comment session with scripts enabled and is constructed with forms.py:
from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    content = forms.CharField(required=True, widget=forms.Textarea)
    
    class Meta:
        model = Comment
        fields = ('content',)
views.py :
from django.shortcuts import render
from .forms import CommentForm

def viewComment(request):
    if request.method == 'POST':
        form = CommentForm(request.POST)
        if form.is_valid():
            form.save()
        else:
            form = CommentForm()
    else:
        form = CommentForm()
    return render(request, 'comment.html', {'form': form})
    
and template : (Note that this is the template for entering comment, but not viewing the comment)
<form method='POST'>
    {% csrf_token %}
    {{ form.as_p }}
    <input type='submit' value='Submit'>
</form>
Nothing in forms.py or views.py is going to deal with XSS. Thus, we have to do it on our own. There, is what a markdown tool can help. Of course, you can clone any markdown tool from github, but in this demo I am going to build my own.
2. [buttons for using scripts] We love to have text-decorations and insert links while typing online. To avoid users from typing these common scripts manually, (and to assist those not familiar with HTML tags), we can create some buttons for them. Firstly, head to template :
<form method='POST'>
    {% csrf_token %}
    <div>
        <button type='button' class='btn' id='bold'>Bold</button>
        <button type='button' class='btn' id='link'>Link</button>
    </div>
    {{ form.as_p }}
    <input type='submit' value='Submit'>
</form>
This will add 2 buttons Bold and Link above the fields. Then add the following javascript/jQuery to the same template:
<script>
$(document).ready(function(){
    $('#bold').click(function(){
        var temp = $('#id_content').val();
        $(#'id_content').val(temp + '[bold]Text Here...[/bold]');
    });
    $('#link').click(function(){
        var temp = $('#id_content').val();
        $(#'id_content').val(temp + '[url]Url Here...[midurl]Name of Page Here...[/url]');
    })
});
</script>
Such that when users click on either button, this will add corresponding tag, e.g. [bold]Text Here...[/bold], into the content field. #id_content should be the id of your content field as long as your field name is set as content in forms.py .
3. [adding form method] Secondly, go to forms.py , import regex and add a form method updateContent :
from django import forms
from .models import Comment
#Add this
import re

class CommentForm(forms.ModelForm):
    content = forms.CharField(required=True, widget=forms.Textarea)
    
    class Meta:
        model = Comment
        fields = ('content',)
    
    #Add this
    def updateContent(self):
        data = self.cleaned_data['content']
        lines = data.split('\n')
        newLines = []
        tags = {'[bold]': '<b>',
                '[/bold]': '</b>',
                '[url]': '<a href="',
                '[midurl]': '">',
                '[/url]': '</a>'}
        for line in lines:
            for tag in tags:
                line = re.sub(tag, tags[tag], line)
            newLines.append(line)
        newData = '\n'.join(newLines)
        return newData
Now our form has a method updateContent which detects the tags we created and return the comment content with proper HTML tags instead.
4. [calling the form method] finally head to views.py , here we modify a few lines, include calling the form method, updating the content before saving the form:
from django.shortcuts import render
from .forms import CommentForm

def viewComment(request):
    if request.method == 'POST':
        #Change this
        temp = CommentForm(request.POST)
        if temp.is_valid():
            #Add these
            content = temp.updateContent()
            form = temp.save(commit=False)
            form.content = content
            
            form.save()
        else:
            form = CommentForm()
    else:
        form = CommentForm()
    return render(request, 'comment.html', {'form': form})
In this snippet, we named a new variable temp to validate first. After that, run the form method updateContent and return the new content to a new variable content . Since we need to modify the form details before saving, we have to call temp.save(commit=False) . Lastly, assign content to form.content and de facto save the form.
5. [escaping the angle brackets] It is time to check out your comment page in browser. There should be the bold and link buttons, which add tags to content field on click. Through submitting the comment, these tags become proper HTML tags, which can be ran on the page with {{ field | safe }}. But what we have done is just providing alternatives to users, not restricting them from typing their own scripts! Oh, this is cake now. What we have done so far is created some tags for masking proper HTML tags. Similary, we can mask users' angle brackets via the same old form method. Head back to forms.py and add 2 more lines:
from django import forms
from .models import Comment
import re

class CommentForm(forms.ModelForm):
    content = forms.CharField(required=True, widget=forms.Textarea)
    
    class Meta:
        model = Comment
        fields = ('content',)
    
    def updateContent(self):
        data = self.cleaned_data['content']
        lines = data.split('\n')
        newLines = []
        #Add this dict
        brackets = {'<': '&lt;'; '>': '&gt;'}

        tags = {'[bold]': '<b>',
                '[/bold]': '</b>',
                '[url]': '<a href="',
                '[midurl]': '">',
                '[/url]': '</a>'}
        for line in lines:
            #And add this loop
            for b in brackets:
                line = re.sub(b, brackets[b], line)

            for tag in tags:
                line = re.sub(tag, tags[tag], line)
            newLines.append(line)
        newData = '\n'.join(newLines)
        return newData
See? It's done! By converting the angle brackets into their corresponding HTML entities, whenever users type their own scripts, it fails to run. Instead, the brackets will show up as normal brackets in the viewing template.


Bootstrap 3 - Buttons and colors

Permalink -

When we build a navbar, there is a button with class .navbar-toggler which is used to open and collapse the nav-items on mobile devices. A navbar-toggler is a special button since it is not quite actually a button class in bootstrap.


1. [.btn] In fact, Bootstrap has a standard class for buttons - .btn . This defines the button object should present itself as a typical button like this: and it is called by:
<button class='btn btn-primary' type='button'>Button</button>

2. [color] The .btn-primary defines the color of the button. There are different colors available such as .*-primary .*-secondary .*-danger etc, where * can be btn text bg etc. For more detail of Bootstrap colors, you take a look at this Bootstrap Cheatsheet . This cheatsheet is very useful, it shows every classes you may come across.
3. [button sizes] In addition to colors, there are classes of different button sizes. If you look for Button Modifiers in the cheatsheet, there are .btn-sm , .btn-lg and .btn-block.
<button type='button' class='btn btn-secondary btn-sm'>
    Small button
</button>

<button type='button' class='btn btn-secondary btn-lg'>
    Large button
</button>

<button type='button' class='btn btn-secondary btn-block'>
    Button block
</button>

4. [disable] In case you wish to prevent users from clicking certain buttons, you can disable it. There is a class disabled :
<button type='button' class='btn btn-danger btn-block disabled'>
    You cannot click this
</button>

For more detail of buttons, you can either go through the Bootstrap Cheatsheet or visit the official site at Buttons.Bootstrap


Bootstrap 2 - Responsive navbar and .active with jQuery

Permalink -

image from w3schools In last post we understood the basic of grid system and how it helps on responsive design. This time we are going to focus on the menu bar or the navbar .


1. [navbar classes] In this demo, we are going to construct a navbar on top of the screen in which the links will be shown from left to right.
<nav class='navbar'>
    <a href='#' class='navbar-brand'>Site Icon</a>
        
    <div class='navbar-nav'>
        <a href='./home/' class='nav-item nav-link'>Home</a>
        <a href='./page1/' class='nav-item nav-link'>Page1</a>
        <a href='./page2/' class='nav-item nav-link'>Page2</a>
    </div>
</nav>
Classes being used: navbar - tells it is going to be a navbar, navbar-brand - class for Icon or Logo or a Name, navbar-nav - the parent of the nav-items nav-items & nav-link - the link items
2. [responsive] To make a navbar responsive to devices, it means that the navbar will arrange the nav-item in different way in order to fit the screen and the nav-item will be hidden. By clicking a button with class navbar-toggler , the nav-item will be shown on a dropdown list.
<nav class='navbar'>
    <a href='#' class='navbar-brand'>Icon</a>
    <!--add the toggle button-->
    <button class='navbar-toggler' type='button' data-toggle='collapse' data-target='#navlist'>
        <span class='navbar-toggler-icon'></span>
    </button>
    
    <!--add a parent div to navbar-nav-->
    <div class='collapse navbar-collapse' id='navlist'>
        <div class='navbar-nav'>
            <a href='./home/' class='nav-item nav-link'>Home</a>
            <a href='./page1/' class='nav-item nav-link'>Page1</a>
            <a href='./page2/' class='nav-item nav-link'>Page2</a>
        </div>
    </div>
</nav>
Now, we have a button navbar-toggler which will toggle the target navbar by id #navlist. The page will not be wrapped and this button will not be available, until the page is being visited on a small screen such as smartphones.
3. [active link with jQuery] Bootstrap also provides a decorator for the active nav-link. The class active is used to remark the corresponding nav-item which the user is reading. Instead of adding the class manually on every page, we are going to add the class using jQuery in this demo The prerequisite is the address of each page In our examples, the addresses are: home = ./home/ page1 = ./page1/ page2= ./page2/ So here we go,
<script>
$(document).ready(function(){
    $(".nav-item").removeClass('active');
    var current = $(location).attr('href');
    
    if(current.search('home') > 0){
        $("[href='./home/']").addClass('active')
    } else if(current.search('1') > 0){
        $("[href='./page1/']").addClass('active')
    } else if(current.search('2') > 0){
        $("[href='./page2/']").addClass('active')
    }
});
</script>
This script will first remove all the active class whenever user visit a new page, then it looks for the current location and matches the nav-item. Like this:


Bootstrap 1 - Basic understanding the 12-grid system

Permalink -

Bootstrap is the most common and handy tool for prettifying your website by CSS. It decorates your site by providing a library of Classes from which you can build a responsive webdesign, space & position your contents and make pretty forms and buttons, etc. To get started, we need to have our .html set like this:

<!doctype html>
<html lang="en">
  <head>
    <!-- Required meta tags -->
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <!-- Bootstrap CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">

  </head>
  <body>

  <!-- jQuery, popper and Bootstrap js -->
  <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
    <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js" integrity="sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4" crossorigin="anonymous"></script>
  </body>
</html>

1. [Grid-System] To position every block or content, Bootstrap provides classes that runs the 12-grid system. This popular CSS system divides a page into 12 columns of same width. By using the classes Bootstrap provides, we can easily define if certain content should occupy 1 or 3 or even 100% width in a container. e.g.
<div class='col-3'>
This occupies 3columns
</div>

<div class='col-7'>
This occupies 7columns
</div>

<div class='col-2'>
This occupies 2columns
</div>

To effectively use the 12-grid system, we need to enclose the above snippets into a container:
<div class='container'>
    <div class='row'>
        <div class='col-3'>
        This occupies 3columns
        </div>

        <div class='col-7'>
        This occupies 7columns
        </div>

        <div class='col-2'>
        This occupies 2columns
        </div>
    </div>
</div>
Such that it becomes
This occupies 3columns
This occupies 7columns
This occupies 2columns

2. [Responsive grids] So the grids are now properly shown in 3-7-2 ratio on desktop, but how about on mobile devices? To make the site becomes responsive to devices, we can add more classes in each of the above div :
<div class='container'>
    <div class='row'>
        <div class='col-12 col-lg-3'>
        This occupies 3columns
        </div>

        <div class='col-12 col-lg-7'>
        This occupies 7columns
        </div>

        <div class='col-12 col-lg-2'>
        This occupies 2columns
        </div>
    </div>
</div>
The original classes now become col-lg-* which means they are only run on large (lg) or larger screen. Meanwhile, three col-12 are added so have each of them will occupy all 12 columns on extra small (xs) to medium (md) screen size. You may take a look on this table for better understanding on the definition of extra small to extra large.


Day16 - Social Login with social-auth-app-django

Permalink -

These days, rather than login with ID + PW, we often prefer to login via openID authentication. These OAUTH are provided by various social networks, such as Twitter, Google, Facebook etc. To allow users login with OAUTH, there is a Django package recommended, social-auth-app-django . For more details of its documentations, here is the link. OAuth 2.0: An Overview(YouTube)


1. [Installation + Settings] Install social-auth-app-django
$pipenv install social_auth_app_django
Add path or url in url.py:
path('', include('social_django.urls', namespace='social')),

Install app under settings.py:
INSTALLED_APPS = (
    ...
    'social_django',
    ...
)

MIDDLEWARE = [
    ...
    'social_django.middleware.SocialAuthExceptionMiddleware',
]

AUTHENTICATION_BACKENDS = (
    #Insert these before django's backend
    'social_core.backends.open_id.OpenIdAuth',
    'social_core.backends.google.GoogleOpenId',
    'social_core.backends.google.GoogleOAuth2',
    'social_core.backends.google.GoogleOAuth',
    'social_core.backends.twitter.TwitterOAuth',
    'social_core.backends.yahoo.YahooOpenId',
    'social_core.backends.facebook.FacebookOAuth2',

    'django.contrib.auth.backends.ModelBackend',
)

#Add pipelines
SOCIAL_AUTH_PIPELINE = (
    'social_core.pipeline.social_auth.social_details',
    'social_core.pipeline.social_auth.social_uid',
    'social_core.pipeline.social_auth.auth_allowed',
    'social_core.pipeline.social_auth.social_user',
    'social_core.pipeline.user.get_username',
    'social_core.pipeline.social_auth.associate_by_email',
    'social_core.pipeline.user.create_user',
    'social_core.pipeline.social_auth.associate_user',
    'social_core.pipeline.social_auth.load_extra_data',
    'social_core.pipeline.user.user_details',
)

TEMPLATES = [
    {
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': [os.path.join(BASE_DIR, 'templates').replace('\\', '/')],
        'APP_DIRS': True,
        'OPTIONS': {
            'context_processors': [
               ...
               ...
                #Add these
                'social_django.context_processors.backends',  
                'social_django.context_processors.login_redirect', 
            ],
        },
    },
]


2. [Get access token] To use OAUTH from social networks, you need to register and create an app with their services. In this demo, we are using Twitter p.s. "Request email address" should be checked,as we need to grab visitors' email address to merge any existing account on our site. Copy the KEY and SECRET from Twitter application settings and paste tosettings.py:
SOCIAL_AUTH_TWITTER_KEY = 'YOUR TWITTER APP KEY'
SOCIAL_AUTH_TWITTER_SECRET = 'YOUR TWITTER APP SECRET'

p.s. If you are using facebook OAUTH,you may also need this pipeline to grab email address:
SOCIAL_AUTH_FACEBOOK_SCOPE = ['email']
SOCIAL_AUTH_FACEBOOK_PROFILE_EXTRA_PARAMS = {'fields': 'id,name,email'}

For security reason, you should always keep the KEY and SECRET under .env using python-decouple.
3. [font-end settings] Finally, we add a link into login.html:
<a href='{% url "social:begin" "twitter" %}'>Login with Twitter</a>

Now, you should be able to see this page of authenication using the link.


Day15 - Customized Django Admin

Permalink -

除了customized form, Django的admin page其實也可以被customized。 要管理的項目都收納在<app>/admin.py裡面。 例如要管理comments,就要在admin.py

from django.contrib import admin
from.models import Comment

admin.site.register(Comment)

但得出來的樣子是這樣: 1. [@property] 要把主題,內容,author,time都一一顯示出來,先到models.py修改Comment Content, add short_content property:
from django.db import models
from django.template.defaultfilters import truncatewords

class Post(models.Model):
    ...
    ...

class Comment(models.Model):
    parent_post = models.ForeignKey(Post, on_delete=models.CASCADE)
    content = models.TextField()
    ...
    ...

    @property
    def short_content(self):
        return truncatewords(self.content, 10)

因為只想在admin list顯示出部份content
2. [Create subclass of admin.ModelAdmin] 回到admin.py
from django.contrib import admin
from .models import Comment

class CommentAdmin(admin.ModelAdmin):
    list_display = ('parent_post', 'short_content', 'author', 'time')

admin.site.register(Comment, CommentAdmin)

admin page就可以看到Comment變成這樣:
3. [Adding filters] 除了顯示不同columns,還可以好像excel 一樣使用filter功能。 admin.py
from django.contrib import admin
from .models import Comment

class CommentAdmin(admin.ModelAdmin):
    list_display = ('parent_post', 'short_content', 'author', 'time')
    list_filter = ('parent_post', 'author', 'time')

admin.site.register(Comment, CommentAdmin)

這樣在頁面的右邊Filter List就有我們設定的選項。
4. [Adding search fields] 如果項目太多還可以使用search_fields去search某些項目。 admin.py
from django.contrib import admin
from .models import Comment

class CommentAdmin(admin.ModelAdmin):
    list_display = ('parent_post', 'short_content', 'author', 'time')
    list_filter = ('parent_post', 'author', 'time')
    search_fields = ('parent_post__title', 'content', 'author__username')

admin.site.register(Comment, CommentAdmin)

版面就會在左上方出現了一個search bar。 要留意這裡的parent_post變成parent_post__title以及author變成author__username。 在Comment model裡面,這兩個fields都是ForeignKey連接到另一object,但search_fields可以接受的search items只有CharField或TextField,如果強行search object e.g. author,Django會出現ValueError。 要解決首先要搞清楚我們是想要search甚麼,例如parent_post我們其實search Post.title,而author其實search User.username。所以變成parent_post__title以及author__username,使用"__"double underscores去連接相對應的CharFIeld或TextField。


Day14 - Post comment, ForeignKey

Permalink -

extend User Profile時,利用了models.OneToOneField把每一個User object連接一個Profile。 Django的Field Relation還有:

  • OneToOneField
  • ManyToManyField
  • ForeignKey (即OneToMany)
這次在每一篇post下面加入comment form,要用的是ForeignKey
1. [Create Comment model]models.py加入新model - Comment
from django.db import models
from django.contrib.auth.models import User

class Post(models.Model):
    ...
    ...

class Comment(models.Model):
    parent_post = models.ForeignKey(Post, on_delete=models.CASCADE)
    author = models.ForeignKey(User, one_delete=models.CASCADE)
    content = models.TextField()
    time = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return '%s %s %s' % (self.parent_post, self.author, self.time)

  • 使用ForeignKey把Comment model連接上Post model,即eachPost object is related to many Comment objects
  • 同樣地,eachUser object is related to many Comment objects
  • 使用parent_post, author 以及time作為每篇comment的title string。 e.g.

2. [Create form]forms.py
from django import forms
from .models import Comment

class CommentForm(forms.modelForm):
    content = forms.CharField(required=True, widget=forms.Textarea(attrs={'placeholder':'Comment here...'}))

    class Meta:
        model = Comment
        field = ('content',)


3. [Insert comments in post detail page]views.py把comment session加至每篇post的下面:
from .forms import CommentForm
from .models import Post, Comment
from django.shortcuts import redirect, render
from django.contrib.auth import views as av

def postpage(request, key):
    post = Post.objects.get(pk=key)
    error = ''
    if request.user.is_authenticated:
        if request.method == "POST":
            temp = CommentForm(request.POST)
            if temp.is_valid():
                form = temp.save(commit=False)
                form.parent_post = post
                form.author = request.user
                form.save()
                form = CommentForm()
            else:
                form = CommentForm()
                error = 'There is something wrong of your comment content!'
        else:
            form = CommentForm()
    else:
        form = ''
    return render(request, 'postpage.html', {'post':post, 'comment':comment, 'form':form, 'error':error})

  • 限制只讓已登入用戶留言,利用form = ''在HTML template叫用戶登入
  • 因為Comment model有兩個parameters - parent_post及author不在form顯示,所以要用commit=false的方法自定value,然後再form.save()
  • 因為希望用戶在留comment後可以留在同一頁看到自己的comment,所以在成功form.save()後重新把form assign為unbounded form
  • 用parent_post把該Post object的comments filter成一個list,並放在最後才assign,讓新加的留言可以即時加到comment list裡面。

4. [Modify post detail page template]templates/postpage.html修改內容:
<div>{{ post.content }}</div>
    ...
    ...
    <div>Comments:</div>

    <!--To display comments-->
    {% if comment %}
    {% for c in comment %}
    {{ c.author }}: {{ c.content }}
    {% endfor %}
    {% endif %}

    <!--Check if user is logined-->
    {% if form == '' %}
    <span>Please <a href="{% url 'login' %}?next={{ request.path }}">login</a> to leave comments.</span>
    {% else %}

    <!--Display comment form-->
    <form method="POST">
    {% csrf_token %}
    {{ form.as_p }}
    <input type='submit' Value='Submit'>
    </form>
    {% endif %}

這樣每一篇Post的content下面都會出現
  1. previous comments
  2. new comment form


Day13 - Paginator, MultiSelectField

Permalink -

之前在index 利用for-loop 顯示出post_list一篇篇的post,但有一個問題沒有解決:分頁。 假如post_list有100個posts,而不想由100~1全都顯示在同一頁,Django很貼心地設有Paginator。 Paginator是在django.core.pagintor.Paginator,有關的內容可以到DjangoProject參考。 1. [Modify view function] 首先到views.py


  • 這裡使用Paginator把post_lost分成每頁10篇,where p is a subclass of Paginator
  • 當網址被GET類request打開時,找尋'page',提取一個由1開始數的頁數。1-based e.g. 1,2,3,4,5,6....
  • 把剛提取的頁數回傳至class method - Paginator.get_page(<number>), return一個page object( 例如Paginator(somelist, 5).page(1) )

2. [Add paginator to template] 到templates把以下HTML內容加入:

上面使用到的parameters包括:
  • posts = Paginator.page,從views.py return的page object
  • posts.has_previous, posts.has_next 是Paginator post object的boolean
  • .number, .previous_page_number, .next_page_number都是page object的methods
  • posts.paginator.num_pages是從page object call返associated Paginator object後,使用Paginator的class methodnum_pages

之前提及過ChoiceField以及SelectDateWidget,是在一堆選項或日子當中選取其中一個。 但假如想一次過勾選好幾個,例如tags,可以使用open source - MultiSelectField。 詳細資料可以到這裡參考。 1. [django-multiselectfield] 打開terminal安裝:
form multiselectfield import MultiSelectField

class Post(models.Model):
    ...
    tag = MultiSelectField(<max_choices=3>, choices=(
        ('tag1','tag1'),
        ('tag2','tag2'),
        ('tag3','tag3'),
        ('tag4','tag4'),
    ))


Day12 - Extend User Profile, OneToOneField, Signal, SelectDateWidget

Permalink -

上一篇新增的MyAccount頁面,只是把Django完有的User Models 以表單形式顯示出來,該用戶使用。 如果需要增加多點欄位,e.g. Gender、DOB、Address等等profile,我們需要把預設的User Models extend。 在SQL,兩個獨立的tables可以被合體:

SELECT * 
FROM User
INNER JOIN Profile ON User.id=Profile.id

在Django裡面也可以把兩個models連接,並同步更新,要使用的工具包括:
  • OneToOneField,類似SQL中把Models連上。 詳細內容可以到DjangoProject參考
  • django.db.models.signals.post_save,作為model A 發出save()時的signal類別
  • Decorator: django.dispatch.receiver,接收Signals,讓model B 跟隨model A同步更新

1. [Create new model] 首先到<app>.models.py新增一個model - Profile
from django.contrib.auth.models import User
from django import models
from django.dispatch import receiver
from django.db.models.signals import post_save

class Profile(models.Model):
    user = models.OneToOneField(User, on_delete=models.CASCADE)
    gender = models.CharField(null=True, blank=True, max_length=1)
    birthday = models.DateField(null=True, blank=True)
    City = models.TextField(null=True, blank=True)

@receiver(post_save, sender=User)
def usercreated(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)

@receiver(post_save, sender=User)
def userupdated(sender, instance, **kwargs):
    instance.profile.save()

這個class的內容重點:
  • 建立新model class - Profile
  • 使用了OneToOneField把Profile連接到auth.models.User
  • on_delete=models.CASCADE的意思是當該user被刪除時,profile同時淹沒
  • 當有新User被建立時,利用django.dispatch.receiver接收signal - post_save,新建一項Profile object至該User
  • 同樣,當用戶更新auth.models.User資料時,再save一下profile

2. [Create new form]<app>.forms.py 建立新form:
from django import forms
from .models import Profile

class ProfileForm(forms.modelForm):
    gender = forms.ChoiceField(required=False, choices=(('',''),('M','M'),('F','F')))
    birthday = forms.DateField(required=False, widget=forms.widget.SelectDateWidget(years=range(1917, 2018)))
    city = forms.CharField(required=False)

    class Meta:
        model = Profile
        fields = ('gender', 'birthday', 'city')

建立FORM跟之前其他沒什麼分別,只是這裡有兩種特別forms.fields使用了
  • forms.ChoiceField,可以使用set自定有甚麼choices。 每一個小tuple裡面,第一格是顯示在database GUI的名字 第二格才是顯示在頁面form內的選項
  • forms.widget.SelectDateWidget出來的效果就是三個selections,年月日。 假如沒有指定年份,它會顯示出今年至未來的年份。 因為這項form是birthday,所以故意限定years=range(1917,2018)這100年

3. [Modify view function]<app>.views.myaccount,加上ProfileForm:
from .forms imort UserAccount, ProfileForm

def myaccount(request):
    if request.method == "POST":
        form = UserAccount(request.POST, instance=request.user)
        profile_form = ProfileForm(request.POST, instance=request.user.profile)
        if form.is_valid() and profile_form.is_valid():
            form.save()
            profile_form.save()
            return appindex(request)
        else:
            error = 'There is something wrong about your information.'
            form = UserAccount(instance=request.user)
            profile_form = ProfileForm(instance=request.user.profile)
            return render(request, 'registration/myaccount.html', {'form':form, 'profile_form': profile_form, 'error':error})
    else:
        form = UserAccount(instance=request.user)
        profile_form = ProfileForm(instance=request.user.profile)
        return render(request, 'registration/myaccount.html', {'form':form, 'profile_form': profile_form})

在白色地方,加上ProfileForm,以便用戶在同一頁一次過save
4. [Modify template]templates/registration/myaccount.html,修改頁面,加入profile_form
{% extends 'base.html' %}
{% block content %}

<form method="POST">
    {% csrf_token %}
    {% for field in form %}
        <label for='{{ field.name }}'>{{ field.label_tag }}</label>: 
        {% if field.name != "password" %}
            {{ field }}
        {% endif %}
        {% if field.help_text %}
            {{ field.help_text | safe }}
        {% endif %}
    {% endfor %}

    {% for field in profile_form %}
        <label for='{{ field.name }}'>{{ field.label_tag }}</label>:
        {{ field }}
        {% if field.help_text %}
            {{ field.help_text }}
        {% endif %}
    {% endfor %}

    <input type='submit' value='Submit'>
</form>

{% endblock %}

這樣在同一頁內,可以讓用戶同時修改基本User資料,以及增加Profile資料 5. [No profile for existing users] 由於Profile只是在新增用戶時,才會被建立表單, 所以exiting users在登入時,會引起step 1內的instance.profile.save()錯誤。 最簡單的解決方法,是manually在shell為該用戶增建profile。 在terminal打開pipenv run python manage.py shell
>from django.contrib.auth.models import User
>u = User.objects.get(username='<username>')
>from <app>.models import Profile
>p = Profile(user=u, gender='M')
>p.save()

這樣就會在database加上了一行新的項目給該用戶 當該用戶再登入時,就不會再出error。


Day11 - UserChangeForm

Permalink -

除了password reset, password change, 當然還有修改用戶的資料。 Django更改用戶資料跟用戶註冊的原理差不多,都是使用內建的Form: django.contrib.auth.forms.UserChangeForm 但內建的UserChangeForm其實是給superuser例如admin用的,所以表單內容會比較多 - 如上圖。 為了讓用戶自行更改資料,我們可以新增一個subclass - UserAccount


1. [Set url] 先到urls.py新增urlspattern:
from <app> import views as myviews

urlspattern=[
    ...
    ...
    path('myaccount/', myviews.myaccount, name='myaccount'),
]


2. [Create view function] 新建一個views function - myaccount
from .forms import UserAccount

def myaccount(request):
    if request.method == "POST":
        form = UserAccount(request.POST, instance=request.user)
        if form.is_valid():
            form.save()
            return appindex(request)
        else:
            error = 'There is something wrong about your information.'
            form = UserAccount(instance=request.user)
            return render(request, 'registration/myaccount.html', {'form':form, 'error':error})
    else:
        form = UserAccount(instance=request.user)
        return render(request, 'registration/myaccount.html', {'form':form})

留意這裡我們除了request.POST,還加了instance=request.user 因為這一次我們希望把用戶既有的資料,預先填寫在form裡面,所以我們要強調instance。
3. [Create new form] 然後到<app>.forms開設一個class - UserAccount為UserChangeForm的subclass:
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import User

class UserAccount(UserChangeForm):
    username = forms.CharField(required=True, disabled=True)
    email = forms.EmailField(required=False, help_text='You may update your email address here')
    first_name = forms.CharField(required=False, help_text='You may edit your nickname here for display', label='Nickname')
    password = forms.CharField(
        required=False, 
        help_text="You may change your password <a href='registration/password_change'>here</a>"
    )

    class Meta:
        model = User
        fields = ('username', 'first_name', 'email', 'password',)

這裡我們利用subclass UserAccount overrides UserChangeForm原本的表單。 先是把我們希望有的fields都加上自行寫help_text,required都改為False,因為我們不一定需要用戶更改了甚麼; 再把first_name的label改為Nickname, 還有把username的field argument加上disabled=True,防止用戶自行修改, 最重要就是在Meta說明哪些fields需要被顯示出來。
4. [Create template] 最後建立一張template - registration/myaccount.html
{% extends 'base.html' %}
{% block content %}

<form method="POST">
    {% csrf_token %}
    {% for field in form %}
        <label for='{{ field.name }}'>{{ field.label_tag }}</label>: 
        {% if field.name != "password" %}
            {{ field }}
        {% endif %}
        {% if field.help_text %}
            {{ field.help_text | safe }}
        {% endif %}
    {% endfor %}
    <input type='submit' value='Submit'>
</form>

{% endblock %}

留意上面,form的內容加上了一行condition。 當for loop遇到password field時,我們故意把它不顯示出來。 因為Django 的password並不是普通地儲存在database,而是用hasher等加密了。 如果不遮罩,password field的內容會顯示出貌似:pbkdf2_xxxxxxxxxxxxxxxxxxxxxxx 而且即使用戶在這頁面改密碼自行更改,Django也不會接納。 所以乾脆把password field隱藏,然後在help text加上password_change的連結。 因為helptext是一串plain text,要把step 3的連結有效顯示,該用戶點擊,就要在help_text加上safe的filter tag。 這樣就可以變成:


Day10 - Django's views.login, @login_required,

Permalink -

之前我們有試過用自己寫的app.views.loginfunction,利用return把登入後頁面回傳到我們的首頁。 但其實Django也有它預設的login page - django.contrib.auth.views.login 登入後頁面可以使用login_redirect_url, 還可以配合django.contrib.auth.decorators.login_required使用{{ next }} parameter。 1. [Set url] 要使用預設的login功能,先到urlspattern修改: urls.py

from django.contrib.auth import views as av
django.urls import path

urlspattern=[
    ...
    ...
    path('login/', av.login, name='login'),
]

這樣就變成使用了Django's default login function。
2. [Relocate login.html] 然後把我們早前建好的template - login.html 放到 registration/login.html
3. [Login redirect after logged in] default的login function可以使用LOGIN_REDIRECT_URL來指定。 比如我們要在登入後,回到首頁 - name=blog, 到settings.py新增:
LOGIN_REDIRECT_URL = 'blog'


4. [Login required decorator] 假設有用戶在未登入時,打開了必需登入的頁面,即views function有被加上@login_required的,用戶會先被redirect至login。 如果我們希望用戶在登入後,再次回到本來的function,可以在registration/login.html加上這一行:
<form method='post'>
{% csrf_token %}
<input type='hidden' name='next' value='{{ next }}'>
...
...
這樣用戶就會在登入後被回傳至之前的頁面。 跟LOGIN_REDIRECT_URL不同,{{ next }}是在有前一功能頁時才會被使用到, 所以LOGIN_REDIRECT_URL還是需要存在。


Day9 - Change Password

Permalink -

當用戶在已登入狀態下,也可以更改password。 利用Django 內建的django.contrib.auth.forms.PasswordChangeForm可以輕鬆地執行表格。 而backend部份是由兩個functions組成:

  • django.contrib.auth.views.password_change
  • django.contrib.auth.views.password_change_done
font-end則需要兩個HTML頁面:
  • templates/registration/password_change_form.html
  • templates/registration/password_change_done.html

1. [A schema to the steps] 開始前先了解一下整個更改密碼的程序:
  1. urls.py 收到要求打開django.contrib.auth.views.password_change function
  2. Django會以function decorators @login_required去確認用戶已登入,並執行下一步
  3. auth.views.password_change查核資料正確性,及render django.contrib.auth.password_change_done 但我們會自行寫另一個app.views.changepassworddone,讓更改密碼後自動送出email notification
2. [Set url] 第一步,先到urls.py加入urlspattern:
from django.contrib.auth import views as av from <app> import views as myviews urlspattern = [ .... .... path('password_change/', av.password_change, name='password_change'), path('password_change/done/', myviews.changepassworddone, name='password_change_done'), ]
3. [Login_required decorator] @XXXXX 是Python裡面的function wrapper/decorator的意思,詳細內容可以到TheCodeShip參考。 因為password_change 以及password_change_done是需要用戶先登入,所以官方設定它們都有@login_required這個decorator。 詳細有關Password Change的official code,可以在Django Source Code For Auth.Views參考。 由於我們是用預設的auth.views去執行password_change,所以我們不用親自再寫,只需要配合用上@login_required就可以了。 到settings.py加上:
LOGIN_URL = 'login' 這樣就完成了第二步。
4. [Create template] 建立templatesregistration/password_change_form.html,以及registration/password_change_done.htmlpassword_change_form.html
{% extends 'base.html' %} {% block content %} <form method='POST'> {% for field in form %} <label for={{ field.name }}>{{ field.label_tag }}</label> {{ field }} {% if {{ field.help_text }} %} {{ field.help_text }} {% endif %} {% endfor %} <input type='submit' value='Change Password'> </form> {% endblock %} password_change_done.html
{% extends 'base.html' %} {% block content %} <span>Your password is changed successfully</span> {% endblock %}
5. [View function for email notification] 如果step 1 的urlspattern是使用av.password_change_done,其實已經完成了。 但因為我們要做一個可以發送email的版本,該用戶知道密碼被更改了, 所以要到app.views.py建立function - changepassworddone以及修改sendmail: from django.contrib.auth.decorators import login_required from django.template.loader import render_to_string from django.core.mail import send_mail @login_required def changepassworddone(request): title = 'changepassword' sendmail(request, title) return render(request, 'registration/password_change_done.html', context=None) def sendmail(request, title): email_title = title recipient = request.user.email if email_title = 'changepassword': email_content = render_to_string('registration/changepassword_email.txt', {'username':request.user.username}) send_mail( email_title, ‘’, '<youremail@email.com>', [recipient,], html_message=email_content )
6. [Email template] 最後是建立一個email內容的template,registration/changepassword_email.txt: {% autoescape off %} Hi {{ username }}! Your password is successfully changed recently! If this is you, please ignore this email. Otherwise, you are strongly recommended to reset your password on our site. Thank you. xxx Team {% endautoescape %} Done!


Day8 - Password Reset

Permalink -

Password Reset 是Django的built-in function,under django.contrib.auth.views。 因為是auth.views,所以我們不需在app/views.py自建function, 只需設定urls.py以及建立HTML檔在templates/registration/裡面。 1. [Set url] 先到settings/urls.py新增4條urlspattern:

  • password_reset
  • password_reset_done
  • password_reset_confirm
  • password_reset_complete
from django.urls import path
from django.contrib.auth.views import password_reset, password_reset_done, password_reset_confirm, password_reset_complete

urlspattern=[
...
...
    path('resetpassword/', password_reset, name='password_reset'),
    path('resetdone', password_reset_done, name='password_reset_done'),
    path('reset/<uidb64>/<token>/',
        password_reset_confirm, name='password_reset_confirm'),
    path('reset/done/', password_reset_complete, name='password_reset_complete'),
]

或是簡單地:
from django.urls import path, include

urlspattern=[
...
...
    path('', include('django.contrib.auth.urls')),
]

path()是Django2.0的新功能,收納在django.urls裡面。 在Django2.0,django.urls.path可謂取代了舊式的django.conf.urls.url, 往後會再有筆記。 這裡要知道的只是,django.contrib.auth.urls。 這套在auth底下的urls.py包括了
  • login
  • logout
  • password_reset
  • password_reset_done
  • password_reset_confirm
  • password_reset_complete
  • password_change
  • password_change_done
所以只需一行include('django.contrib.auth.urls'),就已經足夠了。 但要小心,因為它會override我們本來自設好的app.views.login以及app.views.logout功能, 而且在其他自設views function內,也不能直接用parameter name來returndjango.contrib.auth.views內的function,如這篇提及的django.contrib.auth.views.password_reset也不例外。 /* include本來是收納在django.conf.urls,在Django2.0,它改為收納在django.urls,跟path, re_path一樣。 */
2. [Create templates] 雖然urlspattern只有4項,但built-in password_reset 功能需要6個templates,放在templates/registration/裡面。
  • password_reset_form.htmlReset password頁面讓用戶輸入email address
  • password_reset_done.htmlSubmit Form後的頁面
  • password_reset_subject.txt用戶收到的email title
  • password_reset_email.html用戶收到的email內容
  • password_reset_confirm.html從email點擊連結後,重置密碼的頁面
  • password_reset_complete.html重置完成的頁面

3. [password_reset_form.html] registration/password_reset_form.html的內容: e.g.
{% extends 'base.html' %}
{% block content %}

<div>
<span>Please Enter Your Email Address To Reset Password</span>
{% for field in form %}
    <label for='{{ field.name }}'>{{ field.label_tag }}</label> {{ field }}
{% endfor %}
</div>

{% endblock %}


4. [password_reset_done.html] registration/password_reset_done.html的內容: e.g.
{% extends 'base.html' %}
{% block content %}

<div>
<span>An email is sent to you to reset your password.</span>
<span>If it is not shown in your inbox, please also check if it falls into spam folder.</span>
</div>

{% endblock %}

formauth.views.password_reset內的parameter。
5. [password_reset_subject.txt] registration/password_reset_subject.txt只是email的title: e.g. Request: Reset Your Password - from xxx website
6. [password_reset_email.html] registration/password_reset_email.html的內容: e.g.
{% autoescape off %}

We've received a request to reset your password.
If it was not you, please ignore this email and change your password on our site.
If you wish to proceed, please follow the link below:

{{ protocol }}://{{ domain }}{% url 'password_reset_confirm' uidb64=uid token=token %}

{% endautoescape %}

uidb64=uidtokenauth.views.password_reset_confirm內需要的。
7. [password_reset_comfirm.html] registration/password_reset_comfirm.html的內容: e.g.
{% extends 'base.html' %}
{% block content %}

{% if validlink %}
<form method="POST">
    {% for field in form %}
        <label for={{ field.name }}>{{ field.label_tag }}</label>{{ field }}
    {% endfor %}
    <span>Your new password should not be the same as old.</span>
    <span>And it should be consists of 8 alphanumeric characters</span>
    <input type='submit' value='Reset'>
</form>
{% else %}
<span>Your link is invalid or it is already used.</span>
<span>Please make a new request if you wish to retry.</span>

{% endblock %}

validlink是由email回傳到registration/password_reset_confirm.html時的parameter。 /* 雖然整個頁面應該只有一格填寫email的 input box,但利用for loop可以custom 介面。 */
8. [password_reset_complete.html] registration/password_reset_complete.html的內容: e.g.
{% extends 'base.html' %}
{% block content %}

Your password is reset successfully!

{% endblock %}


就這樣,一個完整的password reset procedures完成了。 /* 可以把registration/password_reset_form.html的連結,加到login頁面,方便用戶忘記密碼時使用。 */


Day7 - Registration (Unique Email) with email confirmation

Permalink -

這篇會比較長一點,因為要用到前幾篇的functions。 1. [Registration] Django的用戶登記、認證、註冊⋯等等都是由 django.contrib.auth 打理。 用戶註冊的程序比起登入要繁複一點,但logic大致相同。 首先到urls.py加入連結方式:

urlspattern=[
...
...
    url(r'^registration/$', registration, name='registration'),
]
因為我們之前註冊用戶是在terminal使用createsuperuser,所以在設定views.py之前,要先建立一套form給用戶作自己註冊。 正如之前提及,所有用戶的都經由django.contrib.auth打理,所以註冊用戶時我們要用到的有:
  • django.contrib.auth.models.User
  • django.contrib.auth.forms.UserCreationForm
  • django.forms
UserCreationForm是Django built-in的註冊用戶表單,其實我們可以使用Django內建的去註冊帳戶。 可是因為原有的註冊不要求email address,跟我們real practice有點脫節的感覺, 所以像之前的POST FORM一樣,我們還是自己重建較好。
2. [Create new form]forms.py建立form class Registraion:
from django.contrib.auth.models import User
from django.contrib.auth.forms import UserCreationForm
from django import forms

class Registration(UserCreationForm):
    username = forms.CharField(required=True, widget=forms.TextInput(attrs={'autofocus':'autofocus'}))
    email = forms.EmailField(required=True, help_text='Valid Email Required.')
    password1 = forms.CharField(required=True, help_text='At Least 8 Chars with aplabets + numbers', max_length=8, widget=forms.PasswordInput)

    class Meta:
        model = User
        fields = (username, email, password1, password2)
就這樣,我們重新定義了幾個fields,包括username,email及password1。 /* Django 本來就有兩個password,一個叫password1,另一個叫password2作confirmation用 */ 上面重新定義form時,有幾點值得留意:
  • 把email都變成required=True,即必需項目, 還加入help_text準備讓HTML顯示出黎。
  • 另外我們還在username及password1引入了widget,目的是更改它們既有的form種類。 例如password1改成PasswordInput,用家在輸入表格時,密碼會自己遮閉,變成我們熟識的「....」
  • widget另一個好處是可以為表格增加attributes,例如size, rows, cols, resize, autofocus等等, 原理其實是把它看成HTML5裡的<Input>,以username為例:<input type='text' autofocus>這樣意思。
/* 有關django forms的fields還有widget的種類,可以參考Forms Fields以及Forms Widgets */
3. [Create view function]views.py編寫之前,我們先草擬一下要寫一個怎樣的 Register function:
  1. urls接收到要運行views的command, 而views.Register連接HTML template - templates/account/registration.html
  2. 如果用戶已登入,回傳到首頁
  3. 設定一個subclass為我們剛寫的class - forms.Registration: form = forms.Registration()
  4. 當用戶使用POST METHOD送出資料時,要審查一下form裡面的資料正確性: form.is_valid()
  5. 審查email address有沒有重覆, 因為Django只會查核username是否唯一,所以我們要手動加入這項檢查, checkfault, error = form.clean_user(),要再到forms.py增加clean_user function
  6. 如全部資料正確,把用戶登入,回傳到首頁 可以參考Day4 - Custom Login Page and Logout如何把用戶登入
  7. 發送email至新用戶,修改及利用views.sendmail 簡單發送email 功能views.sendmail可以參考Day6 - Send Emails (python-decouple)
  8. 如有資料出錯,在註冊頁顯示error
/* 要留意紅字,等等我會需要到forms.py再作修改以配合使用。 */ 了解好了,就到views.py編寫新function - Register
from django.shortcuts import render
from django.contrib.auth.models import User
from django.contrib import auth
from .forms import Registration


def Register(request):
    if request.user.is_authenticated:
        return blogindex(request)

    if request.method == "POST":
        form  = Registration(request.POST)

        if form.is_valid():
            checkfault, error = form.clean_user()

            if checkfault == True:
                form = Registration()
                return render(request, 'account/registration.html', {'form':form, 'errorcode':error})

            else:
                user.form.save()
                username = request.POST.get('username')
                password = request.POST.get('password1')
                user = auth.authenticate(username=username, passowrd=password)
                auth.login(request, user)
                title = 'registration'
                return sendmail(request, title=title)

        else:
            form = Registration()
            error = 'There is something wrong about your information.'
            return render(request, 'account/registration.html', {'form':form, 'errorcode':error})

    else:
        form = Registration()
        return render(request, 'account/registration.html', {'form':form})

上面的views.Register 引用了兩個額外的functions:
  • forms.clean_user()
  • views.sendmail()
所以接下來要到forms.Registrationviews.sendmail 做少少修改。
4. [Create form method]forms.py裡面,找到我們一開始寫的class Registration。 在裡面,新增一個Form method - clean_user
from django.contrib.auth.models import User

class Registration(UserCreationForm):

    def clean_user(self):
        form_data = self.cleaned_data
        fault = True
        error = ''
        if User.objects.get(form_data['email']).count() > 0:
            error = 'Email Already Exits!'
            return fault, error
        else:
            fault = False
            return fault, error

這樣views.Register裡面的form是subclass,就可以使用superclass的功能clean_user()了。 當使用時,self.cleaned_data就是form.cleaned_data這意思, 作用是在form.is_valid()通過下,進一步肯定資料是合格,符合python套用。 /* 一般的填漏資料、字數錯誤等等,會在is_valid()下排除了 */ cleaned_data後,我們利用User.objects.get() .count() 就可以數到User models裡面,有多少個使用了一樣的email address。 當發現重複的email address時,它會回傳fault=True & error = 'Email Already Exits!'到step 4 的view.Registercheckfault, error
5. [Modify view for email] 當資料完全合格時,我們的views.Register會使用form.save()註冊及把用戶登入。 此外還會把一個叫title的parameter,傳到 sendmail function,作發送註冊確認email。 所以我們新建或修改views.pysendmail內容,變成這樣:
from django.template.loader import render_to_string
from django.core.mail import send_mail

def sendmail(request, title=title)
    recipient = request.user.email

    if title == 'registration':
        email_title = "Confirmation of Registration at XXX website"
        email_content = render_to_string('account/registration.txt', {'username':request.user.username})

    send_mail(
                    email_title,
                    ‘’,
                    '<yourmail>@gmail.com',
                    [recipient,],
                    html_message=email_content
    )

    return blogindex(request)

這樣就會發送email到註冊用戶了!。。。不過還沒有內容 上面使用了django.template.loader.render_to_string這個功能,它會把一個名為account/registration.txt的txt file內容加到parameter email_content。 所以要建立這個txt file,內容就是email內容: e.g.
{% autoescape off %}

Hello <span style='color: blue'>{{ username }}! </span>

Nice to meet you!
Welcome to our site and we want to <span style='color: red'>Thank You</span> for registering!

Cheers,
xxx Team

{% endautoescape %}

這裡用了{% autoescape off %} 是以防文件內容需要用到HTML/CSS,例如連結、字體等可以正常顯示。
6. [Templates] 還差甚麼? 就是一頁account/registration.html。 它的內容很簡單:
{% extends 'base.html' %}
{% block content %}

    <form method="POST">
    {% csrf_token %}
    {% for field in form %}
        <label for='username'>{{ field.label_tag }}</label>{{ field }}
        {% if {{ field.help_text }} %}
            {{ field.help_text }}
        {% endif %}
    {% endfor %}
    <input type='submit' value='Sign Up'>
    </form>

{% endblock %}

終於,Registration + email confirmation完成了。 /* 當然可以用HTML + CSS把這頁美化一下。 */


Day6 - Send Emails (python-decouple)

Permalink -

1. [ Email settings] 利用Django 自用send email 給用戶,例如註冊、重置密碼等,要先在settings.py設定系統的電郵。 到settings.py增設:

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
這裡是設定我們Django 的email後台功能,裡面的console一字是指在localhost發出, 通常我們不會這樣使用,除非是在terminal測試用。所以要把它改成:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_TLS = True
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_HOST_USER = '<username>@gmail.com'
EMAIL_HOST_PASSWORD = '<gmail password>'
上面是以GMAIL作例子,這樣Django就會知識要利用GMAIL渠道發送emails。 但當中實在有太多個人資料,我們可以用python-decouple來隔接填寫parameters。
2. [python-decouple] Python-decouple 是python的open source之一,詳情可以在這裡查看。 到terminal安裝:
pipenv install python-decouple
或是 (假如你沒有安裝pipenv virtual environment)
python -m pip install python-decouple
把step 1裡面的EMAIL BACKEND settings copy & paste到一個新文檔: .env 存放在manage.py的同一層。(即BASE_DIR) 接著把settings.py 裡面的內容改為:
from decouple import config

EMAIL_USE_TLS = config('EMAIL_USE_TLS', cast=bool)
EMAIL_HOST = config('EMAIL_HOST')
EMAIL_PORT = config('EMAIL_PORT', cast=int)
EMAIL_HOST_USER = config('EMAIL_HOST_USER')
EMAIL_HOST_PASSWORD = config('EMAIL_HOST_PASSWORD')
這就利用了python-decouple的config功能,當運行Django時,它會自動把.env裡面的parameters套上。 要留意這裡因為EMAIL_USE_TLS是boolean,所以要使用cast=bool, 同樣地EMAIL_PORT是數字,所以要加上cast=int **題外話** 其他parameters如: settings.py裡面的SECRET_KEY,也可以加到.env內。
3. [send email] 設定好上面的以後,到views.py試寫一個名叫sendmail的功能:
from django.core.mail import send_mail

def sendmail(request)
    send_mail(
        'Email Title',
        'Email Content',
        'xxx@gmail.com',
        ['recipient@xxx.com',],
    )
    return blogindex(request)
留意收件人是一個python list,所以要打成 ['xxx@xxx.com', 'yyy@yyy.com',] 即使只有一位收件人,那一個comma是不可少的。 從urls.pysetup這views function的urlspattern:
from <app>.views import sendmail

urlspattern=[
...
    url(r'^sendmail/$', sendmail, name='email'),
]
到web browser打開 "../sendmail/" ,點擊連結,Django把你置定的Email title, content, 由 (sender) 傳到 (recipient)了。 /* 假如你的GMAIL沒有正常發送email出去,請記得到google account,Allow less secure apps,disable 2-step-verification以及disable captcha */


Day5 - Custom Post Form (form.py)

Permalink -

1. [Django Form] 要在Django 建立Form作post threads用,可以新建form.py under app 。 例如app - blog的posts是在Django 預設的admin page才可以貼文, 我們可以把該model class重新定義一下。 在新增的blog/form.py內面建立表單:

from django import forms
from .models import Post
class PostForm(forms.ModelForm):

class Meta:
    model = Post
    fields = ('title', 'content', 'photo', 'photo_upload',)

class Meta裡面,我們表明哪個model需要制作表單, 而fields則說明表單有哪些細項。
2. [Set url] 接著我們又建立新 urlpatterns給post page用 e.g.settings/urls.py
urlpatterns=[
    ...,
    path(r'post_new/', post_new, name='post_new'),
]

還有到views.pydefine post_new這個function
from .forms import PostForm

def post_new(request):
    user = request.user
    if not user.is_authenticate or user.username != "admin":
        return app_index(request)

    if request.method == "POST":
        postform = PostForm(request.POST)
        if postform.is_valid:
            postform.save()
            return app_index(request)
    else: 
        return render(request, 'post_new.html', {'form':postform})

這裡我們驗證了帳目是admin才可以進入版面,而且回傳{'form':postform}post_new.html作表單用
3. [Create a new template] 建立templates/post_new.html。 利用Django 內建的{{form.as_p}}都可以順利完成,也可以自定介面: 以上面form.py定義的field包括有title, content, photo, photo_upload e.g.
<form method="POST">
{% csrf_token %}
{% for field in form %}
    <label for='{{ field.name }}'>{{ field.label_tag }}</label>
    {{ field }}
    {% if field.help_text %}
        {% field.help_text %}
    {% endif %}
{% endfor %}
</form>


Day4 - Custom Login Page and Logout

Permalink -

1. [Custom Login Page] Django有自己的built-in authentication 認證用戶: django.contrib.auth 。 而且有自己的built-in models (database)儲存users資料: django.contrib.auth.models.User。 全都在 django.contrib.auth 裡面,預設在INSTALLED APPS。


2. [Set url] 要建Login Page,先到urls.py:
from <app_name>.views import login
from django.urls import path

urlpatterns = [
...
    path(r’login/’, login, name=’login’)
]


3. [Set view function] 再到blog.views.py,新增function:
from django.contrib.auth import authenticate, login

def login(request):
 if request.user.is_authenticated:
  redirect(‘blog’)

 if request.method == ‘POST’: 
  username = request.POST.get(‘username’)
  password = request.POST.get(‘password’)
  user = authenticate(username=username, password=password)

  if user:
   login(request, user)
   loginstatus = True
   returnapp_index(request)
  else:
   loginstatus = False
   return render(request, ‘account/login.html’)

 else:
  loginstatus = None
  return render(request, ‘account/login.html’)

要留意is_authenticated()在Django2.0已改成沒有()
4. [create html template] 然後新增一頁login.html。e.g. ,templates/account/login.html 頁面內以POST 形式回傳input form輸入的資料:
<form method="POST">
{% csrf_token %}
<label for="username">Username:</label>
<input type="text" name="username">
<label for="password">Password:</label>
<input type="text" name="password">
<input type="submit" value="Login">
{% if loginstatus == False %}
 <span>Your username/password is wrong.</span>
{% endif %}


5. [Redirect to index] 還沒完,因為在Step 3,我們把Login完的動作改為
return app_index(request)

回傳到blog首頁,希望在成功登入後把menu bar 的Login字,改為Logout, 可以在base.html的menu位置增加if condition
{% if request.user.is_authenticated %}
    <a href="{% url 'logout' %}">Logout</a>
{% else %}
    <a href="{% url 'login' %}">Login</a>

這樣login後的連結會變成logout。
6. [Logout] Logout的做法跟login大致相同,而且更為單簡。 只需在views.py增加function:
from django.contrib.auth import logout

def logout(request):
 logout(request)
 return app_index(request)

然後在urls.py新增regex:
from <app_name>.views import logout

urlpatterns = [
 ...
 ...
 url(r'^logout/$', logout, name='logout'),
]

這次不用新增templates,因為在views.py return 的是app_index() 。


Day3 - Debug, Deploy to PythonAnyWhere

Permalink -

1. [Debug] settings.py裡面有一小項名為DEBUG。 顧名思義,就是在出錯時頁面會回傳error codes。 它是一個Boolean,最初都設為True,方便developers使用。 例如網址錯誤,它會顯示exceptions (error page),說明現有哪些regex是可以通過。 但當真正deploy於internet時,假如遇上這種情況,我們會比較希望頁面顯示成404 Page Not Found。 所以除非under development,它的值應該為False。 雖說是False,但管理人員還是可以收到error code。 同樣在settings.py,可以設立一個list:

ADMIN =[ ("<admin_name>", "email_address") ]

這樣即使在DEBUG = False 情況下,有關的admins還是會收到full exception的email。
2. [Deploy] 把django site upload至 :::PythonAnyWhere:::。 一個支援Python + Django 的平台: 先把整個project zipped:
>python -m zipfile -c <project_name>.zip <project_name>

連上:::PythonAnyWhere::: 到FILE分頁,把<project_name>.zip upload上去 到console頁面,開啟bash console
$unzip <project_name>.zip

如需要把project在pythonanywhere也打算virtual environment下運行,可以在同一console新增:
$virtualenv --python=python3.6 <project_name>_venv
$source <project_name>_venv/bin/activate
$python -m pip install pipenv
$pipenv install Django

到WEB分頁新增app,使用manual configuration,揀選python版本 同樣在WEB分頁,把virtual environment部分的路徑加入:
/home/<username>/.virtualenv/<project_name>_venv/

到code部分,把WSGI的內容 copy&paste 改為:
import os
import sys

path = '/home/<username>/<project_name>'
if path not in sys.path:
 sys.path.append(path)

os.environ['DJANGO_SETTINGS_MODULE'] = '<project_name>.settings'

from django.core.wsgi import get_wsgi_application
from django.contrib.staticfiles.handlers import StaticFilesHandler
application = StaticFilesHandler(get_wsgi_application())

最後在同樣WEB分頁點選reload,重新載入設定。
日後雖要修改網站時,同樣把project zipped,upload,用console unzip,reload。


Day2 - Staticfiles, Media, Dynamic URL, Reversed List

Permalink -

1. [STATIC_URL] 架設網站少不免用到其他scripts,例如:JS, jQuery, CSS 等。這些為網站添上風格的, 當然每一頁都用得到,但亦不會每一頁都重寫一遍,而是把編碼儲存於某些特定子folders內。 有別於以往使用 ../ 或 ./ 等路徑,Django有static這個功能。 假設要把編寫好的CSS檔案儲存: 確認settings.py - INSTALLED APPSdjango.contrib.staticfiles這一項預設apps。 然後在settings.py最下方,define static的路徑:

STATIC_ROOT = os.path.join(BASE_DIR, 'static')

/* tied to particular apps */

STATIC_URL = '/static/'

/* not tied to specific apps */

STATICFILES_DIRS = (                                                   
    os.path.join(BASE_DIR,'static'),
) 

要注意staticfiles_dirs 不可以跟static_root一樣。
2. [Load static into templates] 把CSS file 儲存於static/css/ 內。 當要在HTML中使用時,要先在HTML最頂加入 {% load static %} 接著在呼叫CSS的路徑時,改成: <a href=“{% static 'css/xxx.css' %}”> 最後在terminal命令collectstatic: python manage.py collectstatic
3. [MEDIA_URL] 假設在models.py當中有些data是用到upload, e.g. files, photos等,可以在models.py - class 裡面define: e.g.
class Post(models.Model):
    ...
    ...
    photo_upload = ImageField(null=True, blank=True) / FileField(null=True, blank=True)

null & blank =True to prevent exceptions without default values 因為要upload media, 需要在settings.py declare media路徑:
MEDIA_ROOT = os.path.join(BASE_DIR, “media”)
MEDIA_URL = “/media/“


4. [Setup url] 同時在urls.py 加入:
from django.conf import settings
from django.conf.urls.static import static

urlpatterns =[...] 
urlpatterns += static(settings.MEDIA_URL, documents_root=MEDIA_ROOT)


5. [Migrate new data and collectstatic] 因為這次 step3 + step4分別在models.py新增了data-type; 更改了MEDIA_URL; 所以要在terminal makemigrations, migrate, collectstatic
>python manage.py makemigrations
>python manage.py migrate
>python manage.py collectstatic


6. [Recalling uploaded photo] 假設需要在HTML引用uploaded media, 因為己經在urlpatterns defined, 所以HTML內的引用路徑為: <href=“{{blog.photo_upload.url}}">
7. [DynamicURL] 由於class的每一個element都有自己的primary key(pk), 所以可以利用pk呼叫各element的網址。 首先在urls.py - urlpatterns增加一行: /* name='postpage' 是為日後在HTML內部呼叫時用的,下面會再提及 */
url(r'blog/(int: key)/', postpage, name='postpage')

然後到apps/views.py新增function 'postpage':
def postpage(request, key):
    post = Post.objects.get(pk=key)
    return render(request, page.html, {'post': post})


8. [Using "name" to call a url] 假如要在HTML內部呼叫某一element的頁面,可以用step7 declared的url name: e.g. {% url 'name' key=post.pk %}
9. [Order_by] 因為使用python for loop列出所有posts時,會由Posts[0], Posts[1], Posts[2],..., Posts[n] 這個次序去完成, 所以最好的方法是用在views.pydeclare function時,把post_list 改成descending list: e.g.
from .models import Post
from django.shortcuts import render

def app_index(request):
    post_list = Post.objects.all().order_by('-time')
    return render(request, 'app_index.html', {'post_list': post_list})

The '-' sign means descending order, i.e. from newest to oldest "time" value.


Day1 - 由零開始架置Django

Permalink -

花了一個月的時間去學Python + NumPy + Pandas, 終於移師到Django! 記錄一下每天的進度+筆記。


1. [Install pipenv] 先安裝一個運行virtual environment的package- pipenv,再安裝Django
$python -m pip install pipenv
$pipenv install Django

由於每個 Project 的程式、版本、配置也可能各有不同,所以最好用 VirutalEnvironment 運行。
2. [Create Django Project] 在 Terminal 使用cd進入你所想的folder以建立project,輸入command建立project:
$django-admin.py startproject <project_name>
新project會被建立在Users/<username>/<project_name>這個新folder。
3. [Open your project using IDE] 使用 Drag & Drop 將 project top folder 加至 Sublime Text3 的side bar。
4. [Runserver] 利用 Terminal 啟動 Django project 。
$cd <project_name>
$python manage.py runserver


5. [Create an app to the project] 新增app至project: $python manage.py startapp <app_name> #新增子 folder <app_name> 在 project 裡面 打開project/settings.py加入新app:
INSTALLED_APPS = [
...
...
    "<app_name>",
]

7. [Setup url] 因為每當到某一網址時,Django都會利用 <project_name>/urls.py 發出request找相對應的views。 到<project_name>/urls.py
from <app_name>.views import app_index
from django.urls import path

urlspattern = [
    ...
    path('index/', app_index, name='app_index'),
]

這樣在browser打開url ../index/時,Django就會跳到<app_name>.views.app_index這個function。
8. [Create a view function] Define <app_name>.views.app_index: 到<app_name>/views.py
from django.shortcuts import render

def app_index(request):
    render(request, "app_index.html", {<dictionary of any elements to be passed to app_index.html>}) 

#dict 例如{time: datetime.now()}
9. [Define templates path] 利用terminal 新增directory templates,以作架置render blog link 的HTML用。 在project目錄下: mkdir templates 設定 templates folder 的路徑: <project_name>/settings.py
TEMPLATES -  'DIRS': [os.path.join(BASE_DIR, 'templates').replace('\\', '/')]

然後可以在 SublimeText3 內安心編寫HTML #注意,所有elements在HTML內要用 雙括弧 {{ elements }},才可以回傳至render的dictionary。
10. [Choosing database] Django 預設了幾種 database 制式:SQlite3, MySQL, PostgreSQL,這裡使用SQLite3。 或可以在project/settings.py - DATABASE 內更換:
MySQL: django.db.backends.mysql
SQLite 3: django.db.backends.sqlite3
PostgreSQL: django.db.backends.postgresql_psycopg2


11. [Create a new model to database] 假設app需要用到名為Post的Database,我們需要加入新class - Post: <app_name>/models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=100)
    content = models.TextField
    photo = models.URLField(blank=True)
    time = DataTimeField(auto_now_add=True) 

每當models有修改時,都要makemigrations & migrate:
$python manage.py makemigrations
$python manage.py migrate


12. [Create Superuser and setup Admin session] Admin 是 Django 預設的 INSTALLED APPS 之一,可以在settings內找到。 利用新增admin或superuser:
$python manage.py createsuperuser
然後輸入以下的資料: Username (leave blank to use 'YOUR_NAME'): Email address: your_name@yourmail.com Password: Password (again): Superuser created successfully. 大功告成 告訴 Django,blog的models (即Post) 需要被 admin 管理: 到<app_name>/admin.py
from django.contrib import admin

admin.site.register(Post)

登登!可以到 http://127.0.0.1:8000/admin 管理了。 -->To Be Continued