diff --git a/README.md b/README.md index c0c51cb..ad3be47 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ on the [Django Tutorial project](https://docs.djangoproject.com/en/4.2/intro/tut ## Install and Run ### Run Setup.py Method - -Clone this repository and Run `setup.py` to install and run the project +1. Install [Python 3.11 or later](https://www.python.org/downloads/) +2. Clone this repository and Run `setup.py` to install and run the project **Don't forget to answer the question from `setup.py` to setup the project** ```bash diff --git a/mysite/settings.py b/mysite/settings.py index f5523db..3ed39ce 100644 --- a/mysite/settings.py +++ b/mysite/settings.py @@ -10,6 +10,8 @@ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ +import logging +import os from pathlib import Path from decouple import config, Csv @@ -70,6 +72,95 @@ TEMPLATES = [ WSGI_APPLICATION = 'mysite.wsgi.application' +# * Loggin template from https://www.youtube.com/watch?v=m_EkU56KdJg, Modify a bit. + +LOGS_DIR = os.path.join(BASE_DIR, 'logs') + +if not os.path.exists(LOGS_DIR): + os.makedirs(LOGS_DIR) + +# ! LOGGERS -> entry point into the logging system. + +LOGGERS = ( + { + "django": { + "handlers": ["console_handler", "info_handler"], + "level": "INFO", + }, + "django.request": { + "handlers": ["error_handler"], + "level": "INFO", + "propagate": True, + }, + "django.template": { + "handlers": ["error_handler"], + "level": "DEBUG", + "propagate": True, + }, + "django.server": { + "handlers": ["error_handler"], + "level": "INFO", + "propagate": True, + }, + }, +) + +# ! FORMATTER -> a log record needs to be rendered as text. Formatters describe the exact format of that text. + +FORMATTERS = ( + { + "verbose": { + "format": "{levelname} {asctime:s} {name} {threadName} {thread:d} {module} {filename} {lineno:d} {name} {funcName} {process:d} {message}", + "style": "{", + }, + "simple": { + "format": "{levelname} {asctime:s} {name} {module} {filename} {lineno:d} {funcName} {message}", + "style": "{", + }, + }, +) + +# ! HANDLERS -> The handler is the engine that determines what happens to each message in a logger + +HANDLERS = { + # StreamHandler -> sends log messages to the console + "console_handler": { + "class": "logging.StreamHandler", + "formatter": "simple", + "level": "DEBUG" + }, # RotatingFileHandlers -> write log messages to files + "info_handler": { + "class": "logging.handlers.RotatingFileHandler", + "filename": f"{BASE_DIR}/logs/blogthedata_info.log", + "mode": "a", + "encoding": "utf-8", + "formatter": "verbose", + "level": "INFO", + "backupCount": 5, + "maxBytes": 1024 * 1024 * 5, # 5 MB + }, + "error_handler": { + "class": "logging.handlers.RotatingFileHandler", + "filename": f"{BASE_DIR}/logs/blogthedata_error.log", + "mode": "a", + "formatter": "verbose", + "level": "WARNING", + "backupCount": 5, + "maxBytes": 1024 * 1024 * 5, # 5 MB + }, +} + + +# ! LOGGING + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, # If set to True, it disables all loggers from previous configurations + "formatters": FORMATTERS[0], + "handlers": HANDLERS, + "loggers": LOGGERS[0], +} + # Database # https://docs.djangoproject.com/en/4.2/ref/settings/#databases diff --git a/polls/apps.py b/polls/apps.py index 1dffd05..ab3fd84 100644 --- a/polls/apps.py +++ b/polls/apps.py @@ -4,4 +4,6 @@ from django.apps import AppConfig class PollsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'polls' - \ No newline at end of file + + def ready(self) -> None: + import polls.signals \ No newline at end of file diff --git a/polls/forms.py b/polls/forms.py index 1e738ea..ffdf2f8 100644 --- a/polls/forms.py +++ b/polls/forms.py @@ -1,3 +1,6 @@ +import logging +from typing import Any + from django import forms from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User @@ -5,7 +8,7 @@ from django.contrib.auth.models import User class SignUpForm(UserCreationForm): tailwind_class = "w-full border-2 border-gray-300 bg-gray-100 rounded-lg focus:ring focus:border-blue-300 focus:shadow-none" - + logger = logging.getLogger('signup_form') username = forms.CharField(widget=forms.TextInput(attrs={'class': tailwind_class}), error_messages={ 'unique': 'This username is already in use.', @@ -18,6 +21,16 @@ class SignUpForm(UserCreationForm): ) password2 = forms.CharField(widget=forms.PasswordInput(attrs={'class': tailwind_class}),) + # commit -> default =True -> If commit is True -> want to save the user object to the database. + def save(self, commit: bool = True) -> Any: + user = super().save(commit=False) + + if commit: + user.save() + self.logger.info(f"User registered with username: {user.username}") + + return user + class Meta: model = User fields = ('username', 'password1', 'password2') diff --git a/polls/signals.py b/polls/signals.py new file mode 100644 index 0000000..4f63242 --- /dev/null +++ b/polls/signals.py @@ -0,0 +1,38 @@ +import logging +from django.contrib.auth.signals import user_logged_in, user_logged_out, user_login_failed +from django.dispatch import receiver + +log = logging.getLogger("django") + +def get_client_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip + +#! https://stackoverflow.com/questions/37618473/how-can-i-log-both-successful-and-failed-login-and-logout-attempts-in-django +@receiver(user_logged_in) +def user_logged_in_callback(sender, request, user, **kwargs): + ip = get_client_ip(request) + + log.info('Login User: {user} via ip: {ip}'.format( + user=user, + ip=ip + )) + +@receiver(user_logged_out) +def user_logged_out_callback(sender, request, user, **kwargs): + ip = get_client_ip(request) + + log.info('Logout User: {user} via ip: {ip}'.format( + user=user, + ip=ip + )) + +@receiver(user_login_failed) +def user_login_failed_callback(sender, credentials, **kwargs): + log.warning('Login Failed for: {credentials}'.format( + credentials=credentials, + )) \ No newline at end of file diff --git a/polls/views.py b/polls/views.py index bcdf518..76af896 100644 --- a/polls/views.py +++ b/polls/views.py @@ -1,4 +1,5 @@ -from django.http import HttpResponseRedirect +import logging + from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse from django.views import generic @@ -13,6 +14,9 @@ from .forms import SignUpForm from .models import Choice, Question, Vote +logger = logging.getLogger("django") + + class IndexView(generic.ListView): """View for index.html.""" @@ -98,15 +102,19 @@ def vote(request, question_id): A function that updates the database. Adds a vote count to the choice that the user votes for in a specific question_id. """ + ip = get_client_ip(request) question = get_object_or_404(Question, pk=question_id) if request.method == "POST": try: selected_choice = question.choice_set.get(pk=request.POST["choice"]) except (KeyError, Choice.DoesNotExist): + logger.error(f"User {request.user.username} ({ip}) didn't select choice.") messages.error(request, "You didn't select a choice.") return redirect("polls:detail", question_id) + logger.info(f"User {request.user.username} ({ip}) select choice {selected_choice}") + if question.can_vote(): # ! Return 1. object element 2. boolean status of creation vote, created = Vote.objects.update_or_create( @@ -116,8 +124,10 @@ def vote(request, question_id): ) if created: + logger.info(f"User {request.user.username} ({ip}) vote on choice {selected_choice}") messages.success(request, "You voted successfully🥳") else: + logger.info(f"User {request.user.username} ({ip}) update his answer to {selected_choice}") messages.success(request, "You updated your vote🥳") return redirect("polls:results", question_id) @@ -126,4 +136,14 @@ def vote(request, question_id): return redirect("polls:index") else: messages.error(request, "Invalid request method.") - return redirect("polls:index") \ No newline at end of file + return redirect("polls:index") + + +# https://stackoverflow.com/questions/4581789/how-do-i-get-user-ip-address-in-django +def get_client_ip(request): + x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') + if x_forwarded_for: + ip = x_forwarded_for.split(',')[0] + else: + ip = request.META.get('REMOTE_ADDR') + return ip