diff --git a/accounts/__init__.py b/accounts/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/admin.py b/accounts/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/accounts/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/accounts/apps.py b/accounts/apps.py
new file mode 100644
index 0000000..9b3fc5a
--- /dev/null
+++ b/accounts/apps.py
@@ -0,0 +1,5 @@
+from django.apps import AppConfig
+
+
+class AccountsConfig(AppConfig):
+ name = 'accounts'
diff --git a/accounts/forms.py b/accounts/forms.py
new file mode 100644
index 0000000..cabaed6
--- /dev/null
+++ b/accounts/forms.py
@@ -0,0 +1,43 @@
+from django import forms
+from .models import CustomUser
+from django.contrib.auth import get_user_model
+
+User = get_user_model()
+
+
+class SignUpForm(forms.ModelForm):
+ password1 = forms.CharField(label='Password', widget=forms.PasswordInput)
+ password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput)
+
+ class Meta:
+ model = User
+ fields = ['name','email','phone']
+
+ def clean_password2(self):
+ password1 = self.cleaned_data.get("password1")
+ password2 = self.cleaned_data.get("password2")
+ if password1 and password2 and password1 != password2:
+ raise forms.ValidationError("Passwords don't match")
+ if len(password1) < 8:
+ raise forms.ValidationError('It must be 8 character or more')
+ return password2
+
+ def save(self, commit=True):
+ user = super().save(commit=False)
+ user.set_password(self.cleaned_data["password1"])
+ if commit:
+ user.save()
+ return user
+
+
+class CreateStaffForm(forms.ModelForm):
+
+ class Meta:
+ model = User
+ fields = ['name','email','phone','is_active']
+
+class UpdateStaffForm(forms.ModelForm):
+
+ class Meta:
+ model = User
+ fields = ['name','email','phone','is_active']
\ No newline at end of file
diff --git a/accounts/managers.py b/accounts/managers.py
new file mode 100644
index 0000000..1b7e934
--- /dev/null
+++ b/accounts/managers.py
@@ -0,0 +1,30 @@
+from django.db import models
+from django.contrib.auth.models import BaseUserManager
+
+
+class CustomUserManager(BaseUserManager):
+ def create_user(self, email, name, password=None):
+
+ if not email:
+ raise ValueError('Staff must have an email address')
+
+ user = self.model(
+ email=self.normalize_email(email),
+ name=name,
+ )
+
+ user.set_password(password)
+ user.save(using=self._db)
+ return user
+
+ def create_superuser(self, email, name, phone, password=None):
+
+ user = self.create_user(
+ email,
+ password=password,
+ name=name,
+ )
+ user.is_active = True
+ user.is_superuser = True
+ user.save(using=self._db)
+ return user
\ No newline at end of file
diff --git a/accounts/migrations/0001_initial.py b/accounts/migrations/0001_initial.py
new file mode 100644
index 0000000..f142c81
--- /dev/null
+++ b/accounts/migrations/0001_initial.py
@@ -0,0 +1,44 @@
+# Generated by Django 3.0.7 on 2020-10-13 07:44
+
+import accounts.models
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='CustomUser',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('password', models.CharField(max_length=128, verbose_name='password')),
+ ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
+ ('name', models.CharField(max_length=100, verbose_name='Full Name')),
+ ('email', models.EmailField(max_length=100, unique=True, verbose_name='Email Address')),
+ ('phone', models.CharField(max_length=50, unique=True, verbose_name='Phone Number')),
+ ('is_superuser', models.BooleanField(default=False)),
+ ('is_admin', models.BooleanField(default=False)),
+ ('is_manager', models.BooleanField(default=True)),
+ ('is_staff', models.BooleanField(default=False)),
+ ('is_active', models.BooleanField(default=False)),
+ ('created_by', models.CharField(blank=True, max_length=100, null=True)),
+ ],
+ options={
+ 'abstract': False,
+ },
+ ),
+ migrations.CreateModel(
+ name='Profile',
+ fields=[
+ ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, serialize=False, to=settings.AUTH_USER_MODEL)),
+ ('profile_pic', models.ImageField(default='profile_pics/user.svg', upload_to=accounts.models.profile_pic_filename, verbose_name='Profile Picture')),
+ ],
+ ),
+ ]
diff --git a/accounts/migrations/__init__.py b/accounts/migrations/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/accounts/models.py b/accounts/models.py
new file mode 100644
index 0000000..4309908
--- /dev/null
+++ b/accounts/models.py
@@ -0,0 +1,61 @@
+from uuid import uuid4
+from django.db import models
+from django.contrib.auth.models import AbstractBaseUser
+
+
+from .managers import CustomUserManager
+
+
+class CustomUser(AbstractBaseUser):
+ name = models.CharField(verbose_name='Full Name', max_length=100)
+ email = models.EmailField(verbose_name='Email Address', unique=True, max_length=100)
+ phone = models.CharField(max_length=50, unique=True, verbose_name='Phone Number')
+
+ is_superuser = models.BooleanField(default=False)
+ is_admin = models.BooleanField(default=False)
+
+ is_manager = models.BooleanField(default=True)
+ is_staff = models.BooleanField(default=False)
+
+ is_active = models.BooleanField(default=False)
+ created_by = models.CharField(max_length=100, blank=True, null=True)
+
+ objects = CustomUserManager()
+
+ USERNAME_FIELD = 'email'
+ REQUIRED_FIELDS = ['name', 'phone']
+
+
+ def has_perms(self, perm, obj=None):
+
+ if self.is_superuser or self.is_admin or self.is_manager:
+ return True
+
+ def has_module_perms(self, app_label):
+
+ if self.is_superuser or self.is_admin or self.is_manager:
+ return True
+
+ def __str__(self):
+ return self.email
+
+
+# Profile Picture
+def profile_pic_filename(instance, filename):
+ ext = filename.split('.')[1]
+ new_filename = f'{uuid4()}.{ext}'
+ return f'profile_pics/{new_filename}'
+
+
+class Profile(models.Model):
+ user = models.OneToOneField(CustomUser, primary_key=True, on_delete=models.CASCADE)
+ profile_pic = models.ImageField(verbose_name='Profile Picture', default='profile_pics/user.svg', upload_to=profile_pic_filename)
+
+ def get_absolute_url(self):
+ return reverse('accounts:profile', kwargs={'pk': self.user_id})
+
+ def get_profile_update_url(self):
+ return reverse('accounts:profile-update', kwargs={'pk': self.user_id})
+
+ def __str__(self):
+ return f'{self.user.name} Profile'
\ No newline at end of file
diff --git a/accounts/templates/accounts/account_activation_email.html b/accounts/templates/accounts/account_activation_email.html
new file mode 100644
index 0000000..1cb6b75
--- /dev/null
+++ b/accounts/templates/accounts/account_activation_email.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+ Registration Confirmation - Fagrimacs
+
+
+
+
+ Welcome to Fagrimacs
+
+ Please click link below to confirm your email and complete registration.
+
+
+ {{ confirm_url }}
+
+
+
diff --git a/accounts/templates/accounts/create_staff.html b/accounts/templates/accounts/create_staff.html
new file mode 100644
index 0000000..77c1de4
--- /dev/null
+++ b/accounts/templates/accounts/create_staff.html
@@ -0,0 +1,72 @@
+{% extends 'dashboard/base.html' %}
+{% block content %}
+
+
+ {% include 'messages.html' %}
+
+
+
+
+
+
+
+ {% if staff|length > 0 %}
+
+
+ | # | Name | Phone | Email |
+
+
+ {% for person in staff %}
+
+ | {{ person.num }}. |
+
+ {{ person.name }}
+ |
+ {{ person.phone_number }} |
+ {{ person.email }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
No staff have been added yet
+ {% endif %}
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/customuser_confirm_delete.html b/accounts/templates/accounts/customuser_confirm_delete.html
new file mode 100644
index 0000000..d912bd8
--- /dev/null
+++ b/accounts/templates/accounts/customuser_confirm_delete.html
@@ -0,0 +1,59 @@
+{% extends 'base.html' %}
+{% block content %}
+
+
+ {% include 'messages.html' %}
+
+
+
+
+
+
Proceed to delete {{ customuser.name }} ?
+
+
+
+
+
+
+
+
+
+
+ | Name | {{ customuser.name }} |
+ | Phone | {{ customuser.phone_number }} |
+ | Email | {{ customuser.email }} |
+ | Created | {{ customuser.date_joined|date:'d-m-Y H:i' }} |
+
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/edit_staff.html b/accounts/templates/accounts/edit_staff.html
new file mode 100644
index 0000000..9d0e60a
--- /dev/null
+++ b/accounts/templates/accounts/edit_staff.html
@@ -0,0 +1,87 @@
+{% extends 'dashboard/base.html' %}
+{% block title %}Update Staff{% endblock %}
+{% block content %}
+
+
+ {% include 'messages.html' %}
+
+
+
+
+
+
+
+
+ | Name | {{ person.name }} |
+ | Phone | {{ person.phone_number }} |
+ | Email | {{ person.email }} |
+ | Created | {{ person.date_joined|date:'d-m-Y H:i' }} |
+ | Branch | {{ person.branch }} |
+
+
+ {% if request.user.is_superuser %}
+
+ Set Permissions
+
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/home.html b/accounts/templates/accounts/home.html
new file mode 100644
index 0000000..6bd4474
--- /dev/null
+++ b/accounts/templates/accounts/home.html
@@ -0,0 +1,52 @@
+{% extends 'dashboard/base.html' %}
+{% block title %}Staff{% endblock title %}
+
+{% block content %}
+
+
+ {% include 'messages.html' %}
+
+
+
+ {% if staff|length > 0 %}
+
+
+
+ | # | Name | Phone | Email | Branch |
+
+
+ {% for person in staff %}
+
+ | {{ person.num }}. |
+
+ {{ person.name }}
+ |
+ {{ person.phone_number }} |
+ {{ person.email }} |
+ {{ person.branch }} |
+
+ {% endfor %}
+
+
+ {% else %}
+
No staff have been added yet
+ {% endif %}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/login.html b/accounts/templates/accounts/login.html
new file mode 100644
index 0000000..a0edb8c
--- /dev/null
+++ b/accounts/templates/accounts/login.html
@@ -0,0 +1,22 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/templates/accounts/registration_complete.html b/accounts/templates/accounts/registration_complete.html
new file mode 100644
index 0000000..5b4bd61
--- /dev/null
+++ b/accounts/templates/accounts/registration_complete.html
@@ -0,0 +1,13 @@
+{% extends 'base.html' %}
+
+
+{% block content %}
+
+
+
+
Registration Complete.
+
You can login now
+
Login
+
+
+{% endblock %}
diff --git a/accounts/templates/accounts/registration_pending.html b/accounts/templates/accounts/registration_pending.html
new file mode 100644
index 0000000..5df7bd7
--- /dev/null
+++ b/accounts/templates/accounts/registration_pending.html
@@ -0,0 +1,23 @@
+{% extends 'base.html' %}
+
+{% load crispy_forms_tags %}
+
+
+{% block content %}
+
+
+
+
+ Thank you for registering your account in Fagrimacs.
+
+
+
+ {{ message }}
+
+
+
+
+{% endblock %}
diff --git a/accounts/templates/accounts/signup.html b/accounts/templates/accounts/signup.html
new file mode 100644
index 0000000..43afbd4
--- /dev/null
+++ b/accounts/templates/accounts/signup.html
@@ -0,0 +1,17 @@
+{% extends 'base.html' %}
+{% load crispy_forms_tags %}
+
+{% block content %}
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/accounts/tests.py b/accounts/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/accounts/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/accounts/tokens.py b/accounts/tokens.py
new file mode 100644
index 0000000..4944c2b
--- /dev/null
+++ b/accounts/tokens.py
@@ -0,0 +1,13 @@
+import six
+from django.contrib.auth.tokens import PasswordResetTokenGenerator
+
+
+class AccountActivationTokenGenerator(PasswordResetTokenGenerator):
+ def _make_hash_value(self, user, timestamp):
+ user_id = six.text_type(user.pk)
+ ts = six.text_type(timestamp)
+ is_active = six.text_type(user.is_active)
+ return f'{user_id}{ts}{is_active}'
+
+
+account_activation_token = AccountActivationTokenGenerator()
\ No newline at end of file
diff --git a/accounts/urls.py b/accounts/urls.py
new file mode 100644
index 0000000..41cafd6
--- /dev/null
+++ b/accounts/urls.py
@@ -0,0 +1,19 @@
+from django.urls import path
+
+from .import views
+
+app_name = 'accounts'
+
+
+urlpatterns = [
+ path('signup/', views.signup, name='signup'),
+ path('login/', views.Login.as_view(), name='login'),
+ path('logout/', views.Logout.as_view(), name='logout'),
+ path('confirm-email///',
+ views.ConfirmRegistrationView.as_view(), name='confirm-email'),
+ path('create-staff/', views.CreateStaff.as_view(), name='create-staff'),
+ path('update-staff//', views.UpdateStaff.as_view(), name='update-staff'),
+ path('update-password//', views.UpdatePassword.as_view(), name='update-password'),
+ path('delete-staff//', views.DeleteStaff.as_view(), name='delete-staff'),
+
+]
\ No newline at end of file
diff --git a/accounts/views.py b/accounts/views.py
new file mode 100644
index 0000000..d0c3d6c
--- /dev/null
+++ b/accounts/views.py
@@ -0,0 +1,240 @@
+from django.conf import settings
+from django.shortcuts import render, redirect, get_object_or_404, reverse
+from django.core.mail import EmailMessage
+from django.urls import reverse_lazy
+from django.template.loader import get_template
+from django.utils.encoding import force_bytes, force_text
+from django.utils.http import urlsafe_base64_decode, urlsafe_base64_encode
+from django.views.generic import TemplateView, FormView, DeleteView
+from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
+from django.contrib.auth.views import LoginView
+from django.contrib.messages.views import SuccessMessageMixin
+from django.contrib.auth import authenticate, login, logout
+#from .utils import get_all_perms, selected_perms
+from django.contrib import messages
+from django.contrib.auth.mixins import PermissionRequiredMixin
+from django.contrib.auth.models import Permission
+from django.contrib.auth import get_user_model
+
+from .tokens import account_activation_token
+from .forms import SignUpForm, CreateStaffForm, UpdateStaffForm
+from .models import Profile
+
+
+User = get_user_model()
+
+BASE_URL = 'http://127.0.0.1:8000'
+
+
+class Login(LoginView):
+ template_name = 'accounts/login.html'
+
+ def get_success_url(self):
+ url = self.get_redirect_url()
+ if url:
+ return url
+ elif self.request.user.is_admin:
+ return reverse('/')
+ elif self.request.user.is_superuser:
+ return f'/admin/'
+ else:
+ return reverse('dashboard')
+
+
+def signup(request):
+ form = SignUpForm(request.POST or None)
+ if form.is_valid():
+ user = form.save()
+ user_email = form.cleaned_data['email']
+ user.save()
+
+ #create profile
+ profile = Profile(user=user)
+ profile.save()
+
+ # send confirmation email
+ token = account_activation_token.make_token(user)
+ user_id = urlsafe_base64_encode(force_bytes(user.id))
+ url = BASE_URL + reverse('accounts:confirm-email',
+ kwargs={'user_id': user_id, 'token': token})
+ message = get_template(
+ 'accounts/account_activation_email.html').render(
+ {'confirm_url': url})
+ mail = EmailMessage(
+ 'Account Confirmation',
+ message,
+ to=[user_email],
+ from_email=settings.EMAIL_HOST_USER)
+ mail.content_subtype = 'html'
+ mail.send()
+
+ return render(request, 'accounts/registration_pending.html',
+ {'message': (
+ 'A confirmation email has been sent to your email'
+ '. Please confirm to finish registration.')}
+ )
+ return render(request, 'accounts/signup.html', {
+ 'form': form,
+ })
+
+
+
+class ConfirmRegistrationView(TemplateView):
+
+ def get(self, request, user_id, token):
+ user_id = force_text(urlsafe_base64_decode(user_id))
+
+ user = User.objects.get(pk=user_id)
+
+ context = {
+ 'message': 'Registration confirmation error. Please click the reset password to generate a new confirmation email.'
+ }
+
+ if user and account_activation_token.check_token(user, token):
+ user.is_active = True
+ user.save()
+ context['message'] = 'Registration complete. Please login'
+
+ return render(request, 'accounts/registration_complete.html', context)
+
+
+class CreateStaff(FormView):
+ template_name = 'accounts/create_staff.html'
+ form_class = CreateStaffForm
+
+ def get(self, request, *args, **kwargs):
+
+ staffs = User.objects.filter(is_staff=True, created_by=self.request.user).order_by('-pk')[:10]
+
+ context = {
+ 'form': self.form_class,
+ 'staffs': staffs
+ }
+
+ return render(request, self.template_name, context=context)
+
+ def post(self, request, *args, **kwargs):
+
+ form = self.form_class(data=request.POST)
+
+ if form.is_valid():
+
+ staff_obj = form.save(commit=False)
+ staff_obj.is_staff = True
+ staff_obj.is_manager = False
+ staff_obj.created_by = self.request.user
+
+ # Set default password to phone_number
+ staff_obj.set_password(
+ raw_password=form.cleaned_data['phone']
+ )
+
+ staff_obj.save()
+
+
+ messages.success(request, 'Success, staff created', extra_tags='alert alert-success')
+
+ return redirect(to='accounts:home')
+
+ staffs = User.objects.filter(is_staff=True, created_by=self.request.user).order_by('-pk')[:10]
+
+ context = {
+ 'form': form,
+ 'staffs': staffs
+ }
+
+ messages.error(request, 'Errors occurred', extra_tags='alert alert-danger')
+
+ return render(request, self.template_name, context=context)
+
+
+
+class UpdateStaff(FormView):
+ template_name = 'accounts/edit_staff.html'
+ form_class = UpdateStaffForm
+ password_form = PasswordChangeForm
+
+ def post(self, request, *args, **kwargs):
+
+ person = get_object_or_404(User, pk=self.kwargs['pk'])
+
+ form = self.form_class(instance=person, data=request.POST)
+
+ if form.is_valid():
+
+ form.save()
+
+ messages.success(request, 'Success, staff details updated', extra_tags='alert alert-success')
+
+ return redirect(to='accounts:update-staff', pk=self.kwargs['pk'])
+ else:
+
+ context = {
+ 'form': self.form_class(data=request.POST, instance=person),
+ 'person': person,
+ 'password_form': self.password_form
+ }
+
+ messages.error(request, 'Failed, errors occurred.', extra_tags='alert alert-danger')
+
+ return render(request, self.template_name, context=context)
+
+ def get(self, request, *args, **kwargs):
+
+ person = get_object_or_404(User, pk=self.kwargs['pk'])
+
+ password_form = self.password_form(user=person)
+
+ password_form.fields['old_password'].widget.attrs.pop("autofocus", None)
+
+ context = {
+ 'form': self.form_class(instance=person),
+ 'person': person,
+ 'password_form': password_form
+ }
+
+ return render(request, self.template_name, context=context)
+
+
+
+class DeleteStaff(DeleteView):
+
+ model = User
+
+ def get_success_url(self):
+
+ messages.success(self.request, 'Success, staff deleted', extra_tags='alert alert-info')
+
+ return reverse_lazy('accounts:home')
+
+
+class UpdatePassword(FormView):
+ form_class = PasswordChangeForm
+
+ def post(self, request, *args, **kwargs):
+
+ staff = get_object_or_404(User, pk=self.kwargs['pk'])
+
+ form = self.form_class(user=staff, data=request.POST)
+
+ if form.is_valid():
+
+ form.save()
+
+ messages.success(request, 'Success, password updated', extra_tags='alert alert-success')
+ else:
+ messages.error(request, 'Failed, password NOT updated', extra_tags='alert alert-danger')
+
+ return redirect(to='accounts:update-staff', pk=self.kwargs['pk'])
+
+
+class Logout(FormView):
+ form_class = AuthenticationForm
+ template_name = 'accounts/login.html'
+
+ def get(self, request, *args, **kwargs):
+
+ logout(request)
+
+ return redirect(to='accounts:login')
+
diff --git a/config/settings.py b/config/settings.py
index 24c896b..6365026 100644
--- a/config/settings.py
+++ b/config/settings.py
@@ -31,12 +31,16 @@
# Application definition
INSTALLED_APPS = [
+ 'accounts',
+
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
+
+ 'crispy_forms',
]
MIDDLEWARE = [
@@ -54,7 +58,7 @@
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [],
+ 'DIRS': [os.path.join(BASE_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -114,7 +118,24 @@
USE_TZ = True
+#Email Configurations For Development
+EMAIL_HOST_USER = 'noreply@example.com'
+EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
+EMAIL_FILE_PATH = os.path.join(BASE_DIR, 'sent_mails')
+
+
+AUTH_USER_MODEL = 'accounts.CustomUser'
+
+CRISPY_TEMPLATE_PACK = 'bootstrap4'
+
+LOGOUT_REDIRECT_URL = 'accounts:login'
+
+
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/
STATIC_URL = '/static/'
+MEDIA_URL = '/media/'
+
+STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static'),]
+MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
diff --git a/config/urls.py b/config/urls.py
index 082579f..fcde83e 100644
--- a/config/urls.py
+++ b/config/urls.py
@@ -1,21 +1,9 @@
-"""config URL Configuration
-
-The `urlpatterns` list routes URLs to views. For more information please see:
- https://docs.djangoproject.com/en/3.0/topics/http/urls/
-Examples:
-Function views
- 1. Add an import: from my_app import views
- 2. Add a URL to urlpatterns: path('', views.home, name='home')
-Class-based views
- 1. Add an import: from other_app.views import Home
- 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
-Including another URLconf
- 1. Import the include() function: from django.urls import include, path
- 2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
-"""
from django.contrib import admin
-from django.urls import path
+from django.urls import path, include
+from .import views
urlpatterns = [
path('admin/', admin.site.urls),
+ path('dashboard/', views.Dashboard.as_view(), name='dashboard'),
+ path('accounts/', include('accounts.urls', namespace='accounts')),
]
diff --git a/config/views.py b/config/views.py
new file mode 100644
index 0000000..1545f00
--- /dev/null
+++ b/config/views.py
@@ -0,0 +1,6 @@
+from django.shortcuts import render
+from django.views.generic import TemplateView
+
+
+class Dashboard(TemplateView):
+ template_name = 'index.html'
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 93a6ec2..70e65d2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1 +1,4 @@
-Django==3.0.7
\ No newline at end of file
+Django==3.0.7
+django-crispy-forms==1.9.2
+six==1.15.0
+Pillow==7.2.0
\ No newline at end of file
diff --git a/templates/base.html b/templates/base.html
new file mode 100644
index 0000000..b04cbe4
--- /dev/null
+++ b/templates/base.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+ Django Saas
+
+
+
+ {% block content %}
+ {% endblock %}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
new file mode 100644
index 0000000..6549be5
--- /dev/null
+++ b/templates/index.html
@@ -0,0 +1,6 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+{% endblock %}
\ No newline at end of file