diff --git a/sahara/db.sqlite3 b/sahara/db.sqlite3 index a9ec1ea..39db227 100644 Binary files a/sahara/db.sqlite3 and b/sahara/db.sqlite3 differ diff --git a/sahara/main/__pycache__/__init__.cpython-312.pyc b/sahara/main/__pycache__/__init__.cpython-312.pyc index 08c17ff..03635d8 100644 Binary files a/sahara/main/__pycache__/__init__.cpython-312.pyc and b/sahara/main/__pycache__/__init__.cpython-312.pyc differ diff --git a/sahara/main/__pycache__/admin.cpython-312.pyc b/sahara/main/__pycache__/admin.cpython-312.pyc index 736ac7f..bf855af 100644 Binary files a/sahara/main/__pycache__/admin.cpython-312.pyc and b/sahara/main/__pycache__/admin.cpython-312.pyc differ diff --git a/sahara/main/__pycache__/apps.cpython-312.pyc b/sahara/main/__pycache__/apps.cpython-312.pyc index 8d62f81..ee52d2f 100644 Binary files a/sahara/main/__pycache__/apps.cpython-312.pyc and b/sahara/main/__pycache__/apps.cpython-312.pyc differ diff --git a/sahara/main/__pycache__/forms.cpython-312.pyc b/sahara/main/__pycache__/forms.cpython-312.pyc index c5b5796..4fb3138 100644 Binary files a/sahara/main/__pycache__/forms.cpython-312.pyc and b/sahara/main/__pycache__/forms.cpython-312.pyc differ diff --git a/sahara/main/__pycache__/managers.cpython-312.pyc b/sahara/main/__pycache__/managers.cpython-312.pyc deleted file mode 100644 index 4369e48..0000000 Binary files a/sahara/main/__pycache__/managers.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/__pycache__/models.cpython-312.pyc b/sahara/main/__pycache__/models.cpython-312.pyc index 49e3c15..250731b 100644 Binary files a/sahara/main/__pycache__/models.cpython-312.pyc and b/sahara/main/__pycache__/models.cpython-312.pyc differ diff --git a/sahara/main/__pycache__/urls.cpython-312.pyc b/sahara/main/__pycache__/urls.cpython-312.pyc index 1bc0233..5a051a8 100644 Binary files a/sahara/main/__pycache__/urls.cpython-312.pyc and b/sahara/main/__pycache__/urls.cpython-312.pyc differ diff --git a/sahara/main/__pycache__/views.cpython-312.pyc b/sahara/main/__pycache__/views.cpython-312.pyc index 8adbc42..2f14f32 100644 Binary files a/sahara/main/__pycache__/views.cpython-312.pyc and b/sahara/main/__pycache__/views.cpython-312.pyc differ diff --git a/sahara/main/admin.py b/sahara/main/admin.py index bf461d9..bab3cf1 100644 --- a/sahara/main/admin.py +++ b/sahara/main/admin.py @@ -1,8 +1,37 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin +from .models import User # Import your custom User model -from .models import BaseUser +class CustomUserAdmin(UserAdmin): + model = User -# Register your models here. + # Fields to display in the list view of the admin + list_display = ['email', 'first_name', 'last_name', 'is_staff', 'is_superuser', 'is_active'] -admin.site.register(BaseUser) + # Fields to search in the admin + search_fields = ['email', 'first_name', 'last_name'] + # Default ordering of records in the admin + ordering = ['email'] + + # Fieldsets for the add and change forms in the admin + fieldsets = ( + (None, {'fields': ('email', 'password')}), + ('Personal Info', {'fields': ('first_name', 'last_name')}), + ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser')}), + ('Important Dates', {'fields': ('last_login',)}), # Remove 'date_joined' if it doesn't exist + ) + + # Fieldsets for the add form in the admin + add_fieldsets = ( + (None, { + 'classes': ('wide',), + 'fields': ('email', 'first_name', 'last_name', 'password1', 'password2', 'is_staff', 'is_superuser', 'is_active'), + }), + ) + + # Filters for the list view in the admin + list_filter = ('is_staff', 'is_superuser', 'is_active') + +# Register your custom User model with the custom UserAdmin class +admin.site.register(User, CustomUserAdmin) \ No newline at end of file diff --git a/sahara/main/forms.py b/sahara/main/forms.py index b8a7f9a..5f6afaf 100644 --- a/sahara/main/forms.py +++ b/sahara/main/forms.py @@ -1,45 +1,52 @@ from django import forms -from django.contrib.auth.forms import UserCreationForm -from .models import BaseUser +from django.contrib.auth.forms import AuthenticationForm +from .models import User -class BaseUserForm(UserCreationForm): - class Meta: - model = BaseUser - fields = [ - 'first_name', - 'last_name', - 'bio', - 'address', - 'contact_number', - 'image', - 'citizenship', - 'certificate', - 'price', - 'role', - 'email', - ] - password1 = forms.CharField( - widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Password'}), - label='Password', - min_length=8, +class UserRegistrationForm(forms.ModelForm): + password = forms.CharField( + widget=forms.PasswordInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter your password', + }) ) - password2 = forms.CharField( - widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': 'Confirm Password'}), - label='Confirm Password', - min_length=8, + confirm_password = forms.CharField( + widget=forms.PasswordInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Confirm your password', + }) ) - def clean_password2(self): - password1 = self.cleaned_data.get('password1') - password2 = self.cleaned_data.get('password2') + class Meta: + model = User + fields = ['first_name', 'last_name', 'email', 'password'] + widgets = { + 'first_name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter your first name', + }), + 'last_name': forms.TextInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter your last name', + }), + 'email': forms.EmailInput(attrs={ + 'class': 'form-control', + 'placeholder': 'Enter your email address', + }), + } + + def clean(self): + cleaned_data = super().clean() + password = cleaned_data.get('password') + confirm_password = cleaned_data.get('confirm_password') - if password1 and password2 and password1 != password2: - raise forms.ValidationError("Passwords don't match.") - return password2 + if password and confirm_password and password != confirm_password: + raise forms.ValidationError("Passwords do not match.") - def save(self, commit=True): - user = super().save(commit=False) - user.set_password(self.cleaned_data['password1']) - if commit: - user.save() - return user +class UserLoginForm(AuthenticationForm): + def __init__(self, *args, **kwargs): + self.request = kwargs.pop('request', None) + super(UserLoginForm, self).__init__(*args, **kwargs) + # Change the username field label and placeholder to 'Email' + self.fields['username'].label = 'Email' + self.fields['username'].widget.attrs.update({'placeholder': 'Enter your email'}) + self.fields['password'].widget.attrs.update({'placeholder': 'Enter your password'}) diff --git a/sahara/main/migrations/0001_initial.py b/sahara/main/migrations/0001_initial.py index c2014a6..2039572 100644 --- a/sahara/main/migrations/0001_initial.py +++ b/sahara/main/migrations/0001_initial.py @@ -1,8 +1,5 @@ -# Generated by Django 5.1.4 on 2025-01-11 12:36 +# Generated by Django 5.1.4 on 2025-01-11 17:02 -import django.contrib.auth.models -import django.contrib.auth.validators -import django.utils.timezone from django.db import migrations, models @@ -16,41 +13,22 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='BaseUser', + 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')), - ('image', models.ImageField(blank=True, null=True, upload_to='user_profiles/')), - ('bio', models.TextField(blank=True, null=True)), - ('address', models.TextField(blank=True, null=True)), - ('contact_number', models.CharField(blank=True, max_length=15, null=True)), - ('citizenship', models.CharField(blank=True, max_length=100, null=True)), - ('certificate', models.FileField(blank=True, null=True, upload_to='certificates/')), - ('is_verified', models.BooleanField(default=False)), - ('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('ratings', models.DecimalField(decimal_places=2, default=0.0, max_digits=3)), - ('role', models.CharField(choices=[('CLIENT', 'CLIENT'), ('ADMIN', 'ADMIN'), ('SERVICE_PROVIDER', 'SERVICE_PROVIDER')], default='CLIENT', max_length=50)), - ('member_since', models.DateTimeField(auto_now_add=True)), - ('profile_updated', models.DateTimeField(auto_now=True)), - ('token', models.CharField(blank=True, max_length=256, null=True)), + ('email', models.EmailField(max_length=254, unique=True)), + ('first_name', models.CharField(max_length=30)), + ('last_name', models.CharField(max_length=30)), + ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False)), + ('is_superuser', 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': 'Base User', - 'verbose_name_plural': 'Base Users', + 'abstract': False, }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], ), ] diff --git a/sahara/main/migrations/0002_user_bio_user_member_since_user_price_user_profile_and_more.py b/sahara/main/migrations/0002_user_bio_user_member_since_user_price_user_profile_and_more.py new file mode 100644 index 0000000..95499b1 --- /dev/null +++ b/sahara/main/migrations/0002_user_bio_user_member_since_user_price_user_profile_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 5.1.4 on 2025-01-11 19:36 + +import datetime +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='user', + name='bio', + field=models.CharField(default=datetime.datetime(2025, 1, 11, 19, 36, 0, 579795, tzinfo=datetime.timezone.utc), max_length=200), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='member_since', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='price', + field=models.PositiveBigIntegerField(default=10), + preserve_default=False, + ), + migrations.AddField( + model_name='user', + name='profile', + field=models.ImageField(blank=True, null=True, upload_to=''), + ), + migrations.AddField( + model_name='user', + name='updated_on', + field=models.DateField(auto_now=True), + ), + ] diff --git a/sahara/main/migrations/__pycache__/0001_initial.cpython-312.pyc b/sahara/main/migrations/__pycache__/0001_initial.cpython-312.pyc index 0a08844..0cb52ae 100644 Binary files a/sahara/main/migrations/__pycache__/0001_initial.cpython-312.pyc and b/sahara/main/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/sahara/main/migrations/__pycache__/0002_alter_baseuser_options_baseuser_date_joined_and_more.cpython-312.pyc b/sahara/main/migrations/__pycache__/0002_alter_baseuser_options_baseuser_date_joined_and_more.cpython-312.pyc deleted file mode 100644 index bf1341d..0000000 Binary files a/sahara/main/migrations/__pycache__/0002_alter_baseuser_options_baseuser_date_joined_and_more.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/migrations/__pycache__/0002_alter_baseuser_username.cpython-312.pyc b/sahara/main/migrations/__pycache__/0002_alter_baseuser_username.cpython-312.pyc deleted file mode 100644 index b1485a6..0000000 Binary files a/sahara/main/migrations/__pycache__/0002_alter_baseuser_username.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/migrations/__pycache__/0002_user_bio_user_member_since_user_price_user_profile_and_more.cpython-312.pyc b/sahara/main/migrations/__pycache__/0002_user_bio_user_member_since_user_price_user_profile_and_more.cpython-312.pyc new file mode 100644 index 0000000..f71382a Binary files /dev/null and b/sahara/main/migrations/__pycache__/0002_user_bio_user_member_since_user_price_user_profile_and_more.cpython-312.pyc differ diff --git a/sahara/main/migrations/__pycache__/0003_alter_baseuser_price.cpython-312.pyc b/sahara/main/migrations/__pycache__/0003_alter_baseuser_price.cpython-312.pyc deleted file mode 100644 index 2ad9068..0000000 Binary files a/sahara/main/migrations/__pycache__/0003_alter_baseuser_price.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/migrations/__pycache__/0003_remove_baseuser_role.cpython-312.pyc b/sahara/main/migrations/__pycache__/0003_remove_baseuser_role.cpython-312.pyc deleted file mode 100644 index 945339c..0000000 Binary files a/sahara/main/migrations/__pycache__/0003_remove_baseuser_role.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/migrations/__pycache__/0004_alter_baseuser_managers_baseuser_role.cpython-312.pyc b/sahara/main/migrations/__pycache__/0004_alter_baseuser_managers_baseuser_role.cpython-312.pyc deleted file mode 100644 index efc3cce..0000000 Binary files a/sahara/main/migrations/__pycache__/0004_alter_baseuser_managers_baseuser_role.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/migrations/__pycache__/0005_alter_baseuser_managers.cpython-312.pyc b/sahara/main/migrations/__pycache__/0005_alter_baseuser_managers.cpython-312.pyc deleted file mode 100644 index 94be69c..0000000 Binary files a/sahara/main/migrations/__pycache__/0005_alter_baseuser_managers.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/migrations/__pycache__/0006_remove_baseuser_username.cpython-312.pyc b/sahara/main/migrations/__pycache__/0006_remove_baseuser_username.cpython-312.pyc deleted file mode 100644 index b2f6519..0000000 Binary files a/sahara/main/migrations/__pycache__/0006_remove_baseuser_username.cpython-312.pyc and /dev/null differ diff --git a/sahara/main/migrations/__pycache__/__init__.cpython-312.pyc b/sahara/main/migrations/__pycache__/__init__.cpython-312.pyc index aac4277..732ba5a 100644 Binary files a/sahara/main/migrations/__pycache__/__init__.cpython-312.pyc and b/sahara/main/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/sahara/main/models.py b/sahara/main/models.py index efa7069..fb77b44 100644 --- a/sahara/main/models.py +++ b/sahara/main/models.py @@ -1,41 +1,110 @@ -from django.contrib.auth.models import AbstractUser from django.db import models +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin +from django.core.mail import send_mail +from django.utils.encoding import force_bytes +from django.template.loader import render_to_string +from django.contrib.auth.tokens import default_token_generator +from django.conf import settings +from django.utils.http import urlsafe_base64_encode -class BaseUser(AbstractUser): - # Additional fields to extend the default user model - - # Profile-related fields - image = models.ImageField(upload_to='user_profiles/', blank=True, null=True) - bio = models.TextField(blank=True, null=True) - address = models.TextField(blank=True, null=True) - contact_number = models.CharField(max_length=15, blank=True, null=True) - citizenship = models.CharField(max_length=100, blank=True, null=True) - certificate = models.FileField(upload_to='certificates/', blank=True, null=True) - - # Verification and status - is_verified = models.BooleanField(default=False) - - # Financial or business-related fields - price = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) - ratings = models.DecimalField(max_digits=3, decimal_places=2, default=0.00) - - # Role and other identifiers - role = models.CharField( - max_length=50, - choices=[('CLIENT', 'CLIENT'), ('ADMIN', 'ADMIN'), ('SERVICE_PROVIDER', 'SERVICE_PROVIDER')], - default='CLIENT' - ) - - # Time-related fields + +class Service(models.Model): + name = models.CharField(max_length=20) + description = models.CharField(max_length=100, blank=True, null=True) + def __str__(self): + return self.name + +class UserManager(BaseUserManager): + def create_user(self, first_name, last_name, email, password=None, **extra_fields): + """ + Creates and saves a User with the given first name, last name, email, and password. + """ + if not email: + raise ValueError('Users must have an email address') + + email = self.normalize_email(email) + user = self.model( + first_name=first_name, + last_name=last_name, + email=email, + **extra_fields + ) + user.set_password(password) + user.save(using=self._db) + + # Send verification email + try: + self.send_verification_email(user) + except: + print("Couldnot send verification email.") + + return user + + def create_superuser(self, first_name, last_name, email, password=None, **extra_fields): + """ + Creates and saves a superuser with the given first name, last name, email, and password. + """ + extra_fields.setdefault('is_staff', True) + extra_fields.setdefault('is_superuser', True) + extra_fields.setdefault('is_active', True) + + if extra_fields.get('is_staff') is not True: + raise ValueError('Superuser must have is_staff=True.') + if extra_fields.get('is_superuser') is not True: + raise ValueError('Superuser must have is_superuser=True.') + + return self.create_user(first_name, last_name, email, password, **extra_fields) + + def send_verification_email(self, user): + # Generate the token and the uid for the verification URL + token = default_token_generator.make_token(user) + uid = urlsafe_base64_encode(force_bytes(user.pk)) + verification_url = f"{settings.FRONTEND_URL}/verify-email/{uid}/{token}/" + + subject = "Verify Your Email Address" + message = render_to_string('emails/verification_email.html', { + 'user': user, + 'verification_url': verification_url, + }) + + send_mail( + subject, + message, + settings.DEFAULT_FROM_EMAIL, + [user.email], + fail_silently=False, + ) + + +class User(AbstractBaseUser, PermissionsMixin): + email = models.EmailField(unique=True) + first_name = models.CharField(max_length=30) + last_name = models.CharField(max_length=30) + is_active = models.BooleanField(default=True) + is_staff = models.BooleanField(default=False) + is_superuser = models.BooleanField(default=False) + profile = models.ImageField(blank=True,null=True) + price = models.PositiveBigIntegerField(blank=True, null=True) + bio = models.CharField(max_length=200, blank=True, null=True) member_since = models.DateTimeField(auto_now_add=True) - profile_updated = models.DateTimeField(auto_now=True) - - # Token field for verification or authentication purposes - token = models.CharField(max_length=256, blank=True, null=True) - + updated_on = models.DateField(auto_now=True) + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = ['first_name', 'last_name'] + + objects = UserManager() + def __str__(self): - return self.username - - class Meta: - verbose_name = 'Base User' - verbose_name_plural = 'Base Users' + return self.email + + def get_full_name(self): + return f"{self.first_name} {self.last_name}" + + def get_short_name(self): + return self.first_name + +class ServiceRequest(models.Model): + client = models.ForeignKey(User,on_delete=models.CASCADE,related_name="client") + service_provider = models.ForeignKey(User,on_delete=models.CASCADE,related_name="client") + agreed_price = models.DecimalField(max_digits=999,decimal_places=2 , default=0) + agreed_on = models.DateField(auto_now_add=True) + is_completed = models.BooleanField(default=False) diff --git a/sahara/main/templates/emails/verification_email.html b/sahara/main/templates/emails/verification_email.html new file mode 100644 index 0000000..6f82145 --- /dev/null +++ b/sahara/main/templates/emails/verification_email.html @@ -0,0 +1,12 @@ + + +
+Hello {{ user.get_full_name }},
+Please click the link below to verify your email address:
+ +If you did not create an account, please ignore this email.
+ + \ No newline at end of file diff --git a/sahara/main/templates/main/create_user.html b/sahara/main/templates/main/create_user.html deleted file mode 100644 index fe40825..0000000 --- a/sahara/main/templates/main/create_user.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - -Hello {{ user.first_name }},
-Thank you for signing up with Sahara! We're excited to have you on board.
-Please click the button below to activate your account:
- Activate Your Account -If you did not sign up for this account, please ignore this email.
-Best regards,
Sahara Team
Already have an account? Login here.
+The verification link is invalid or has expired.
+ + \ No newline at end of file diff --git a/sahara/main/templates/main/verification_success.html b/sahara/main/templates/main/verification_success.html new file mode 100644 index 0000000..2ad2912 --- /dev/null +++ b/sahara/main/templates/main/verification_success.html @@ -0,0 +1,10 @@ + + + +Your email has been verified. You can now log in.
+ + \ No newline at end of file diff --git a/sahara/main/urls.py b/sahara/main/urls.py index 33d5cee..82d2a3a 100644 --- a/sahara/main/urls.py +++ b/sahara/main/urls.py @@ -1,9 +1,11 @@ from django.urls import path -from .views import homeView,send_mail_page,createUserView,activate_account +from . import views urlpatterns = [ - path('',homeView, name="home"), - path('send-email/',send_mail_page,name="sendmail"), - path('user/register/',createUserView,name="register"), - path('activate/