diff --git a/.gitignore b/.gitignore index a979ee7..a1a43d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,21 @@ -/venv \ No newline at end of file +# Created by https://www.toptal.com/developers/gitignore/api/django +# Edit at https://www.toptal.com/developers/gitignore?templates=django + +### Django ### +*.log +*.pot +*.pyc +__pycache__/ +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/file.csv b/file.csv new file mode 100644 index 0000000..a7f1128 --- /dev/null +++ b/file.csv @@ -0,0 +1,6 @@ +title,author,publisher,available_quantity +Book 1,Author 1,Publisher A,10 +Book 2,Author 2,Publisher B,15 +Book 3,Author 3,Publisher A,8 +Book 4,Author 4,Publisher C,20 +Book 5,Author 5,Publisher B,12 diff --git a/library/settings.py b/library/settings.py index 51069ea..8c691e9 100644 --- a/library/settings.py +++ b/library/settings.py @@ -10,7 +10,23 @@ https://docs.djangoproject.com/en/4.2/ref/settings/ """ +import os from pathlib import Path +import environ +import certifi + +env = environ.Env() +environ.Env.read_env() + +# Previous settings ... +EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' +EMAIL_HOST = env('EMAIL_HOST') +EMAIL_PORT = 587 +EMAIL_USE_TLS = True +EMAIL_HOST_USER = env('EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = env('EMAIL_HOST_PASSWORD') +os.environ['SSL_CERT_FILE'] = certifi.where() + # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -37,6 +53,10 @@ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'users', + 'library_management', + 'rest_framework', + 'django_crontab', ] MIDDLEWARE = [ @@ -54,7 +74,7 @@ TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': ['templates'], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -80,7 +100,6 @@ } } - # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators @@ -99,6 +118,7 @@ }, ] +AUTH_USER_MODEL = 'users.User' # Internationalization # https://docs.djangoproject.com/en/4.2/topics/i18n/ @@ -121,3 +141,12 @@ # https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +CRONJOBS = [ + ('* * * * *', 'library_management.cron.send_return_reminders', + '>> /Users/rohansaeed/Desktop/library-management/library_management/debug7.log') +] + + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') diff --git a/library/urls.py b/library/urls.py index 89c46af..a19fbd7 100644 --- a/library/urls.py +++ b/library/urls.py @@ -15,8 +15,10 @@ 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 urlpatterns = [ path('admin/', admin.site.urls), + path('user/', include('users.urls')), + path('lib/', include('library_management.urls')) ] diff --git a/library_management/admin.py b/library_management/admin.py new file mode 100644 index 0000000..3834417 --- /dev/null +++ b/library_management/admin.py @@ -0,0 +1,8 @@ +from django.contrib import admin + +from .models import Book, BookIssue, NewBookTicket +# Register your models here. + +admin.site.register(Book) +admin.site.register(BookIssue) +admin.site.register(NewBookTicket) diff --git a/library_management/apps.py b/library_management/apps.py new file mode 100644 index 0000000..9591f7f --- /dev/null +++ b/library_management/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class LibraryManagementConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'library_management' diff --git a/library_management/cron.py b/library_management/cron.py new file mode 100644 index 0000000..e425b94 --- /dev/null +++ b/library_management/cron.py @@ -0,0 +1,18 @@ +from django.core.mail import send_mail +from django.utils import timezone +from .models import BookIssue + + +def send_return_reminders(): + actual_return_date = timezone.now() + timezone.timedelta(days=15) + overdue_books = BookIssue.objects.filter( + return_date__lt=actual_return_date, returned=True) + for book in overdue_books: + user = book.user + subject = 'Return Reminder' + message = f"Dear {user.username}, please remember to return the book '{book.name}' as soon as possible." + from_email = 'rohan.saeed@arbisoft.com' + recipient_list = [user.email] + + send_mail(subject, message, from_email, + recipient_list, fail_silently=False) diff --git a/library/__init__.py b/library_management/management/__init__.py similarity index 100% rename from library/__init__.py rename to library_management/management/__init__.py diff --git a/library_management/management/commands/__init__.py b/library_management/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_management/management/commands/my_books.py b/library_management/management/commands/my_books.py new file mode 100644 index 0000000..782d050 --- /dev/null +++ b/library_management/management/commands/my_books.py @@ -0,0 +1,41 @@ +# myapp/management/commands/import_books_from_csv.py +import csv +from django.core.management.base import BaseCommand + +from library_management.models import Book + + +class Command(BaseCommand): + help = 'Import books from a CSV file' + + def add_arguments(self, parser): + parser.add_argument('file_path', type=str, help='Path to the CSV file') + + def handle(self, *args, **kwargs): + file_path = kwargs['file_path'] + try: + with open(file_path, 'r') as csvfile: + reader = csv.DictReader(csvfile) + for row in reader: + title = row['title'] + author = row['author'] + publisher = row['publisher'] + available_quantity = int(row['available_quantity']) + + book, created = Book.objects.get_or_create( + name=title, + author=author, + publisher=publisher, + defaults={'quantity': available_quantity} + ) + + if not created: + book.quantity = available_quantity + book.save() + + self.stdout.write(self.style.SUCCESS( + f'Successfully imported book: {title}')) + + except FileNotFoundError: + self.stdout.write(self.style.ERROR( + 'File not found. Please check the file path.')) diff --git a/library_management/migrations/0001_initial.py b/library_management/migrations/0001_initial.py new file mode 100644 index 0000000..32bcf4f --- /dev/null +++ b/library_management/migrations/0001_initial.py @@ -0,0 +1,49 @@ +# Generated by Django 4.2.6 on 2023-11-06 05:45 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Book', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('author', models.CharField(max_length=100)), + ('publisher', models.CharField(max_length=100)), + ('image', models.ImageField(blank=True, null=True, upload_to='books/')), + ('quantity', models.PositiveIntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='NewBookTicket', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('Pending', 'Pending'), ('Approved', 'Approved'), ('Rejected', 'Rejected')], default='Pending', max_length=20)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library_management.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='BookIssue', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('issued_date', models.DateField(auto_now=True)), + ('return_date', models.DateField()), + ('returned', models.BooleanField(default=False)), + ('status', models.CharField(choices=[('Issued', 'Issued'), ('Pending', 'Pending'), ('Rejected', 'Rejected')], default='Pending', max_length=20)), + ('book', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='library_management.book')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/library_management/migrations/0002_alter_bookissue_status.py b/library_management/migrations/0002_alter_bookissue_status.py new file mode 100644 index 0000000..9e5cfef --- /dev/null +++ b/library_management/migrations/0002_alter_bookissue_status.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.6 on 2023-11-07 05:21 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('library_management', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='bookissue', + name='status', + field=models.CharField(choices=[('pending', 'Pending'), ('issued', 'Issued'), ('rejected', 'Rejected')], default='pending', max_length=9), + ), + ] diff --git a/library_management/migrations/__init__.py b/library_management/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/library_management/models.py b/library_management/models.py new file mode 100644 index 0000000..6a5cac6 --- /dev/null +++ b/library_management/models.py @@ -0,0 +1,70 @@ +from django.conf import settings +from django.db import models +from users.models import User +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.core.mail import send_mail + + +class Book(models.Model): + name = models.CharField(max_length=100) + author = models.CharField(max_length=100) + publisher = models.CharField(max_length=100) + image = models.ImageField(upload_to='books/', null=True, blank=True) + quantity = models.PositiveIntegerField(default=0) + + +class BookIssue(models.Model): + # REQUEST_STATUS_CHOICES = ( + # ('Issued', 'Issued'), + # ('Pending', 'Pending'), + # ('Rejected', 'Rejected'), + # ) + user = models.ForeignKey(User, on_delete=models.CASCADE) + book = models.ForeignKey(Book, on_delete=models.CASCADE) + issued_date = models.DateField(auto_now=True) + return_date = models.DateField() + returned = models.BooleanField(default=False) + + class Status(models.TextChoices): + """ + Status of each request, changed to depict the current status + Utilized TextChoices here as I required some comparisons with keys using request data + """ + PENDING = "pending", "Pending" + ISSUED = "issued", "Issued" + REJECTED = "rejected", "Rejected" + status = models.CharField(choices=Status.choices, + max_length=9, default=Status.PENDING) + + # status = models.CharField( + # max_length=20, choices=REQUEST_STATUS_CHOICES, default='Pending') + + +class NewBookTicket(models.Model): + BOOK_TICKET_STATUS_CHOICES = ( + ('Pending', 'Pending'), + ('Approved', 'Approved'), + ('Rejected', 'Rejected'), + ) + user = models.ForeignKey(User, on_delete=models.CASCADE) + book = models.ForeignKey(Book, on_delete=models.CASCADE) + status = models.CharField( + max_length=20, choices=BOOK_TICKET_STATUS_CHOICES, default='Pending') + + def __str__(self): + return f'{self.user} - {self.book}' + + +@receiver(post_save, sender=NewBookTicket) +def call_car_api(sender, instance, **kwargs): + subject = 'Ticket Received' + message = f"Dear {instance.user.username}, we have got your request regarding '{instance.book.name}'. We will soon inform you about availability ." + from_email = 'rohan.saeed@arbisoft.com' + recipient_list = [instance.user.email] + + send_mail(subject, message, from_email, + recipient_list, fail_silently=False) + + print('Ticket object created') + print(sender, instance, kwargs) diff --git a/library_management/permissions.py b/library_management/permissions.py new file mode 100644 index 0000000..902f3f5 --- /dev/null +++ b/library_management/permissions.py @@ -0,0 +1,8 @@ +from rest_framework import permissions + + +class IsStaffEditorPermission(permissions.DjangoModelPermissions): + def has_permission(self, request, view): + if request.method == 'GET': + return request.user and request.user.is_authenticated + return request.user and request.user.is_authenticated and (request.user.is_librarian or request.user.is_staff) diff --git a/library_management/serializers.py b/library_management/serializers.py new file mode 100644 index 0000000..a1f6104 --- /dev/null +++ b/library_management/serializers.py @@ -0,0 +1,68 @@ +from rest_framework import serializers +from .models import Book, BookIssue, NewBookTicket + + +class BookSerializer(serializers.ModelSerializer): + class Meta: + model = Book + fields = ['name', 'author', 'publisher', 'image', 'quantity'] + + +class UserIssueSerializer(serializers.ModelSerializer): + class Meta: + model = BookIssue + fields = ['user', 'book', + 'issued_date', 'returned', 'return_date'] + read_only_fields = ['returned'] + extra_kwargs = {'user': {'default': serializers.CurrentUserDefault()}} + + +class LibrarianIssueSerializer(serializers.ModelSerializer): + class Meta: + model = BookIssue + fields = ['user', 'book', 'issued_date', + 'returned', 'return_date', 'status'] + + +class LibrarianUpdateIssueSerializer(serializers.ModelSerializer): + class Meta: + model = BookIssue + fields = ['user', 'book', 'issued_date', + 'returned', 'return_date', 'status'] + read_only_fields = ['id', 'user', 'book'] + + +class UserUpdateIssueSerializer(serializers.ModelSerializer): + class Meta: + model = BookIssue + fields = ['user', 'book', + 'issued_date', 'returned', 'return_date'] + read_only_fields = fields + + +class UserTicketSerializer(serializers.ModelSerializer): + class Meta: + model = NewBookTicket + fields = ['id', 'user', 'book', 'status'] + read_only_fields = ['status'] + + +class LibrarianTicketSerializer(serializers.ModelSerializer): + class Meta: + model = NewBookTicket + fields = ['id', 'user', 'book', 'status'] + read_only_fields = fields + + +class LibrarianUpdateTicketSerializer(serializers.ModelSerializer): + class Meta: + model = NewBookTicket + fields = ['id', 'user', 'book', 'status'] + read_only_fields = ['id', 'user', 'book'] + + +class UserUpdateTicketSerializer(serializers.ModelSerializer): + class Meta: + model = NewBookTicket + fields = ['id', 'user', 'book', 'status'] + read_only_fields = fields diff --git a/library_management/tests.py b/library_management/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/library_management/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/library_management/urls.py b/library_management/urls.py new file mode 100644 index 0000000..b6e6cb4 --- /dev/null +++ b/library_management/urls.py @@ -0,0 +1,13 @@ +from django.urls import path +from . import views +urlpatterns = [ + + path('', views.BookList.as_view(), name='book-list'), + path('/', views.BookDetail.as_view()), + + path('issues/', views.BookIssueList.as_view(), name='issues'), + path('issues//', views.BookIssueDetail.as_view()), + + path('tickets/', views.BookTicketView.as_view(), name='tickets'), + path('tickets//', views.BookTicketDetail.as_view()), +] diff --git a/library_management/views.py b/library_management/views.py new file mode 100644 index 0000000..d0b9d35 --- /dev/null +++ b/library_management/views.py @@ -0,0 +1,78 @@ +from .models import Book, BookIssue, NewBookTicket +from .permissions import IsStaffEditorPermission +from rest_framework import generics +from .serializers import BookSerializer, UserIssueSerializer, LibrarianIssueSerializer, LibrarianUpdateIssueSerializer, UserTicketSerializer, LibrarianTicketSerializer, LibrarianUpdateTicketSerializer, UserUpdateIssueSerializer, UserUpdateTicketSerializer + + +class BookList(generics.ListCreateAPIView): + queryset = Book.objects.all() + serializer_class = BookSerializer + permission_classes = [IsStaffEditorPermission] + + def get_queryset(self): + queryset = super().get_queryset() + name = self.request.query_params.get('name') + if name is not None: + queryset = queryset.filter(name__contains=name) + return queryset + + +class BookDetail(generics.RetrieveAPIView): + queryset = Book.objects.all() + serializer_class = BookSerializer + lookup_field = 'pk' + + +class BookIssueList(generics.ListCreateAPIView): + queryset = BookIssue.objects.all() + + def get_queryset(self): + queryset = super().get_queryset() + if not self.request.user.is_librarian: + queryset = queryset.filter(user=self.request.user) + return queryset + + def get_serializer_class(self): + if self.request.user.is_librarian: + return LibrarianIssueSerializer + return UserIssueSerializer + + +class BookIssueDetail(generics.RetrieveUpdateAPIView): + queryset = BookIssue.objects.all() + lookup_field = 'pk' + + def get_serializer_class(self): + if self.request.user.is_librarian: + return LibrarianUpdateIssueSerializer + return UserUpdateIssueSerializer + + +class BookTicketView(generics.ListCreateAPIView): + queryset = NewBookTicket.objects.all() + + def get_serializer_class(self): + if self.request.user.is_librarian: + return LibrarianTicketSerializer + return UserTicketSerializer + + def get_queryset(self): + queryset = super().get_queryset() + if not self.request.user.is_librarian: + queryset = queryset.filter(user=self.request.user) + return queryset + + +class BookTicketDetail(generics.RetrieveUpdateAPIView): + queryset = NewBookTicket.objects.all() + lookup_field = 'pk' + + def get_serializer_class(self): + if self.request.user.is_librarian: + return LibrarianUpdateTicketSerializer + return UserUpdateTicketSerializer + + def get_queryset(self): + queryset = super().get_queryset() + queryset = queryset.filter(user=self.request.user) + return queryset diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..fac08aa --- /dev/null +++ b/templates/base.html @@ -0,0 +1,43 @@ + + + + + + {% block title %}Authentication App{% endblock %} + + + + + +
{% block content %} {% endblock %}
+ + diff --git a/templates/book_request.html b/templates/book_request.html new file mode 100644 index 0000000..3da207e --- /dev/null +++ b/templates/book_request.html @@ -0,0 +1,14 @@ + + + + Book Request + + +

Request a Book

+ +
+ {% csrf_token %} {{ form.as_p }} + +
+ + diff --git a/templates/book_return.html b/templates/book_return.html new file mode 100644 index 0000000..96b8af2 --- /dev/null +++ b/templates/book_return.html @@ -0,0 +1,18 @@ + + + + Return a Book + + +

Return a Book

+ + {% if books_issued %} +
+ {% csrf_token %} {{ form.as_table }} + +
+ {% else %} +

No books issued.

+ {% endif %} + + diff --git a/templates/book_search_results.html b/templates/book_search_results.html new file mode 100644 index 0000000..9571cf1 --- /dev/null +++ b/templates/book_search_results.html @@ -0,0 +1,20 @@ + + + + Search Results + + +

Search Results

+
    + {% for book in books %} +
  • + Name: {{ book.name }}, Author:{{book.author}}, Available Quantity: + {{book.quantity}} +
  • + + {% empty %} +
  • No books found.
  • + {% endfor %} +
+ + diff --git a/templates/data.html b/templates/data.html new file mode 100644 index 0000000..4847987 --- /dev/null +++ b/templates/data.html @@ -0,0 +1,5 @@ +{% extends 'base.html' %} {% block content %} +
+

{{user}}

+
+{% endblock %} diff --git a/templates/librarian_dashboard.html b/templates/librarian_dashboard.html new file mode 100644 index 0000000..e1855a3 --- /dev/null +++ b/templates/librarian_dashboard.html @@ -0,0 +1,14 @@ +{% extends 'base.html' %} {% block content %} +

Librarian Dashboard

+

Pending Book Requests

+
    + {% for request in pending_requests %} +
  • + {{ request.user }} requested {{ request.book_title }} + Accept + Reject +
  • + {% endfor %} +
+ +{% endblock %} diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..1a22669 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,16 @@ +{% extends 'base.html' %} {% block content %} +

Login

+
+ {% csrf_token %} +
+ + {{ form.username }} +
+ +
+ + {{form.password}} +
+ +
+{% endblock %} diff --git a/templates/max_requests.html b/templates/max_requests.html new file mode 100644 index 0000000..8379d46 --- /dev/null +++ b/templates/max_requests.html @@ -0,0 +1 @@ +

You have reached maximum limit of requests.

diff --git a/templates/my_books.html b/templates/my_books.html new file mode 100644 index 0000000..11587ef --- /dev/null +++ b/templates/my_books.html @@ -0,0 +1,33 @@ + + + + My Books + + +

My Books

+

Issued Books

+
    + {% for book_issue in my_books.issued_books %} +
  • + {{ book_issue.book.name }} (Return by: {{ book_issue.return_date }}) +
  • + {% endfor %} +
+ +

Requested Books

+
    + {% for book_request in my_books.requested_books %} +
  • {{ book_request.book.name }} (Status: {{ book_request.status }})
  • + {% endfor %} +
+ +

Returned Books

+
    + {% for book_return in my_books.returned_books %} +
  • + {{ book_return.book.name }} (Return Date: {{ book_return.return_date }}) +
  • + {% endfor %} +
+ + diff --git a/templates/new_ticket.html b/templates/new_ticket.html new file mode 100644 index 0000000..89fa056 --- /dev/null +++ b/templates/new_ticket.html @@ -0,0 +1,7 @@ +{% extends "base.html" %} {% block content %} +

Create a New Ticket

+
+ {% csrf_token %} {{ form.as_p }} + +
+{% endblock %} diff --git a/templates/profile.html b/templates/profile.html new file mode 100644 index 0000000..5841c6e --- /dev/null +++ b/templates/profile.html @@ -0,0 +1,7 @@ +{% extends 'base.html' %} {% block content %} +

User Profile

+
+ {% csrf_token %} {{ form.as_p }} + +
+{% endblock %} diff --git a/templates/signup.html b/templates/signup.html new file mode 100644 index 0000000..cc501c8 --- /dev/null +++ b/templates/signup.html @@ -0,0 +1,38 @@ +{% extends 'base.html' %} {% block content %} +

Sign Up

+
+ {% csrf_token %} +
+ + {{ form.username }} +
+ +
+ + {{ form.email }} +
+ +
+ + {{form.password1}} +
+ +
+ + {{form.password2}} +
+
+ + {{form.date_of_birth}} +
+
+ + {{form.phone}} +
+
+ + {{form.gender}} +
+ +
+{% endblock %} diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..7b9e4a4 --- /dev/null +++ b/users/admin.py @@ -0,0 +1,5 @@ +from django.contrib import admin +from .models import User +# Register your models here. + +admin.site.register(User) diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..72b1401 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'users' diff --git a/users/forms.py b/users/forms.py new file mode 100644 index 0000000..7086733 --- /dev/null +++ b/users/forms.py @@ -0,0 +1,32 @@ +from django import forms +from django.contrib.auth.forms import UserCreationForm, AuthenticationForm +from .models import User + +GENDER_CHOICES = [ + ('Male', 'Male'), ('Female', 'Female'), ('Other', 'Other') +] + + +class SignUpForm(UserCreationForm): + date_of_birth = forms.DateField( + required=False, widget=forms.DateInput(attrs={'type': 'date'})) + phone = forms.CharField( + required=False, widget=forms.TextInput(attrs={'type': 'text'})) + gender = forms.ChoiceField(required=False, choices=GENDER_CHOICES) + + class Meta: + model = User + fields = ['username', 'password1', 'password2', 'email', + 'date_of_birth', 'phone', 'gender'] + + +class LoginForm(AuthenticationForm): + class Meta: + model = User + fields = ['username', 'password'] + + +class ProfileUpdateForm(forms.ModelForm): + class Meta: + model = User + fields = ['date_of_birth'] diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..875d6b5 --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,48 @@ +# Generated by Django 4.2.5 on 2023-11-06 05:42 + +import django.contrib.auth.models +import django.contrib.auth.validators +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('auth', '0012_alter_user_first_name_max_length'), + ] + + operations = [ + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(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')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('date_of_birth', models.DateField(blank=True, null=True)), + ('phone', models.CharField(blank=True, max_length=15, null=True)), + ('gender', models.CharField(blank=True, choices=[('Male', 'Male'), ('Female', 'Female'), ('Other', 'Other')], max_length=10, null=True)), + ('is_librarian', models.BooleanField(default=False)), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..03c46e1 --- /dev/null +++ b/users/models.py @@ -0,0 +1,13 @@ +from django.db import models +from django.contrib.auth.models import AbstractUser + + +class User(AbstractUser): + date_of_birth = models.DateField(null=True, blank=True) + phone = models.CharField(max_length=15, blank=True, null=True) + gender = models.CharField(max_length=10, choices=[( + 'Male', 'Male'), ('Female', 'Female'), ('Other', 'Other')], blank=True, null=True) + is_librarian = models.BooleanField(default=False) + + def __str__(self): + return self.username diff --git a/users/tests.py b/users/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/users/urls.py b/users/urls.py new file mode 100644 index 0000000..40984b2 --- /dev/null +++ b/users/urls.py @@ -0,0 +1,9 @@ +from django.urls import path, include +from . import views +urlpatterns = [ + path('signup/', views.signup.as_view(), name='signup'), + path('login/', views.user_login.as_view(), name='login'), + path('logout/', views.user_logout.as_view(), name='logout'), + path('profile/', views.profile.as_view(), name='profile'), + path('', views.signup.as_view(), name='main') +] diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..67c5cbf --- /dev/null +++ b/users/views.py @@ -0,0 +1,58 @@ +from django.shortcuts import render, redirect +from django.contrib.auth import login, authenticate, logout +from django.views import View +from .forms import SignUpForm, LoginForm, ProfileUpdateForm + + +class signup(View): + def post(self, request): + form = SignUpForm(request.POST) + if form.is_valid(): + form.save() + username = form.cleaned_data['username'] + password = form.cleaned_data['password1'] + user = authenticate(username=username, password=password) + login(request, user) + return redirect('book-list') + return render(request, 'signup.html', {'form': form}) + + def get(self, request): + form = SignUpForm() + return render(request, 'signup.html', {'form': form}) + + +class user_login(View): + def post(self, request): + form = LoginForm(data=request.POST) + if form.is_valid(): + username = form.cleaned_data['username'] + password = form.cleaned_data['password'] + user = authenticate(username=username, password=password) + if user is not None: + login(request, user) + return redirect('book-list') + return render(request, 'login.html', {'form': form}) + + def get(self, request): + form = LoginForm() + return render(request, 'login.html', {'form': form}) + + +class user_logout(View): + def get(self, request): + logout(request) + return redirect('login') + + +class profile(View): + def post(self, request): + user = request.user + form = ProfileUpdateForm(request.POST, instance=user) + if form.is_valid(): + form.save() + return render(request, 'profile.html', {'form': form}) + + def get(self, request): + user = request.user + form = ProfileUpdateForm(instance=user) + return render(request, 'profile.html', {'form': form})