Agile育成ブログ
未来を変える喜びを
Django

予約サイトシステム


Warning: count(): Parameter must be an array or an object that implements Countable in /home/xs638785/agile-software.site/public_html/wp-content/plugins/rich-table-of-content/functions.php on line 490

教材一覧

models.pyの作成
class Work(models.Model):
    title = models.CharField('タイトル', max_length=100)
    image = models.ImageField(upload_to='images', verbose_name='イメージ画像')
    thumbnail = models.ImageField(upload_to='images', verbose_name='サムネイル', null=True, blank=True)
    skill = models.CharField('スキル', max_length=100)
    url = models.CharField('URL', max_length=100, null=True, blank=True)
    created = models.DateField('作成日時')
    description = models.TextField('説明')

    def __str__(self):
        return self.title

モデル

カスタムのユーザーとパーミッション

Django のパーミッションフレームワークをカスタムのユーザーモデルに簡単に取り入れられるように用意されているのが、Django の PermissionsMixin です。これはユーザーモデルの階層に取り入れることができる抽象モデルで、Django のパーミッションモデルをサポートするのに必要なすべてのメソッドとデーターベースのフィールドを使えるようにしてくれます。

PermissionsMixin は、以下のメソッドと属性を提供します。class models.PermissionsMixinis_superuser

真偽値です。明示的に与えられない場合でも、ユーザーがが全てのパーミッションを持っているかどうかを示します。get_user_permissions(obj=None)New in Django 3.0.

Returns a set of permission strings that the user has directly.

If obj is passed in, only returns the user permissions for this specific object.get_group_permissions(obj=None)

ユーザがグループを通して持つパーミッションの文字列のセットを返します。

obj が渡されたとき、指定されたオブジェクトに対するグループパーミッションのみを返します。get_all_permissions(obj=None)

ユーザがグループおよびユーザパーミッションを通して持つパーミッションの文字列のセットを返します。

obj が渡された場合、指定されたオブジェクトに対するパーミッションのみを返します。has_perm(permobj=None)

ユーザーが指定したパーミッションを持っている場合、True を返します。ここで、perm は "<app label>.<permission codename>" という形式で指定します (permissions を参照)。もし、User.is_active と is_superuser が両方とも True だった場合、このメソッドは常に True を返します。

obj が渡された場合、このメソッドはモデルに対するパーミッションのチェックを行わず、指定されたオブジェクトに対して行います。has_perms(perm_listobj=None)

ユーザーが指定したパーミッションを持っている場合、True を返します。ここで、perm は "<app label>.<permission codename>" という形式で指定します。もし、User.is_active と is_superuser が両方とも True だった場合、このメソッドは常に True を返します。

obj が渡された場合、このメソッドは指定されたオブジェクトに対してパーミッションのチェックを行い、モデルに対しては行いません。has_module_perms(package_name)

Returns True if the user has any permissions in the given package (the Django app label). If User.is_active and is_superuser are both True, this method always returns True.

class CustomUser(AbstractBaseUser, PermissionsMixin):
    email = models.EmailField('メールアドレス', unique=True)
    first_name = models.CharField(('姓'), max_length=30)
    last_name = models.CharField(('名'), max_length=30)
    description = models.TextField('自己紹介', default="", blank=True)
    image = models.ImageField(upload_to='images', verbose_name='プロフィール画像', null=True, blank=True)

view.py

allauth.accountからインポートしたviewsのLoginViewを引数

class LoginView(views.LoginView):
    template_name = 'accounts/login.html'
from django.shortcuts import render, redirect
from django.views import View
from accounts.models import CustomUser
from accounts.forms import ProfileForm,SignupUserForm
from allauth.account import views
from django.contrib.auth.mixins import LoginRequiredMixin

# Create your views here.
class ProfileView(LoginRequiredMixin,View):
    def get(self, request, *args, **kwargs):
        user_data = CustomUser.objects.get(id=request.user.id)

        return render(request, 'accounts/profile.html', {
            'user_data': user_data,
        })

class ProfileEditView(LoginRequiredMixin,View):
    def get(self, request, *args, **kwargs):
        user_data = CustomUser.objects.get(id=request.user.id)
        form = ProfileForm(
            request.POST or None,
            initial={
                'first_name': user_data.first_name,
                'last_name': user_data.last_name,
                'department': user_data.department,
                'description': user_data.description,
                'image': user_data.image,
            }
        )

        return render(request, 'accounts/profile_edit.html', {
            'form': form,
            'user_data': user_data
        })

    def post(self, request, *args, **kwargs):
        form = ProfileForm(request.POST or None)
        if form.is_valid():
            user_data = CustomUser.objects.get(id=request.user.id)
            user_data.first_name = form.cleaned_data['first_name']
            user_data.last_name = form.cleaned_data['last_name']
            user_data.description = form.cleaned_data['description']
            if request.FILES.get('image'):
                user_data.image = request.FILES.get('image')
            user_data.save()
            return redirect('profile')

        return render(request, 'accounts/profile.html', {
            'form': form
        })

class LoginView(views.LoginView):
    template_name = 'accounts/login.html'

class LogoutView(views.LogoutView):
    template_name = 'accounts/logout.html'

    def post(self, *args, **kwargs):
        if self.request.user.is_authenticated:
            self.logout()
        return redirect('/')

class SignupView(views.SignupView):
    template_name = 'accounts/signup.html'
    form_class = SignupUserForm

html

profile.html

<tr>
    <th class="header">プロフィール画像</th>
    <td class="data">
        {% if user_data.image %}
            <img src='/{{ user_data.image.url }}' width=100px>
        {% endif %}
    </td>
</tr>
<tr>
    <th class="header">名前</th>
    <td class="data">{{ user_data.first_name }} {{ user_data.last_name }}</td>
</tr>
<tr>
    <th class="header">メールアドレス</th>
    <td class="data">{{ user_data.email }}</td>
</tr>
<tr>
    <th class="header">自己紹介</th>
    <td class="data">{{ user_data.description }}</td>
</tr>

profile_edit.html

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    <table class="profile_table mb-4">
        <tbody>
            <tr>
                <th class="header">プロフィール画像</th>
                <td class="data">
                    {{ form.image }}
                </td>
            </tr>
            <tr>
                <th class="header">名前</th>
                <td class="data form_wrap form_wrap-2">
                    {% render_field form.first_name class="form-control" placeholder="姓" %}
                    {% render_field form.last_name class="form-control" placeholder="名" %}
                </td>
            </tr>
            <tr>
                <th class="header">自己紹介</th>
                <td class="data">
                    {% render_field form.description class="form-control" placeholder="自己紹介" %}
                </td>
            </tr>
        </tbody>
    </table>

予約モデル

from django.utils import timezone


class Booking(models.Model):
    staff = models.ForeignKey(Staff, verbose_name='スタッフ', on_delete=models.CASCADE)
    first_name = models.CharField('姓', max_length=100, null=True, blank=True)
    last_name = models.CharField('名', max_length=100, null=True, blank=True)
    tel = models.CharField('電話番号', max_length=100, null=True, blank=True)
    remarks = models.TextField('備考', default="", blank=True)
    start = models.DateTimeField('開始時間', default=timezone.now)
    end = models.DateTimeField('終了時間', default=timezone.now)

    def __str__(self):
        start = timezone.localtime(self.start).strftime('%Y/%m/%d %H:%M')
        end = timezone.localtime(self.end).strftime('%Y/%m/%d %H:%M')
        return f'{self.first_name}{self.last_name} {start} ~ {end} {self.staff}'

スタッフリストの作成

style.css

base.html
body {
    background: #F1F1F1;
    display: flex;
    flex-flow: column;
    min-height: 100vh;
}

main {
    flex: 1;
}
store
.storelist img {
    height: 150px;
    object-fit: contain;
}
* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    background: #F1F1F1;
    display: flex;
    flex-flow: column;
    min-height: 150vh;
}

main {
    flex: 1;
}

.card-profile {
    width: 700px;
    border: 0;
    border-radius: 1rem;
    box-shadow: 0 0.2rem 0.2rem 0 rgba(0, 0, 0, 0.1);
}

.button {
    width: 150px;
}

.btn {
    font-size: 80%;
    border-radius: 5rem;
    font-weight: bold;
}

.profile_table {
    width: 100%;
    border: 1px solid #D1DBEB;
    border-radius: 8px;
    border-collapse: separate;
    overflow: hidden;
}

.profile_table .header {
    width: 200px;
    padding: 16px 24px;
    text-align: left;
    background: #F1F5FA;
}

.profile_table .data {
    padding: 16px 24px;
}

.form_wrap {
    display: grid;
    gap: 16px;
}

.form_wrap-2 {
    grid-template-columns: repeat(2, 1fr);
}

.form-control:focus {
    box-shadow: none;
}

.card-auth {
    width: 400px;
    border: 0;
    border-radius: 1rem;
    box-shadow: 0 0.2rem 0.2rem 0 rgba(0, 0, 0, 0.1);
}

.card-title {
    margin-bottom: 2rem;
    font-size: 1.5rem;
}

.card-body {
    padding: 2rem;
}

.form-auth {
    width: 100%;
}

.form-label-group {
    margin-bottom: 1rem;
}

.form-label-group input {
    border-radius: 2rem;
}

.top img {
    object-fit: cover;
    height: 600px;
}

.overlay {
    position: absolute;
}

.title {
    font-size: 4rem;
}

.subtitle {
    font-size: 2rem;
}

.navbar-nav {
    flex-direction: row!important;
}

.nav-color {
    color: black;
}

.nav-color:hover {
    color: #EE6C4D;
}

.nav-color:after {
    content: "";
    display: block;
    height: 2px;
    background: #EE6C4D;
    margin-top: 6px;
    opacity: 0;
    transform: translateY(12px);
    transition: all 0.3s ease-in-out;
}

.nav-color:hover:after {
    transform: translateY(0px);
    opacity: 1;
}

/* store */

.storelist img {
    height: 150px;
    object-fit: contain;
}
/* index */
#home {
    background-image: url(../../../media/images/フリーランスの宿.png);
    min-height: 100vh;
}

/* contact */

#image {
    background-image: url(../../media/images/お問い合わせ.png);
    min-height: 150vh;
}
.contact {

    max-width: 500px;
}

.form-control:focus {
    border-color: #EE6C4D;
    box-shadow: none;
}

.card {
    border: none;
}

/* 店舗情報・地図 */
#location {
    padding: 4% 0;
}
#location .wrapper {
    display: flex;
    justify-content: space-between;
}
.location-info {
    width: 22%;
}
.location-info p {
    padding: 12px 10px;
}
.location-map {
    width: 74%;
}

/* SNS */
#sns {
    background: #FAF7F0;
    padding: 4% 0;
}
#sns .wrapper {
    display: flex;
    justify-content: space-between;
}
#sns .sub-title {
    margin-bottom: 30px;
}
.sns-box {
    width: 30%;
}

お問い合わせ作成

ターミナル上でメール送信の確認ができます。

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'

URL作成

path('', views.ContactView.as_view(), name='store'),

form.py作成

EmailFieldを使います。

from django import forms


class ContactForm(forms.Form):
    name = forms.CharField(max_length=30, label='名前')
    email = forms.EmailField(max_length=30, label='メールアドレス')
    message = forms.CharField(label='メッセージ', widget=forms.Textarea())

views.py

.formsからContactFormをインポートします。
django.core.mailからBadHeaderError, EmailMessageをインポートします。
import textwrapをインポートします。


getとpostに関しては下記記事に詳しく書いています。

https://www.agile-software.site/2021/04/24/django-render%e3%83%a1%e3%82%bd%e3%83%83%e3%83%89/

先頭の空白を削除するためにtextwrap.deden関数を使用します。

contact.html

 {% csrf_token %}
render_field

地図を表示

  1. Googleマップで表示したい場所の住所を入力
  2. 共有ボタンをクリックし、地図を埋め込むをクリック
  3. カスタムサイズを「800×400」のサイズに設定し、HTMLをコピー

SNSレイアウト

Facebook

個人アカウントのページは使うことができません。Facebookページ(ビジネスアカウント)を作成しましょう。

Twitter

You cannot copy content of this page