diff --git a/.env b/.env new file mode 100644 index 0000000..39813f7 --- /dev/null +++ b/.env @@ -0,0 +1,19 @@ +SECRET_KEY=abc +DEBUG=True + +# Databse credentials +DATABASE_ENGINE=django.db.backends.postgresql +DATABASE_NAME=astro +DATABASE_USER=postgres +DATABASE_PASSWORD=qwer +DATABASE_HOST=localhost +DATABASE_PORT=5432 + +CELERY_BROKER_URL=redis://127.0.0.1:6379 +CACHE_BROKER_URL=redis://127.0.0.1:6379/1 + +# creds for sending email service +EMAIL_HOST=sandbox.smtp.mailtrap.io +EMAIL_HOST_USER=aaea48213879d3 +EMAIL_HOST_PASSWORD=31aed4f86b21fb +EMAIL_PORT=2525 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ae412d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +env/ \ No newline at end of file diff --git a/astro/__init__.py b/astro/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/astro/__pycache__/__init__.cpython-39.pyc b/astro/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..7190bb4 Binary files /dev/null and b/astro/__pycache__/__init__.cpython-39.pyc differ diff --git a/astro/__pycache__/celery.cpython-39.pyc b/astro/__pycache__/celery.cpython-39.pyc new file mode 100644 index 0000000..2e65a3e Binary files /dev/null and b/astro/__pycache__/celery.cpython-39.pyc differ diff --git a/astro/__pycache__/settings.cpython-39.pyc b/astro/__pycache__/settings.cpython-39.pyc new file mode 100644 index 0000000..ecaf323 Binary files /dev/null and b/astro/__pycache__/settings.cpython-39.pyc differ diff --git a/astro/__pycache__/urls.cpython-39.pyc b/astro/__pycache__/urls.cpython-39.pyc new file mode 100644 index 0000000..79aaa60 Binary files /dev/null and b/astro/__pycache__/urls.cpython-39.pyc differ diff --git a/astro/__pycache__/wsgi.cpython-39.pyc b/astro/__pycache__/wsgi.cpython-39.pyc new file mode 100644 index 0000000..3968483 Binary files /dev/null and b/astro/__pycache__/wsgi.cpython-39.pyc differ diff --git a/astro/asgi.py b/astro/asgi.py new file mode 100644 index 0000000..bf33f6a --- /dev/null +++ b/astro/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for astro project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'astro.settings') + +application = get_asgi_application() diff --git a/astro/celery.py b/astro/celery.py new file mode 100644 index 0000000..fbaac94 --- /dev/null +++ b/astro/celery.py @@ -0,0 +1,24 @@ +from __future__ import absolute_import, unicode_literals + +import os + +from celery import Celery + +# set the default Django settings module for the 'celery' program. +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "astro.settings") + +app = Celery("astro") + +# Using a string here means the worker don't have to serialize +# the configuration object to child processes. +# - namespace='CELERY' means all celery-related configuration keys +# should have a `CELERY_` prefix. +app.config_from_object("django.conf:settings", namespace="CELERY") + +# Load task modules from all registered Django app configs. +app.autodiscover_tasks() + + +@app.task(bind=True) +def debug_task(self): + print("Request: {0!r}".format(self.request)) diff --git a/astro/settings.py b/astro/settings.py new file mode 100644 index 0000000..78904d0 --- /dev/null +++ b/astro/settings.py @@ -0,0 +1,190 @@ +""" +Django settings for astro project. + +Generated by 'django-admin startproject' using Django 4.1.7. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/4.1/ref/settings/ +""" +from dotenv import load_dotenv + +load_dotenv() + +import os +from os import environ +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = environ["SECRET_KEY"] + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = environ["DEBUG"] == "True" + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "rest_framework.authtoken", + "django_celery_results", + "django_celery_beat", + "users", + "properties", + "bids", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "astro.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": ["templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "astro.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/4.1/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": environ["DATABASE_ENGINE"], + "NAME": environ["DATABASE_NAME"], + "USER": environ["DATABASE_USER"], + "PASSWORD": environ["DATABASE_PASSWORD"], + "HOST": environ["DATABASE_HOST"], + "PORT": environ["DATABASE_PORT"], + } +} + + +# Password validation +# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/4.1/topics/i18n/ + +LANGUAGE_CODE = "en-us" + +TIME_ZONE = "UTC" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/4.1/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + + +REST_FRAMEWORK = { + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.TokenAuthentication", + ], + "DEFAULT_FILTER_BACKENDS": ["django_filters.rest_framework.DjangoFilterBackend"], + "DEFAULT_RENDERER_CLASSES": [ + "rest_framework.renderers.JSONRenderer", + ] + + (["rest_framework.renderers.BrowsableAPIRenderer"] if DEBUG else []), +} + +AUTH_USER_MODEL = "users.User" +STATIC_URL = "/static/" +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, "static"), +] +MEDIA_URL = "/media/" +MEDIA_ROOT = os.path.join(BASE_DIR, "media") + + +CELERY_BROKER_URL = environ["CELERY_BROKER_URL"] +CELERY_RESULT_BACKEND = "django-db" +CELERY_CACHE_BACKEND = "django-cache" +CELERY_ACCEPT_CONTENT = ["json", "pickle"] +CELERY_TASK_SERIALIZER = "json" +CELERY_RESULT_SERIALIZER = "json" +CELERY_BROKER_TRANSPORT_OPTIONS = { + "max_retries": environ.get("CELERY_PUBLISH_MAX_RETRIES", 1) +} +CELERY_SEND_TASK_ERROR_EMAILS = True +CELERY_IGNORE_RESULT = False +CELERY_SEND_EVENTS = True +DJANGO_CELERY_RESULTS_TASK_ID_MAX_LENGTH = 100 + +from celery.schedules import crontab + +CELERY_BEAT_SCHEDULE = { + "mark_inactive_property_in_every_15_minutes": { + "task": "properties.tasks.mark_inactive_property", + "schedule": crontab(minute="*/15"), + } +} + + +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = environ["EMAIL_HOST"] +EMAIL_HOST_USER = environ["EMAIL_HOST_USER"] +EMAIL_HOST_PASSWORD = environ["EMAIL_HOST_PASSWORD"] +EMAIL_PORT = environ["EMAIL_PORT"] diff --git a/astro/urls.py b/astro/urls.py new file mode 100644 index 0000000..be2a60f --- /dev/null +++ b/astro/urls.py @@ -0,0 +1,44 @@ +"""astro URL Configuration + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/4.1/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.conf import settings +from django.conf.urls.static import static +from django.contrib import admin +from django.shortcuts import render +from django.urls import include, path + + +def home(request): + return render(request, "index.html") + + +def about(request): + return render(request, "about.html") + + +def contact(request): + return render(request, "contact.html") + + +urlpatterns = [ + path("", home, name="home"), + path("about/", about, name="about"), + path("contact/", contact, name="contact"), + path("admin/", admin.site.urls), + path("users/", include("users.urls")), + path("properties/", include("properties.urls")), + path("bids/", include("bids.urls")), +] +urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/astro/wsgi.py b/astro/wsgi.py new file mode 100644 index 0000000..17aed8c --- /dev/null +++ b/astro/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for astro project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'astro.settings') + +application = get_wsgi_application() diff --git a/bids/__init__.py b/bids/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bids/__pycache__/__init__.cpython-39.pyc b/bids/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..18f564b Binary files /dev/null and b/bids/__pycache__/__init__.cpython-39.pyc differ diff --git a/bids/__pycache__/admin.cpython-39.pyc b/bids/__pycache__/admin.cpython-39.pyc new file mode 100644 index 0000000..74e897a Binary files /dev/null and b/bids/__pycache__/admin.cpython-39.pyc differ diff --git a/bids/__pycache__/apps.cpython-39.pyc b/bids/__pycache__/apps.cpython-39.pyc new file mode 100644 index 0000000..9799246 Binary files /dev/null and b/bids/__pycache__/apps.cpython-39.pyc differ diff --git a/bids/__pycache__/models.cpython-39.pyc b/bids/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000..a5b4363 Binary files /dev/null and b/bids/__pycache__/models.cpython-39.pyc differ diff --git a/bids/__pycache__/serializers.cpython-39.pyc b/bids/__pycache__/serializers.cpython-39.pyc new file mode 100644 index 0000000..659fadb Binary files /dev/null and b/bids/__pycache__/serializers.cpython-39.pyc differ diff --git a/bids/__pycache__/urls.cpython-39.pyc b/bids/__pycache__/urls.cpython-39.pyc new file mode 100644 index 0000000..eb1b69e Binary files /dev/null and b/bids/__pycache__/urls.cpython-39.pyc differ diff --git a/bids/__pycache__/views.cpython-39.pyc b/bids/__pycache__/views.cpython-39.pyc new file mode 100644 index 0000000..7525978 Binary files /dev/null and b/bids/__pycache__/views.cpython-39.pyc differ diff --git a/bids/admin.py b/bids/admin.py new file mode 100644 index 0000000..1567fd0 --- /dev/null +++ b/bids/admin.py @@ -0,0 +1,11 @@ +from django.contrib import admin +from django.contrib.admin import ModelAdmin + +from bids.models import Auction + + +@admin.register(Auction) +class AuctionAdmin(ModelAdmin): + list_display = ("id", "property", "buyer", "amount", "is_active", "date_created") + list_filter = ("is_active",) + search_fields = ("property__name", "buyer__email", "amount") diff --git a/bids/apps.py b/bids/apps.py new file mode 100644 index 0000000..4c209b8 --- /dev/null +++ b/bids/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BidsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "bids" diff --git a/bids/migrations/0001_initial.py b/bids/migrations/0001_initial.py new file mode 100644 index 0000000..81210e8 --- /dev/null +++ b/bids/migrations/0001_initial.py @@ -0,0 +1,56 @@ +# Generated by Django 4.1.7 on 2023-03-25 17:41 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + ("properties", "0001_initial"), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Auction", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("amount", models.PositiveBigIntegerField()), + ("is_active", models.BooleanField(default=True)), + ( + "date_created", + models.DateTimeField(default=django.utils.timezone.now), + ), + ( + "buyer", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "property", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to="properties.property", + ), + ), + ], + options={ + "verbose_name": "Auction", + "verbose_name_plural": "Auctions", + }, + ), + ] diff --git a/bids/migrations/__init__.py b/bids/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bids/migrations/__pycache__/0001_initial.cpython-39.pyc b/bids/migrations/__pycache__/0001_initial.cpython-39.pyc new file mode 100644 index 0000000..43b91eb Binary files /dev/null and b/bids/migrations/__pycache__/0001_initial.cpython-39.pyc differ diff --git a/bids/migrations/__pycache__/__init__.cpython-39.pyc b/bids/migrations/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..aa1d458 Binary files /dev/null and b/bids/migrations/__pycache__/__init__.cpython-39.pyc differ diff --git a/bids/models.py b/bids/models.py new file mode 100644 index 0000000..d12b01f --- /dev/null +++ b/bids/models.py @@ -0,0 +1,47 @@ +from django.core.exceptions import ValidationError +from django.db import models +from django.utils.timezone import now + +from properties.models import Property +from users.models import User + + +class Auction(models.Model): + property = models.ForeignKey(Property, on_delete=models.PROTECT) + buyer = models.ForeignKey(User, on_delete=models.PROTECT) + amount = models.PositiveBigIntegerField() + is_active = models.BooleanField(default=True) + date_created = models.DateTimeField(default=now) + + class Meta: + verbose_name = "Auction" + verbose_name_plural = "Auctions" + + def __str__(self) -> str: + return f"{self.property.name} - {self.amount}" + + def validate_auction(self): + if not self.property.start_date < now() < self.property.closing_date: + raise ValidationError( + "Bidding date and time must be in between auction start date and closing date." + ) + + def validate_amount(self): + if self.amount <= ( + Auction.objects.filter(property=self.property, is_active=True) + .order_by("-amount") + .first() + or 0 + ): + raise ValidationError("Bidding amount must be greater than highest amount.") + + def full_clean(self, exclude, validate_unique): + super().full_clean(exclude, validate_unique) + self.validate_auction() + self.validate_amount() + + def save(self, *args, **kwargs): + skip_clean = kwargs.pop("skip_clean", False) + if not skip_clean: + self.full_clean(exclude=kwargs.pop("exclude_clean", None)) + return super().save(*args, **kwargs) diff --git a/bids/serializers.py b/bids/serializers.py new file mode 100644 index 0000000..9617baa --- /dev/null +++ b/bids/serializers.py @@ -0,0 +1,9 @@ +from rest_framework import serializers + +from bids.models import Auction + + +class AuctionSerializer(serializers.ModelSerializer): + class Meta: + model = Auction + fields = ("id", "buyer", "property", "amount", "date_created") diff --git a/bids/tests.py b/bids/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/bids/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/bids/urls.py b/bids/urls.py new file mode 100644 index 0000000..b6d6bd9 --- /dev/null +++ b/bids/urls.py @@ -0,0 +1,7 @@ +from django.urls import path + +from bids.views import CreateBidAPI + +urlpatterns = [ + path("create/", CreateBidAPI.as_view(), name="create-bid"), +] diff --git a/bids/views.py b/bids/views.py new file mode 100644 index 0000000..d18307b --- /dev/null +++ b/bids/views.py @@ -0,0 +1,59 @@ +from django.core.exceptions import ValidationError +from django.shortcuts import render + +# Create your views here. +from django.utils.timezone import now +from rest_framework import serializers +from rest_framework.response import Response +from rest_framework.views import APIView + +from bids.models import Auction +from bids.serializers import AuctionSerializer +from properties.models import Property +from properties.permissions import IsBuyer + + +class CreateBidAPI(APIView): + permission_classes = [IsBuyer] + + class InputSerializer(serializers.Serializer): + property = serializers.IntegerField() + amount = serializers.IntegerField() + + def get(self, request): + serializer = self.InputSerializer(data=request.data) + if not serializer.is_valid(): + return Response(data=serializer.errors, status=400) + validated_data = serializer.validated_data + current_time = now() + try: + property = Property.objects.get( + is_active=True, + id=validated_data["property"], + start_date__lte=current_time, + closing_date__gte=current_time, + ) + except Property.DoesNotExist: + return Response(data="Property not found", status=404) + + max_bid = ( + property.auction_set.filter(is_active=True).order_by("-amount").first() + ) + if max_bid and validated_data["amount"] <= max_bid.amount: + return Response( + data=f"Maximum bid is {max_bid.amount}, enter the greater amount...", + status=400, + ) + + bid = Auction( + property=property, buyer=request.user, amount=validated_data["amount"] + ) + try: + bid.save() + except ValidationError as error: + return Response(data=error, status=400) + + return Response( + data=AuctionSerializer(instance=bid).data, + status=201, + ) diff --git a/celerybeat-schedule.bak b/celerybeat-schedule.bak new file mode 100644 index 0000000..22b6f5f --- /dev/null +++ b/celerybeat-schedule.bak @@ -0,0 +1,4 @@ +'entries', (0, 446) +'__version__', (512, 15) +'tz', (1024, 4) +'utc_enabled', (1536, 4) diff --git a/celerybeat-schedule.dat b/celerybeat-schedule.dat new file mode 100644 index 0000000..09f0b44 Binary files /dev/null and b/celerybeat-schedule.dat differ diff --git a/celerybeat-schedule.dir b/celerybeat-schedule.dir new file mode 100644 index 0000000..22b6f5f --- /dev/null +++ b/celerybeat-schedule.dir @@ -0,0 +1,4 @@ +'entries', (0, 446) +'__version__', (512, 15) +'tz', (1024, 4) +'utc_enabled', (1536, 4) diff --git a/helpers/__pycache__/files.cpython-39.pyc b/helpers/__pycache__/files.cpython-39.pyc new file mode 100644 index 0000000..819beda Binary files /dev/null and b/helpers/__pycache__/files.cpython-39.pyc differ diff --git a/helpers/files.py b/helpers/files.py new file mode 100644 index 0000000..02adee5 --- /dev/null +++ b/helpers/files.py @@ -0,0 +1,35 @@ +import os + +from django.conf import settings +from django.core.exceptions import ValidationError +from django.core.files import File +from django.core.files.storage import FileSystemStorage +from django.utils.deconstruct import deconstructible + + +@deconstructible +class RenameFile: + def __init__(self, pattern): + self.pattern = pattern + + def __call__(self, instance, filename): + extension = filename.split(".")[-1] + return self.pattern.format(instance=instance, extension=extension) + + +file_storage = FileSystemStorage(location=settings.BASE_DIR) + + +@deconstructible +class ValidateFileSize: + """ + Used to validate file size + """ + + def __init__(self, max_file_size) -> None: + self.file_size = max_file_size + + def __call__(self, file: File) -> None: + file_size = file.size + if file_size > self.file_size * 1024 * 1024: + raise ValidationError(f"Max file size limit is {file_size}.") diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..6872db0 --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'astro.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/media/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in_black_and_white_frame_in_3d.png b/media/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in_black_and_white_frame_in_3d.png new file mode 100644 index 0000000..38c3eb5 Binary files /dev/null and b/media/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in_black_and_white_frame_in_3d.png differ diff --git a/properties/__init__.py b/properties/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/properties/__pycache__/__init__.cpython-39.pyc b/properties/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..a6c037b Binary files /dev/null and b/properties/__pycache__/__init__.cpython-39.pyc differ diff --git a/properties/__pycache__/admin.cpython-39.pyc b/properties/__pycache__/admin.cpython-39.pyc new file mode 100644 index 0000000..f442030 Binary files /dev/null and b/properties/__pycache__/admin.cpython-39.pyc differ diff --git a/properties/__pycache__/apps.cpython-39.pyc b/properties/__pycache__/apps.cpython-39.pyc new file mode 100644 index 0000000..c1a3eb5 Binary files /dev/null and b/properties/__pycache__/apps.cpython-39.pyc differ diff --git a/properties/__pycache__/models.cpython-39.pyc b/properties/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000..f6b5b5a Binary files /dev/null and b/properties/__pycache__/models.cpython-39.pyc differ diff --git a/properties/__pycache__/permissions.cpython-39.pyc b/properties/__pycache__/permissions.cpython-39.pyc new file mode 100644 index 0000000..395194e Binary files /dev/null and b/properties/__pycache__/permissions.cpython-39.pyc differ diff --git a/properties/__pycache__/serializers.cpython-39.pyc b/properties/__pycache__/serializers.cpython-39.pyc new file mode 100644 index 0000000..5645130 Binary files /dev/null and b/properties/__pycache__/serializers.cpython-39.pyc differ diff --git a/properties/__pycache__/signals.cpython-39.pyc b/properties/__pycache__/signals.cpython-39.pyc new file mode 100644 index 0000000..cddc43b Binary files /dev/null and b/properties/__pycache__/signals.cpython-39.pyc differ diff --git a/properties/__pycache__/tasks.cpython-39.pyc b/properties/__pycache__/tasks.cpython-39.pyc new file mode 100644 index 0000000..3212030 Binary files /dev/null and b/properties/__pycache__/tasks.cpython-39.pyc differ diff --git a/properties/__pycache__/urls.cpython-39.pyc b/properties/__pycache__/urls.cpython-39.pyc new file mode 100644 index 0000000..c7b928d Binary files /dev/null and b/properties/__pycache__/urls.cpython-39.pyc differ diff --git a/properties/__pycache__/views.cpython-39.pyc b/properties/__pycache__/views.cpython-39.pyc new file mode 100644 index 0000000..ad1aef1 Binary files /dev/null and b/properties/__pycache__/views.cpython-39.pyc differ diff --git a/properties/admin.py b/properties/admin.py new file mode 100644 index 0000000..b85e0b2 --- /dev/null +++ b/properties/admin.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from django.contrib.admin import ModelAdmin + +from properties.models import Property, PropertyDocument, PropertyGallery + + +@admin.register(Property) +class PropertyAdmin(ModelAdmin): + list_display = ( + "id", + "name", + "seller", + "base_price", + "start_date", + "closing_date", + "is_active", + ) + list_filter = ("is_active",) + search_fields = ("name", "seller__email", "location") + + +@admin.register(PropertyGallery) +class ProfileGalleryAdmin(ModelAdmin): + list_display = ("id", "property", "file") + search_fields = ("property__name", "property__location") + + +@admin.register(PropertyDocument) +class ProfileDocumentAdmin(ModelAdmin): + list_display = ("id", "property", "document") + search_fields = ("property__name", "property__location") diff --git a/properties/apps.py b/properties/apps.py new file mode 100644 index 0000000..82bf532 --- /dev/null +++ b/properties/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PropertiesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "properties" diff --git a/properties/documents/1/DALLE_2023-04-04_17.31.03_-_fight_between_shark_and_dolfin_in_blu_rvL2oi6.jpg b/properties/documents/1/DALLE_2023-04-04_17.31.03_-_fight_between_shark_and_dolfin_in_blu_rvL2oi6.jpg new file mode 100644 index 0000000..7442dfb Binary files /dev/null and b/properties/documents/1/DALLE_2023-04-04_17.31.03_-_fight_between_shark_and_dolfin_in_blu_rvL2oi6.jpg differ diff --git a/properties/documents/2/DALLE_2023-04-04_17.10.38_-_karate_fight_between_moon_and_sun_and_cnh5RMD.jpg b/properties/documents/2/DALLE_2023-04-04_17.10.38_-_karate_fight_between_moon_and_sun_and_cnh5RMD.jpg new file mode 100644 index 0000000..52c1233 Binary files /dev/null and b/properties/documents/2/DALLE_2023-04-04_17.10.38_-_karate_fight_between_moon_and_sun_and_cnh5RMD.jpg differ diff --git a/properties/gallery/1/DALLE_2023-04-04_17.30.52_-__AGfiTks.png b/properties/gallery/1/DALLE_2023-04-04_17.30.52_-__AGfiTks.png new file mode 100644 index 0000000..ff701a9 Binary files /dev/null and b/properties/gallery/1/DALLE_2023-04-04_17.30.52_-__AGfiTks.png differ diff --git a/properties/gallery/1/DALLE_2023-04-04_17.30.52_-__j3PKomN.png b/properties/gallery/1/DALLE_2023-04-04_17.30.52_-__j3PKomN.png new file mode 100644 index 0000000..ff701a9 Binary files /dev/null and b/properties/gallery/1/DALLE_2023-04-04_17.30.52_-__j3PKomN.png differ diff --git a/properties/gallery/1/DALLE_2023-04-04_17.31.09_-_fight_between_shark_and_dolfin_in_blue__coyBRC9.png b/properties/gallery/1/DALLE_2023-04-04_17.31.09_-_fight_between_shark_and_dolfin_in_blue__coyBRC9.png new file mode 100644 index 0000000..36f7c3e Binary files /dev/null and b/properties/gallery/1/DALLE_2023-04-04_17.31.09_-_fight_between_shark_and_dolfin_in_blue__coyBRC9.png differ diff --git a/properties/gallery/1/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in__U1v1h9x.png b/properties/gallery/1/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in__U1v1h9x.png new file mode 100644 index 0000000..38c3eb5 Binary files /dev/null and b/properties/gallery/1/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in__U1v1h9x.png differ diff --git a/properties/gallery/2/DALLE_2023-03-07_01.33.20_-_flash_is_the_great_leader_and_also_have_WpUCF0S.png b/properties/gallery/2/DALLE_2023-03-07_01.33.20_-_flash_is_the_great_leader_and_also_have_WpUCF0S.png new file mode 100644 index 0000000..fde706a Binary files /dev/null and b/properties/gallery/2/DALLE_2023-03-07_01.33.20_-_flash_is_the_great_leader_and_also_have_WpUCF0S.png differ diff --git a/properties/gallery/2/DALLE_2023-04-04_17.10.38_-__RJRV3j6.png b/properties/gallery/2/DALLE_2023-04-04_17.10.38_-__RJRV3j6.png new file mode 100644 index 0000000..52c1233 Binary files /dev/null and b/properties/gallery/2/DALLE_2023-04-04_17.10.38_-__RJRV3j6.png differ diff --git a/properties/gallery/2/DALLE_2023-04-04_17.31.09_-_fight_between_shark_and_dolfin_in_blue__x1wagbw.png b/properties/gallery/2/DALLE_2023-04-04_17.31.09_-_fight_between_shark_and_dolfin_in_blue__x1wagbw.png new file mode 100644 index 0000000..36f7c3e Binary files /dev/null and b/properties/gallery/2/DALLE_2023-04-04_17.31.09_-_fight_between_shark_and_dolfin_in_blue__x1wagbw.png differ diff --git a/properties/gallery/2/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in__Pa4MSj3.png b/properties/gallery/2/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in__Pa4MSj3.png new file mode 100644 index 0000000..38c3eb5 Binary files /dev/null and b/properties/gallery/2/DALLE_2023-04-04_17.44.15_-_a_sudoku_in_hell_with_a_pubg_player_in__Pa4MSj3.png differ diff --git a/properties/migrations/0001_initial.py b/properties/migrations/0001_initial.py new file mode 100644 index 0000000..ac05f44 --- /dev/null +++ b/properties/migrations/0001_initial.py @@ -0,0 +1,116 @@ +# Generated by Django 4.1.7 on 2023-03-25 17:41 + +import django.core.validators +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name="Property", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("name", models.CharField(max_length=256)), + ("location", models.TextField()), + ( + "base_price", + models.PositiveBigIntegerField( + validators=[django.core.validators.MinValueValidator(10000)] + ), + ), + ("start_date", models.DateTimeField()), + ("closing_date", models.DateTimeField()), + ("is_active", models.BooleanField(default=False)), + ( + "seller", + models.ForeignKey( + on_delete=django.db.models.deletion.PROTECT, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Property", + "verbose_name_plural": "Properties", + }, + ), + migrations.CreateModel( + name="PropertyGallery", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "file", + models.FileField( + upload_to="property_gallery/{instance.property_id}/{instance.file.name}.jpg" + ), + ), + ( + "property", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="properties.property", + ), + ), + ], + options={ + "verbose_name": "Property Gallery", + "verbose_name_plural": "Property Gallery", + }, + ), + migrations.CreateModel( + name="PropertyDocument", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "document", + models.FileField( + upload_to="property_documents/{instance.property_id}/{instance.document.name}.jpg" + ), + ), + ("description", models.TextField()), + ( + "property", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="properties.property", + ), + ), + ], + options={ + "verbose_name": "Property Document", + "verbose_name_plural": "Property Documents", + }, + ), + ] diff --git a/properties/migrations/0002_alter_propertydocument_document_and_more.py b/properties/migrations/0002_alter_propertydocument_document_and_more.py new file mode 100644 index 0000000..4c8593d --- /dev/null +++ b/properties/migrations/0002_alter_propertydocument_document_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-04-06 10:19 + +from django.db import migrations, models +import helpers.files + + +class Migration(migrations.Migration): + dependencies = [ + ("properties", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="propertydocument", + name="document", + field=models.FileField( + upload_to=helpers.files.RenameFile( + "property_documents/{instance.property_id}/{instance.document.name}.jpg" + ) + ), + ), + migrations.AlterField( + model_name="propertygallery", + name="file", + field=models.FileField( + upload_to=helpers.files.RenameFile( + "property_gallery/{instance.property_id}/{instance.file.name}.{extension}" + ) + ), + ), + ] diff --git a/properties/migrations/0003_alter_propertydocument_document_and_more.py b/properties/migrations/0003_alter_propertydocument_document_and_more.py new file mode 100644 index 0000000..40e477a --- /dev/null +++ b/properties/migrations/0003_alter_propertydocument_document_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 4.1.7 on 2023-04-06 10:22 + +from django.db import migrations, models +import helpers.files + + +class Migration(migrations.Migration): + dependencies = [ + ("properties", "0002_alter_propertydocument_document_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="propertydocument", + name="document", + field=models.FileField( + upload_to=helpers.files.RenameFile( + "properties/documents/{instance.property_id}/{instance.document.name}.jpg" + ) + ), + ), + migrations.AlterField( + model_name="propertygallery", + name="file", + field=models.FileField( + upload_to=helpers.files.RenameFile( + "properties/gallery/{instance.property_id}/{instance.file.name}.{extension}" + ) + ), + ), + ] diff --git a/properties/migrations/0004_alter_propertydocument_document_and_more.py b/properties/migrations/0004_alter_propertydocument_document_and_more.py new file mode 100644 index 0000000..c3c9f83 --- /dev/null +++ b/properties/migrations/0004_alter_propertydocument_document_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.1.7 on 2023-04-15 10:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("properties", "0003_alter_propertydocument_document_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="propertydocument", + name="document", + field=models.FileField(upload_to=""), + ), + migrations.AlterField( + model_name="propertygallery", + name="file", + field=models.FileField(upload_to=""), + ), + ] diff --git a/properties/migrations/__init__.py b/properties/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/properties/migrations/__pycache__/0001_initial.cpython-39.pyc b/properties/migrations/__pycache__/0001_initial.cpython-39.pyc new file mode 100644 index 0000000..77ce11a Binary files /dev/null and b/properties/migrations/__pycache__/0001_initial.cpython-39.pyc differ diff --git a/properties/migrations/__pycache__/0002_alter_propertydocument_document_and_more.cpython-39.pyc b/properties/migrations/__pycache__/0002_alter_propertydocument_document_and_more.cpython-39.pyc new file mode 100644 index 0000000..0217c97 Binary files /dev/null and b/properties/migrations/__pycache__/0002_alter_propertydocument_document_and_more.cpython-39.pyc differ diff --git a/properties/migrations/__pycache__/0003_alter_propertydocument_document_and_more.cpython-39.pyc b/properties/migrations/__pycache__/0003_alter_propertydocument_document_and_more.cpython-39.pyc new file mode 100644 index 0000000..8bf238c Binary files /dev/null and b/properties/migrations/__pycache__/0003_alter_propertydocument_document_and_more.cpython-39.pyc differ diff --git a/properties/migrations/__pycache__/0004_alter_propertydocument_document_and_more.cpython-39.pyc b/properties/migrations/__pycache__/0004_alter_propertydocument_document_and_more.cpython-39.pyc new file mode 100644 index 0000000..a067f52 Binary files /dev/null and b/properties/migrations/__pycache__/0004_alter_propertydocument_document_and_more.cpython-39.pyc differ diff --git a/properties/migrations/__pycache__/__init__.cpython-39.pyc b/properties/migrations/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..565919d Binary files /dev/null and b/properties/migrations/__pycache__/__init__.cpython-39.pyc differ diff --git a/properties/models.py b/properties/models.py new file mode 100644 index 0000000..ecefc1a --- /dev/null +++ b/properties/models.py @@ -0,0 +1,141 @@ +from django.core.exceptions import ValidationError +from django.core.validators import FileExtensionValidator, MinValueValidator +from django.db import models +from django.utils.timezone import now + +from helpers.files import RenameFile, ValidateFileSize, file_storage +from properties.tasks import send_emails_task +from users.models import User + + +class Property(models.Model): + """ + Model to store properties information to be auctioned. + """ + + name = models.CharField(max_length=256) + seller = models.ForeignKey(User, on_delete=models.PROTECT) + location = models.TextField() + base_price = models.PositiveBigIntegerField( + validators=[MinValueValidator(10000)] + ) # in INR + start_date = models.DateTimeField() + closing_date = models.DateTimeField() + is_active = models.BooleanField(default=False) + + class Meta: + verbose_name = "Property" + verbose_name_plural = "Properties" + + def __str__(self) -> str: + return self.name + + def validate_dates(self): + if self.start_date < now(): + raise ValidationError("Start date must be greater than present.") + + if self.start_date > self.closing_date: + raise ValidationError("Closing date must be greater than start date.") + + def clean(self): + self.validate_dates() + + def full_clean(self, exclude=None, validate_unique=True) -> None: + breakpoint() + if self.pk and self.is_active: + initial_instance = Property.objects.get(id=self.pk) + if not initial_instance.is_active: + send_emails_task.delay(property_id=self.pk) + + return super().full_clean(exclude, validate_unique) + + def save(self, *args, **kwargs): + skip_clean = kwargs.pop("skip_clean", False) + if not skip_clean: + self.full_clean(exclude=kwargs.pop("exclude_clean", None)) + return super().save(*args, **kwargs) + + +class PropertyGallery(models.Model): + """ + Model to store property images or videos to be auctioned. + """ + + MAX_FILE_ALLOWED = 5 + MAX_FILE_SIZE_ALLOWED = 5 # MB + EXTENSIONS_ALLOWED = ("jpeg", "png", "jpg") + + property = models.ForeignKey(Property, on_delete=models.CASCADE) + file = models.FileField( + validators=[ + ValidateFileSize(max_file_size=MAX_FILE_SIZE_ALLOWED), + FileExtensionValidator(allowed_extensions=EXTENSIONS_ALLOWED), + ], + ) + + class Meta: + verbose_name = "Property Gallery" + verbose_name_plural = "Property Gallery" + + def __str__(self) -> str: + return f"{self.pk} {self.property.name}" + + def validate_file_count(self): + if ( + PropertyGallery.objects.filter(property_id=self.property_id).count() + > self.MAX_FILE_ALLOWED + ): + raise ValidationError("Max file limit reached.") + + def full_clean(self, exclude, validate_unique): + super().full_clean(exclude, validate_unique) + self.validate_file_count() + + def save(self, *args, **kwargs): + skip_clean = kwargs.pop("skip_clean", False) + if not skip_clean: + self.full_clean(exclude=kwargs.pop("exclude_clean", None)) + return super().save(*args, **kwargs) + + +class PropertyDocument(models.Model): + """ + Model to store property documents to be auctioned. + """ + + MAX_DOCUMENT_ALLOWED = 5 + MAX_FILE_SIZE_ALLOWED = 5 # MB + EXTENSIONS_ALLOWED = ("pdf", "jpeg", "png", "jpg") + + property = models.ForeignKey(Property, on_delete=models.CASCADE) + document = models.FileField( + validators=[ + ValidateFileSize(max_file_size=MAX_FILE_SIZE_ALLOWED), + FileExtensionValidator(allowed_extensions=EXTENSIONS_ALLOWED), + ], + ) + description = models.TextField() + + class Meta: + verbose_name = "Property Document" + verbose_name_plural = "Property Documents" + + def __str__(self) -> str: + return f"{self.pk} {self.property.name}" + + def validate_file_count(self): + if ( + PropertyDocument.objects.filter(property_id=self.property_id).count() + > self.MAX_DOCUMENT_ALLOWED + ): + raise ValidationError("Max document limit reached.") + + def full_clean(self, exclude, validate_unique): + super().full_clean(exclude, validate_unique) + self.validate_file_count() + + def save(self, *args, **kwargs): + skip_clean = kwargs.pop("skip_clean", False) + if not skip_clean: + self.full_clean(exclude=kwargs.pop("exclude_clean", None)) + return super().save(*args, **kwargs) diff --git a/properties/permissions.py b/properties/permissions.py new file mode 100644 index 0000000..af51930 --- /dev/null +++ b/properties/permissions.py @@ -0,0 +1,9 @@ +from rest_framework.permissions import IsAuthenticated + + +class IsBuyer(IsAuthenticated): + def has_permission(self, request, view): + return ( + bool(super().has_permission(request=request, view=view)) + and request.user.is_buyer + ) diff --git a/properties/serializers.py b/properties/serializers.py new file mode 100644 index 0000000..1256de1 --- /dev/null +++ b/properties/serializers.py @@ -0,0 +1,29 @@ +from rest_framework import serializers + +from properties.models import Property, PropertyDocument, PropertyGallery + + +class PropertySerializer(serializers.ModelSerializer): + class Meta: + model = Property + fields = ( + "id", + "name", + "seller", + "location", + "base_price", + "start_date", + "closing_date", + ) + + +class PropertyGallerySerializer(serializers.ModelSerializer): + class Meta: + model = PropertyGallery + fields = ("id", "property", "file") + + +class PropertyDocumentSerializer(serializers.ModelSerializer): + class Meta: + model = PropertyDocument + fields = ("id", "property", "document") diff --git a/properties/tasks.py b/properties/tasks.py new file mode 100644 index 0000000..11b801d --- /dev/null +++ b/properties/tasks.py @@ -0,0 +1,98 @@ +from os import environ + +from celery import shared_task +from django.conf import settings +from django.core.mail import send_mail +from django.utils.timezone import now + +from users.models import User + + +@shared_task() +def send_emails_task(property_id: int): + from properties.models import Property + + try: + property = Property.objects.get(id=property_id) + except Property.DoesNotExist: + raise Exception(f" not found") + + email_subject = f"Bidder | New Property - {property.name}" + email_message = f"""Hey, +New property is ready for auction. +Property Details: + Name: {property.name} + Seller: {property.seller.full_name} + Ask Price: {property.base_price} + Auction Start Date: {property.start_date} + Auction Closing Date: {property.closing_date} + Location: {property.location} + +Best regards, +Team Bidder +""" + users = list(User.objects.filter(is_buyer=True).values_list("email", flat=True)) + send_mail( + subject=email_subject, + message=email_message, + recipient_list=users, + from_email="noreply@bidder.com", + auth_user=settings.EMAIL_HOST_USER, + auth_password=settings.EMAIL_HOST_PASSWORD, + ) + + +@shared_task() +def mark_inactive_property(): + from bids.models import Auction + from properties.models import Property + + properties = Property.objects.filter(is_active=True, closing_date__lte=now()) + email_subject = f"Bidder | Congratulations" + buyer_message = """Hey, +Congratulations, you win the auction +Property Details: + Name: {property.name} + Seller: {property.seller.full_name} + Ask Price: {property.base_price} + Auction Start Date: {property.start_date} + Auction Closing Date: {property.closing_date} + Location: {property.location} + +Contact the seller for more information: + Seller email: {property.seller.email} + + +Best regards, +Team Bidder +""" + seller_message = """Hey, +Congratulations, you auction is closed. + +Contact the buyer for more information: + Buyer email: {bid.buyer.email} + Amount: {bid.amount} + + +Best regards, +Team Bidder +""" + + properties.update(is_active=False) + for property in properties: + bid = Auction.objects.filter(is_active=True).order_by("-amount").first() + if bid: + send_mail( + subject=email_subject, + message=seller_message.format(bid=bid), + from_email="noreply@bidder.com", + auth_user=settings.EMAIL_HOST_USER, + auth_password=settings.EMAIL_HOST_PASSWORD, + ) + send_mail( + subject=email_subject, + message=buyer_message.format(property=property), + from_email="noreply@bidder.com", + auth_user=settings.EMAIL_HOST_USER, + auth_password=settings.EMAIL_HOST_PASSWORD, + ) diff --git a/properties/tests.py b/properties/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/properties/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/properties/urls.py b/properties/urls.py new file mode 100644 index 0000000..5fefc0f --- /dev/null +++ b/properties/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from properties.views import ListRunningAuctionsAPI + +urlpatterns = [ + path( + "running-auctions/", + ListRunningAuctionsAPI.get, + name="list_running_auctions", + ), +] diff --git a/properties/views.py b/properties/views.py new file mode 100644 index 0000000..3e541dd --- /dev/null +++ b/properties/views.py @@ -0,0 +1,47 @@ +from django.shortcuts import render +from django.utils.timezone import now +from rest_framework.response import Response +from rest_framework.views import APIView + +from properties.models import Property +from properties.serializers import ( + PropertyDocumentSerializer, + PropertyGallerySerializer, + PropertySerializer, +) +from users.serializers import UserSerializer + + +class ListRunningAuctionsAPI(APIView): + class OutputSerializer(PropertySerializer): + seller = UserSerializer() + gallery = PropertyGallerySerializer(source="propertygallery_set", many=True) + documents = PropertyDocumentSerializer(source="propertydocument_set", many=True) + + class Meta: + model = Property + fields = ( + "id", + "name", + "seller", + "location", + "base_price", + "start_date", + "closing_date", + "gallery", + "documents", + ) + + def get(self, request): + current_time = now() + properties = ( + Property.objects.filter( + is_active=True, + start_date__lte=current_time, + closing_date__gte=current_time, + ) + .select_related("seller") + .prefetch_related("propertygallery_set", "propertydocument_set") + ) + serializer = self.OutputSerializer(properties, many=True) + return render(request, "property.html", serializer.data) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..c60a2fb Binary files /dev/null and b/requirements.txt differ diff --git a/static/20943399.svg b/static/20943399.svg new file mode 100644 index 0000000..10b09ac --- /dev/null +++ b/static/20943399.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/static/KeshavPic.jpg b/static/KeshavPic.jpg new file mode 100644 index 0000000..52f2449 Binary files /dev/null and b/static/KeshavPic.jpg differ diff --git a/static/about achievements.svg b/static/about achievements.svg new file mode 100644 index 0000000..901c3b0 --- /dev/null +++ b/static/about achievements.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/bidder 15.jpg b/static/bidder 15.jpg new file mode 100644 index 0000000..4d69748 Binary files /dev/null and b/static/bidder 15.jpg differ diff --git a/static/bidder 16.jpg b/static/bidder 16.jpg new file mode 100644 index 0000000..9f09cce Binary files /dev/null and b/static/bidder 16.jpg differ diff --git a/static/bidder 17.jpg b/static/bidder 17.jpg new file mode 100644 index 0000000..a9af0d7 Binary files /dev/null and b/static/bidder 17.jpg differ diff --git a/static/bidder 18.jpg b/static/bidder 18.jpg new file mode 100644 index 0000000..73a6c18 Binary files /dev/null and b/static/bidder 18.jpg differ diff --git a/static/bidder1.avif b/static/bidder1.avif new file mode 100644 index 0000000..f78871f Binary files /dev/null and b/static/bidder1.avif differ diff --git a/static/bidder2.jpg b/static/bidder2.jpg new file mode 100644 index 0000000..2e242df Binary files /dev/null and b/static/bidder2.jpg differ diff --git a/static/bidder3.jpg b/static/bidder3.jpg new file mode 100644 index 0000000..791673e Binary files /dev/null and b/static/bidder3.jpg differ diff --git a/static/bidder4.svg b/static/bidder4.svg new file mode 100644 index 0000000..4c3333b --- /dev/null +++ b/static/bidder4.svg @@ -0,0 +1,98 @@ + + + + + + + + + diff --git a/static/bidder5.svg b/static/bidder5.svg new file mode 100644 index 0000000..e304f3f --- /dev/null +++ b/static/bidder5.svg @@ -0,0 +1,282 @@ + + + + + + + + + + + + + + + + + + + diff --git a/static/bidder6.svg b/static/bidder6.svg new file mode 100644 index 0000000..bf08cf3 --- /dev/null +++ b/static/bidder6.svg @@ -0,0 +1,4917 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/bidder8.svg b/static/bidder8.svg new file mode 100644 index 0000000..4a93794 --- /dev/null +++ b/static/bidder8.svg @@ -0,0 +1,2309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/contact.svg b/static/contact.svg new file mode 100644 index 0000000..93eb6e2 --- /dev/null +++ b/static/contact.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/css/about.css b/static/css/about.css new file mode 100644 index 0000000..fca6eb0 --- /dev/null +++ b/static/css/about.css @@ -0,0 +1,204 @@ +/* =============================ACHIEVEMENTS ================================*/ + + +:root{ + --color-primary: #6c63ff; + --color-success:#00bf8e; + --color-warning: #f7c94b; + --color-danger: #f75842; + --color-danger-variant:rgba(247,88,66,0.4); + --color-white:#fff; + --color-light:rgba(255, 255, 255, 0.7); + --color-black: #000; + --color-bg:#1f2641; + --color-bg1: #2e3267; + --color-bg2: #424890; + + --container-width-lg: 76%; + --container-width-md: 90%; + --container-width-sm: 94%; + + --transition: all 400ms ease; + +} + +.about__achievements { + margin-top: 3rem; +} + +.about__achievements-container { + display: grid; + grid-template-columns: 40% 60%; + gap: 5rem; +} + +.about__achievements-right > p { + margin: 1.6rem 0 2.5rem; +} + +.achievements__cards { + display: grid; + grid-template-columns: repeat(3 , 1fr); + gap: 1.5rem; +} + + +.achievement__card { + background: var(--color-bg1); + padding: 1.6rem; + border-radius: 1rem; + text-align: center; + transition: var(--transition); +} + +.achievement__card:hover { + background: var(--color-bg2); + box-shadow: 0 3rem 3rem rgba(0, 0, 0, 0.3); +} + +.achievement__icon { + background: var(--color-danger); + padding: 0.6rem; + border-radius: 1rem; + display: inline-block; + margin-bottom: 2rem; + font-size: 2rem; +} + + +.achievement__card:nth-child(2) .achievement__icon { + background: var(--color-success); +} + +.achievement__card:nth-child(3) .achievement__icon { + background: var(--color-primary); +} + + +.achievement__card p { + margin-top: 1rem; +} + + +/* ==========================TEAM ========================= */ + + +.team { + background: var(--color-bg1); + box-shadow: inset 0 0 3rem rgba(0, 0, 0, 0.5); + +} + +.team__container { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 2rem; + +} + +.team__member { + background: var(--color-bg2); + padding: 2rem; + border: 1px solid transparent; + transition: var(--transition); + position: relative; + overflow: hidden; +} + +.team__member:hover { + background: transparent; + border-color: var(--color-primary); +} + +.team__member-image img { + filter: saturate(0); +} + +.team__member:hover img { + filter: saturate(1); + +} + +.team__member-info * { + text-align: center; + margin-top: 1.4rem; +} + +.team__member-info p { + color: var(--color-light); +} + +.team__member_socials { + position: absolute; + top: 50% ; + transform: translateY(-50%); + right: -100%; + display: flex; + flex-direction: column; + background: var(--color-primary); + border-radius: 1rem 0 0 1rem; + box-shadow: -2rem 0 2rem rgba(0, 0, 0, 0.3); + transition: var(--transition); + +} + +.team__member:hover .team__member_socials { + right: 0; +} + +.team__member-socials a { + padding: 1rem; +} + + +/*======================== MEDIA QUERIES (TABLETS) ===================================== */ + + +@media screen and (max-width: 1024px) { + .about__achievements { + margin-top: 2rem; + } + + .about__achievements-container { + grid-template-columns: 1fr; + gap:4rem; + } + + .about__achievements-left { + width: 80%; + margin: 0 auto; + } + + .team__container{ + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + } + + .team__member{ + padding: 1rem; + } +} + +/*======================== MEDIA QUERIES (MOBILES) ===================================== */ + + + +@media screen and (max-width : 600px) { + .achievements__cards{ + grid-template-columns: 1fr 1fr; + gap: 0.7rem; + } + + .team__container { + grid-template-columns: 1fr 1fr; + gap:0.7rem; + } + + .team__member{ + padding: 0; + } + + .team__member p { + margin-bottom: 1.5rem; + } +} \ No newline at end of file diff --git a/static/css/contact.css b/static/css/contact.css new file mode 100644 index 0000000..97a7dd9 --- /dev/null +++ b/static/css/contact.css @@ -0,0 +1,144 @@ +.contact__container{ + background: var(--color-bg1); + padding: 4rem; + display: grid; + grid-template-columns: 40% 60%; + gap: 4rem; + height: 30rem; + margin: 7rem auto; + border-radius: 1rem; +} + +/* =============================ASIDE =========================================*/ +.contact__aside { + background: var(--color-primary); + padding: 3rem; + border-radius: 1rem; + position: relative; + bottom: 10rem; +} + +.aside__image { + width: 12rem; + margin-bottom: 2rem; + +} + +.contact__aside h2 { + text-align: left; + margin-bottom: 1rem; +} + +.contact__aside p{ + font-size: 0.9rem; + margin-bottom: 2rem; +} + +.contact__detals li { + display: flex; + gap: 1rem; + align-items: center; + margin-bottom: 1rem; + +} + +.contact__socials { + display: flex; + gap: 1rem; + margin-top: 3rem; +} + +.contact__socials a { + background: var(--color-bg2); + padding: 0.5rem; + border-radius: 50%; + font-size: 0.9rem; + transition: var(--transition); +} + +.contact__socials a:hover { + background: transparent; +} + +/*==============================================FORM========================================*/ + +.contact__form { + display: flex; + flex-direction: column; + gap: 1.2rem; + margin-right: 4rem; +} + +.form__name { + display: flex; + gap: 1.2rem; +} + +.contact__form input[type="text"] { + width: 50%; + +} + +input, textarea { + width: 100%; + padding: 1rem; + background: var(--color-bg); + color: var(--color-white); +} + +.contact__form .btn { + width: max-content; + margin-top: 1rem; + cursor: pointer; +} + + + +/*=================================MEDIA QUERIES(TABLETS)===================================*/ +@media screen and (max-width: 1024px) { + .contact { + padding-bottom: 0; + } + + .contact__container { + gap: 1.5rem; + margin-top: 3rem; + height: auto; + padding: 1.5rem; + } + + .contact__aside { + width: auto; + padding: 1.5rem; + bottom: 0; + } + + .contact__form { + align-self: center; + margin-right: 1.5rem; + } +} + + +/*================================MEDIA QUERIES (PHONES) ==================================*/ +@media screen and (max-width : 600px) { + .contact__container { + grid-template-columns: 1fr; + gap: 3rem; + margin-top: 0; + padding: 0; + } + + .contact__form{ + margin: 0 1.5rem 3rem; + } + + .form__name{ + flex-direction: column; + } + + .form__name input[type= "text"] { + width: 100%; + + } +} \ No newline at end of file diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..e7cdeeb --- /dev/null +++ b/static/css/login.css @@ -0,0 +1,54 @@ +h1 { + color: red; + text-align: center; + padding: 6rem; +} + + /* Center the login form horizontally */ + .login-form { + margin: 0 auto; + width: 90%; + max-width: 400px; + } + + /* Style the form input fields */ + .login-form input[type=text], + .login-form input[type=password] { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + display: inline-block; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + } + + /* Style the login button */ + .login-form input[type=submit] { + background-color: #4CAF50; + color: white; + padding: 14px 20px; + margin: 8px 0; + border: none; + border-radius: 4px; + cursor: pointer; + width: 100%; + } + + /* Style the "Sign up" link */ + .login-form p { + text-align: center; + font-size: 14px; + } + + .login-form p a { + color: #4CAF50; + text-decoration: none; + } + + /* Media queries for tablet and mobile devices */ + @media screen and (max-width: 768px) { + .login-form { + width: 100%; + } + } \ No newline at end of file diff --git a/static/css/registration.css b/static/css/registration.css new file mode 100644 index 0000000..a954b58 --- /dev/null +++ b/static/css/registration.css @@ -0,0 +1,70 @@ +body { + font-family: Arial, sans-serif; +} + +h1 { + color: red; + text-align: center; + padding: 4rem; +} + +form { + width: 50%; + margin: 0 auto; +} + +label { + display: block; + margin-bottom: 10px; +} + +input[type="text"], +input[type="email"], +input[type="password"], +textarea { + width: 100%; + padding: 10px; + margin-bottom: 20px; + border: none; + border-radius: 5px; + box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3); +} + +input[type="submit"] { + background-color: #4CAF50; + color: #fff; + padding: 10px 20px; + border: none; + border-radius: 5px; + cursor: pointer; +} + +input[type="submit"]:hover { + background-color: #3e8e41; +} + + +.btn button{ + background-color:#04aa6d; + color: white; + width: 100%; + padding: 10px; + margin: 8px 0px; + border: none; +} +.btn button:hover{ + opacity: 1; +} + + +@media only screen and (min-width: 600px) { + form { + width: 70%; + } +} + +@media only screen and (min-width: 768px) { + form { + width: 50%; + } +} \ No newline at end of file diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..0ab385f --- /dev/null +++ b/static/css/style.css @@ -0,0 +1,618 @@ +* { + margin: 0; + padding: 0; + border: 0; + outline: 0; + text-decoration: none; + list-style: none; + box-sizing: border-box; + + +} +:root{ + --color-primary: #6c63ff; + --color-success:#00bf8e; + --color-warning: #f7c94b; + --color-danger: #f75842; + --color-danger-variant:rgba(247,88,66,0.4); + --color-white:#fff; + --color-light:rgba(255, 255, 255, 0.7); + --color-black: #000; + --color-bg:#1f2641; + --color-bg1: #2e3267; + --color-bg2: #424890; + + --container-width-lg: 76%; + --container-width-md: 90%; + --container-width-sm: 94%; + + --transition: all 400ms ease; + +} + +body{ + font-family: "Montserrat, sans-serif"; + line-height: 1.7; + color: var(--color-white); + background: var(--color-bg); + +} + +.container{ + width: var(--container-width-lg); + margin: 0 auto; +} + +section{ + padding: 6rem 0; + +} + +section h2{ + text-align: center; + margin-bottom: 4rem; + +} + +h1, +h2, +h3, +h4, +h5{ + line-height: 1.2; +} + +h1{ + font-size: 2.4rem; +} + +h2{ + font-size: 2rem; +} + +h3{ + font-size: 1.6rem; +} + +h4{ + font-size: 1.3rem; +} + +a{ + color: var(--color-white) +} + +img{ + width: 100%; + display: block; + object-fit: cover; +} + +.btn{ + display: inline-block; + background: var(--color-white); + color: var(--color-black); + padding: lrem 2rem; + border: 1px solid transparent; + font-weight: 500; + transition: var(--transition); + +} + +.btn:hover{ + + background: transparent; + color: var(--color-white); + border-color: var(--color-white); +} + +.btn-rimary{ + background: var(--color-danger); + color: var(--color-white); +} + +/*=====================NAVBAR====================== */ + +nav{ + background: transparent; + width: 100vw; + height: 5rem; + position: fixed; + top: 0; + z-index: 11; +} + +/* cahnge navbar styles on scroll using javascript */ +.window-scroll { + background: var(--color-primary); + box-shadow: 0 1rem 2rem rgba(0, 0, 0, 0.2); +} + +.nav__container{ + height: 100%; + display: flex; + justify-content: space-between; + align-items: center; + +} + +nav button{ + display: none; +} + +.nav__menu{ + display: flex; + align-items: center; + gap: 4rem; +} + +.nav__menu a{ + font-size: 0.9rem; + transition: var(--transition); +} + +.nav__menu a:hover{ + color: var(--color-bg2); +} + + +/* ========================================HEADER ===================================== */ + +header{ + position: relative; + top: 5rem; + overflow: hidden; + height: 70vh; + margin-bottom: 5rem; +} + +.header__container { + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + gap: 5rem; + height: 100%; +} + +.header__left p { + margin: 1rem 0 2.4rem; +} + + +header{ + position: relative; + top: 5rem; + overflow: hidden; + height: 70vh; + margin-bottom: 5rem; +} + +.header__container { + display: grid; + grid-template-columns: 1fr 1fr; + align-items: center; + gap: 5rem; + height: 100%; +} + +.header__left p { + margin: 1rem 0 2.4rem; +} + +/*===================================================PROPERTIES========================= */ +.categories{ + background: var(--color-bg1); + height: 40rem; +} + +.categories h1{ + line-height: 1; + margin-bottom: 3rem; +} + +.categories__container { + display: grid; + grid-template-columns: 40% 60%; + /*gap: 4rem;*/ +} + +.categories__right { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.2rem; +} + +.category{ + background: var(--color-bg2); + padding: 2rem; + border-radius: 2rem; + transition: var(--transition); +} + +.category:hover { + box-shadow: 0 3rem 3rem rgba(0, 0, 0, 0.3); + z-index: 1; +} + +.category:nth-child(2) .category__icon { + background: var(--color-danger); + +} + +.category:nth-child(3) .category__icon { + background: var(--color-success); + +} +.category:nth-child(4) .category__icon { + background: var(--color-warning); + +} + +.category:nth-child(5) .category__icon { + background: var(--color-success); + +} + + +.category__icon { + background: var(--color-primary); + padding: 0.7rem; + border-radius: 0.9rem; +} + +.category h5 { + margin: 2rem 0 1rem; +} + +.category p { + font-size: 0.85rem; + +} +/* =============================POPULAR PROPERTIES========================== */ + +.properties { + margin-top: 15rem; + +} + +.courses__container{ + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 2rem; +} + +.property { + background: var(--color-bg1); + text-align: center; + border: 1px solid transparent; + transition: var(--transition); + +} + +.property:hover { + background: transparent; + border-color: var(--color-primary); +} + +.property_info { + padding: 2rem; +} + +.property_info p { + margin: 1.2rem 0 2rem; + font-size: 0.9rem; + +} + + +/* =================================TESTIMONIAL ======================= */ + +.testimonials_container { + overflow: hidden; + position: relative; + margin-bottom: 5rem; + +} + +.testimonial { + padding-top: 2rem; +} +.avatar { + width: 6rem; + height: 6rem; + border-radius: 50%; + overflow: hidden; + margin: 0 auto 1rem; + border: 1rem solid var(--color-bg1); +} + +.testimonial__info { + text-align: center; +} + +.testimonial__body { + background: var(--color-primary); + padding: 2rem; + margin-top: 3rem; + position: relative; +} + +.testimonial__body::before { + content: ""; + display: block; + background: linear-gradient( + 125deg, + transparent, + var(--color-primary), + var(--color-primary), + var(--color-primary) + ); + width: 3rem; + height: 3rem; + position: absolute; + left: 50%; + top: -1.5rem; + transform: rotate(45deg); + +} + +/*===========================FOOTER============================ */ + +footer{ + background: var(--color-bg1); + padding-top: 5rem; + font-size: 0.9rem; +} + +.footer__container{ + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 5rem; +} + +.footer__container div h4 { + margin-bottom: 1.2rem; +} + +.footer__1 p { + margin: 0 0 2rem; +} + +footer ul li { + margin-bottom: 0.7rem; +} + +footer ul li a:hover{ + text-decoration: underline; +} + +.footer__socials{ + display: flex; + gap: 1rem; + font-size: 1.2rem; + margin-top: 2rem; +} + +.footer__copyright { + text-align: center; + margin-top: 4rem; + padding: 1.2rem 0; + border-top: 1px solid var(--color-bg2); + +} + +/* ================================MEDIA QUERIES (TABLETS) ==================================== */ + +@media screen and (max-width: 1024px){ + .container{ + width: var(--container-width-md); + } + + h1 { + font-size: 2.2rem; + } + + h2{ + font-size: 1.7rem; + } + + h3{ + font-size: 1.4rem; + } + + h4{ + font-size: 1.2rem; + } + + /*====================================NAVBAR============================ */ + nav button { + display: inline-block; + background: transparent; + font-size: 1.8rem; + color: var(--color-white); + cursor: pointer; + } + + nav button#close-menu-btn { + display: none; + } + + .nav__menu{ + position: fixed; + top: 5rem; + right: 5%; + height: fit-content; + width: 18rem; + flex-direction: column; + gap: 0; + display: none; + } + + .nav__menu li { + width: 100%; + height: 5.8rem; + animation: animateNavItems 400ms linear forwards; + transform-origin: top right; + } + + .nav__menu li:nth-child(2){ + animation-delay: 200ms; + } + .nav__menu li:nth-child(3){ + animation-delay: 400ms; + } + .nav__menu li:nth-child(4){ + animation-delay: 600ms; + } + + @keyframes animateNavItems{ + 0% { + transform: rotateZ(-90deg) rotateX(90deg) scale(0.1); + + } + 100% { + transform: rotateZ(0) rotateX(0) scale(1); + + } + } + + .nav__menu li a { + background: var(--color-primary); + box-shadow: -4rem 6 rem 10rem rgba(0, 0, 0, 0.6); + width: 100%; + height: 100%; + display: grid; + place-items: center; + } + + .nav__menu li a:hover { + background: var(--color-bg2); + color: var(--color-white); + } + + /*===================================================HEADER=====================================*/ + + header{ + height: 52vh; + margin-bottom: 4rem; + } + + .header__container{ + gap: 0; + padding-bottom: 3rem; + } + + /* ======================================CATEGORIES ===============================*/ + + .categories{ + height: auto; + } + + .categories__container{ + grid-template-columns: 1fr; + gap: 3rem; + } + .categories__left{ + margin-top: 0; + } + + + /*=============================POPULAR COURSES=====================================*/ + .courses { + margin-top: 0; + } + + .courses__container{ + grid-template-columns: 1fr 1fr; + } + + + /*=========================================FAQs===============================*/ + + .faqs__container{ + grid-template-columns: 1fr; + } + .faq{ + + padding: 1.5rem; + } + + /*==========================================FOOTER================================*/ + .footer__container{ + grid-template-columns: 1fr 1fr; + } +} + +/* ===============================MEDIA QUERIES (PHONES) ===============================*/ + +@media screen and (mas-width:600px){ + .container{ + width: var(--container-width-sm); + } + + /*=======================================NAVBAR================================*/ + + .nav__menu{ + right: 3%; + } + + + /*==================================HEADER===================================*/ + + header { + height: 100vh; + } + + .header__container{ + grid-template-columns: 1fr; + text-align: center; + margin-top: 0; + } + + .header__left p { + margin-bottom: 1.3rem; + } + + + /*=================================CATEGORIES==============================*/ + .categories__right { + grid-template-columns: 1fr 1fr; + } + + .category { + padding: 1rem; + border-radius: 1rem; + } + + .category__icon { + margin-top: 4px; + display: inline-block; + } + + /*=============================POPULAR COURSES ==================================*/ + + .courses__container{ + grid-template-columns: 1fr; + } + + /*==============================TESTIMONIALS===========================*/ + .testimonial__body{ + padding: 1.2rem; + } + + + /*==================================FOOTER========================*/ + .footer__container{ + grid-template-columns: 1fr; + text-align: center; + gap: 2rem; + } + + .footer__1 p { + margin: 1rem auto; + } + + .footer__socials { + justify-content: center; + } +} \ No newline at end of file diff --git a/static/my piccc.jpg b/static/my piccc.jpg new file mode 100644 index 0000000..7d2cac6 Binary files /dev/null and b/static/my piccc.jpg differ diff --git a/templates/about.html b/templates/about.html new file mode 100644 index 0000000..8cedb76 --- /dev/null +++ b/templates/about.html @@ -0,0 +1,219 @@ +{% load static %} + + + + + + + Bidder - about + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ +
+ + +
+

Achievements

+

+ Lorem ipsum dolor sit amet, consectetur adipisicing elit. Doloribus provident tempore deserunt voluptatem perferendis error sed et, aspernatur alias libero minima placeat? Necessitatibus deleniti asperiores vel inventore provident reiciendis delectus. +

+ +
+
+ + + +

45

+

Property view

+
+ +
+ + + +

790+

+

People

+
+ + +
+ + + +

2

+

Awards

+
+
+
+
+
+ + + + + + + + + + + + + + + + + +
+

Meet Our Team

+
+
+
+ +
+
+

Raja Babu

+

Software Developer

+
+
+ + + +
+
+ + +
+
+ +
+
+

Keshav Mohta

+

Software Developer

+
+
+ + + +
+
+ + + +
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/templates/contact.html b/templates/contact.html new file mode 100644 index 0000000..a607c13 --- /dev/null +++ b/templates/contact.html @@ -0,0 +1,202 @@ +{% load static %} + + + + + + + Bidder - contact + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + + + +
+
+ + +
+ + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..a677c98 --- /dev/null +++ b/templates/index.html @@ -0,0 +1,291 @@ +{% load static %} + + + + + + + Online Auction Management System Using HTML, CSS & JavaScript + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

Online Auction Management System

+

+ + An online auction project that holds online auctions of various products on a website and serves sellers and bidders accordingly. The system is designed to allow users to set up their products for auctions and bidders to register and bid for various products available for bidding. + +

+ Start Bidder + +
+ +
+
+ +
+ +
+
+
+ + + +
+
+
+

Properties

+

+ Lorem ipsum dolor, sit amet consectetur adipisicing elit. Natus sed cum dolores, dicta quas deserunt, nulla nostrum minima voluptatibus distinctio delectus. Dicta quos consequatur voluptates magnam, ex reiciendis nobis consectetur. +

+ More Properties +
+ +
+
+ + +
Unmoderised house and flats
+

+ We’re often asked whether it’s a good idea to carry out improvement work to an unmodernised property, or to sell as is. + +

+
+ +
+ + +
Standard residential Properties
+

Standard construction properties are those constructed with brick and/or block walls under a tiled pitched roof sat on concrete foundations. + +

+
+ +
+ + +
Poor Condition Properties
+

There will be people who see potential in the property and it’s important to open the property up to the widest possible audience.

+
+ +
+ +
Land Properties
+

Service land who is betting for sell.

+
+ +
+ + +
Building Properties
+

Standard Building are also for auction to bet.

+
+ + +
+ + +
Plots properties
+

+ Small Plots are betted to sell. + +

+
+ +
+
+
+ + + + + + + + + + + + + +
+

Our Popular Properties

+
+
+
+ +
+
+

Standard Residential Properties

+

+ Properties are those constructed with brick and/or block walls +


+

Price: 100K

+
+
+ + +
+
+ +
+
+

Land Plots

+

+ Small $ Big Land Plots + + +



+

Price: 500K

+ +
+
+ +
+
+ +
+
+

Poor Condition Properties

+

+ Poor type of Building or Houses. +



+ +

Price: 500K

+ +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/templates/login.html b/templates/login.html new file mode 100644 index 0000000..99757e7 --- /dev/null +++ b/templates/login.html @@ -0,0 +1,130 @@ +{% load static %} + + + + + + + + Bidder - login + + + + + + + + + + + + + + + + + + + + +
+

Login

+
+ {% csrf_token %} + +

+ +

+ +
+ +

Don't have an account? Sign up

+
+ + + + + + + + + + + \ No newline at end of file diff --git a/templates/main.js b/templates/main.js new file mode 100644 index 0000000..c933f27 --- /dev/null +++ b/templates/main.js @@ -0,0 +1,47 @@ +// change navbar stykes on scroll + +window.addEventListener('scroll', ()=>{document.querySelector('nav').classList.toggle('window-scroll', window.scrollY > 0 )}) + +// show/hide faq answer + +const faqs = document.querySelectorAll('.faq'); + +faqs.forEach(faq => { + faq.addEventListener('click', () => { + faq.classList.toggle('open'); + + //change icon + const icon = feq.querySelector('.faq__icon i'); + if(icon.className === 'uil uil-plus') { + icon.className = "uil uil-minus"; + } else{ + icon.className = "uil uil-plus"; + } + }) +}) + + + +//show/hide nav menu + +const menu = document.querySelector(".nav__menu"); +const menuBtn = document.querySelector("#open-menu-btn"); +const closeBtn = document.querySelector("#close-menu-btn"); + + +menuBtn.addEventListener('click', () => { + menu.style.display = "flex"; + closeBtn.style.display= "inline-block"; + menuBtn.style.display = "none"; + +}) + +//close nav menu +const closeNav = () => { + menu.style.display = "none"; + closeBtn.style.display= "none"; + menuBtn.style.display = "inline-block"; + +} + +closeBtn.addEventListener('click', closeNav) diff --git a/templates/property.html b/templates/property.html new file mode 100644 index 0000000..e69de29 diff --git a/templates/registration.html b/templates/registration.html new file mode 100644 index 0000000..6a03c18 --- /dev/null +++ b/templates/registration.html @@ -0,0 +1,194 @@ + + + + + + + Online Auction Management System Using HTML, CSS & JavaScript + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

User Registeration Form

+

Please fill in the form to create an Bidder Account


+
+
+ + +
+
+ + +
+ + +
+
+ +
+ + +
+
+ + +
+ + +
+
+ + +
+ + +
+
+ + +
+ +
+
+
+ + +
+ +
+
+
+ +
+ +
+
+
+ +
+

By creating an account you agree to our Terms & Conditions

+
+ + +
+ +
+
+
+ + + + + + + \ No newline at end of file diff --git a/users/__init__.py b/users/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/__pycache__/__init__.cpython-39.pyc b/users/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..f081f0b Binary files /dev/null and b/users/__pycache__/__init__.cpython-39.pyc differ diff --git a/users/__pycache__/admin.cpython-39.pyc b/users/__pycache__/admin.cpython-39.pyc new file mode 100644 index 0000000..06a94c4 Binary files /dev/null and b/users/__pycache__/admin.cpython-39.pyc differ diff --git a/users/__pycache__/apps.cpython-39.pyc b/users/__pycache__/apps.cpython-39.pyc new file mode 100644 index 0000000..08aef06 Binary files /dev/null and b/users/__pycache__/apps.cpython-39.pyc differ diff --git a/users/__pycache__/models.cpython-39.pyc b/users/__pycache__/models.cpython-39.pyc new file mode 100644 index 0000000..3dcde0a Binary files /dev/null and b/users/__pycache__/models.cpython-39.pyc differ diff --git a/users/__pycache__/serializers.cpython-39.pyc b/users/__pycache__/serializers.cpython-39.pyc new file mode 100644 index 0000000..94d75df Binary files /dev/null and b/users/__pycache__/serializers.cpython-39.pyc differ diff --git a/users/__pycache__/signals.cpython-39.pyc b/users/__pycache__/signals.cpython-39.pyc new file mode 100644 index 0000000..5a774d9 Binary files /dev/null and b/users/__pycache__/signals.cpython-39.pyc differ diff --git a/users/__pycache__/urls.cpython-39.pyc b/users/__pycache__/urls.cpython-39.pyc new file mode 100644 index 0000000..be4d4ac Binary files /dev/null and b/users/__pycache__/urls.cpython-39.pyc differ diff --git a/users/__pycache__/views.cpython-39.pyc b/users/__pycache__/views.cpython-39.pyc new file mode 100644 index 0000000..5e7b804 Binary files /dev/null and b/users/__pycache__/views.cpython-39.pyc differ diff --git a/users/admin.py b/users/admin.py new file mode 100644 index 0000000..25b920b --- /dev/null +++ b/users/admin.py @@ -0,0 +1,18 @@ +from django.contrib import admin +from django.contrib.admin import ModelAdmin + +from users.models import Profile, User + + +@admin.register(User) +class UserAdmin(ModelAdmin): + list_display = ("id", "email", "is_seller", "is_buyer") + list_filter = ("is_seller", "is_buyer") + search_fields = ("email", "first_name", "last_name") + + +@admin.register(Profile) +class ProfileAdmin(ModelAdmin): + list_display = ("id", "user", "pan_card", "aadhar_card") + list_filter = ("user__is_seller", "user__is_buyer") + search_fields = ("user__email", "aadhar_card", "pan_card", "address") diff --git a/users/apps.py b/users/apps.py new file mode 100644 index 0000000..ce49c45 --- /dev/null +++ b/users/apps.py @@ -0,0 +1,11 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "users" + + def ready(self) -> None: + from . import signals + + return super().ready() diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py new file mode 100644 index 0000000..9f04b4d --- /dev/null +++ b/users/migrations/0001_initial.py @@ -0,0 +1,99 @@ +# Generated by Django 4.1.7 on 2023-02-28 17:05 + +import django.contrib.auth.models +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", + ), + ), + ( + "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" + ), + ), + ("email", models.EmailField(max_length=256, unique=True)), + ("first_name", models.CharField(max_length=256)), + ("last_name", models.CharField(blank=True, max_length=256, null=True)), + ( + "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", + }, + managers=[ + ("objects", django.contrib.auth.models.UserManager()), + ], + ), + ] diff --git a/users/migrations/0002_alter_user_managers_user_is_buyer_user_is_seller_and_more.py b/users/migrations/0002_alter_user_managers_user_is_buyer_user_is_seller_and_more.py new file mode 100644 index 0000000..fa8dce0 --- /dev/null +++ b/users/migrations/0002_alter_user_managers_user_is_buyer_user_is_seller_and_more.py @@ -0,0 +1,64 @@ +# Generated by Django 4.1.7 on 2023-03-20 18:43 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0001_initial"), + ] + + operations = [ + migrations.AlterModelManagers( + name="user", + managers=[], + ), + migrations.AddField( + model_name="user", + name="is_buyer", + field=models.BooleanField(default=True), + ), + migrations.AddField( + model_name="user", + name="is_seller", + field=models.BooleanField(default=False), + ), + migrations.CreateModel( + name="Profile", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "photo", + models.ImageField( + blank=True, + null=True, + upload_to="profile_pics/{instance.user.email}.jpg", + ), + ), + ("pan_card", models.CharField(blank=True, max_length=10, null=True)), + ("aadhar_card", models.CharField(blank=True, max_length=12, null=True)), + ("address", models.TextField()), + ( + "user", + models.OneToOneField( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "verbose_name": "Profile", + "verbose_name_plural": "Profiles", + }, + ), + ] diff --git a/users/migrations/0003_alter_profile_photo.py b/users/migrations/0003_alter_profile_photo.py new file mode 100644 index 0000000..e7256ad --- /dev/null +++ b/users/migrations/0003_alter_profile_photo.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-04-06 10:19 + +from django.db import migrations, models +import helpers.files + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0002_alter_user_managers_user_is_buyer_user_is_seller_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="photo", + field=models.ImageField( + blank=True, + null=True, + upload_to=helpers.files.RenameFile( + "profile_pics/{instance.user.email}.{extension}" + ), + ), + ), + ] diff --git a/users/migrations/0004_alter_profile_photo.py b/users/migrations/0004_alter_profile_photo.py new file mode 100644 index 0000000..9f8d449 --- /dev/null +++ b/users/migrations/0004_alter_profile_photo.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.7 on 2023-04-06 10:22 + +from django.db import migrations, models +import helpers.files + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0003_alter_profile_photo"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="photo", + field=models.ImageField( + blank=True, + null=True, + upload_to=helpers.files.RenameFile( + "users/profile_pics/{instance.user.email}.{extension}" + ), + ), + ), + ] diff --git a/users/migrations/0005_alter_profile_photo.py b/users/migrations/0005_alter_profile_photo.py new file mode 100644 index 0000000..88d424c --- /dev/null +++ b/users/migrations/0005_alter_profile_photo.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.7 on 2023-04-15 10:52 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0004_alter_profile_photo"), + ] + + operations = [ + migrations.AlterField( + model_name="profile", + name="photo", + field=models.ImageField(blank=True, null=True, upload_to=""), + ), + ] diff --git a/users/migrations/__init__.py b/users/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/users/migrations/__pycache__/0001_initial.cpython-39.pyc b/users/migrations/__pycache__/0001_initial.cpython-39.pyc new file mode 100644 index 0000000..c7ebd85 Binary files /dev/null and b/users/migrations/__pycache__/0001_initial.cpython-39.pyc differ diff --git a/users/migrations/__pycache__/0002_alter_user_managers_user_is_buyer_user_is_seller_and_more.cpython-39.pyc b/users/migrations/__pycache__/0002_alter_user_managers_user_is_buyer_user_is_seller_and_more.cpython-39.pyc new file mode 100644 index 0000000..8121b0b Binary files /dev/null and b/users/migrations/__pycache__/0002_alter_user_managers_user_is_buyer_user_is_seller_and_more.cpython-39.pyc differ diff --git a/users/migrations/__pycache__/0003_alter_profile_photo.cpython-39.pyc b/users/migrations/__pycache__/0003_alter_profile_photo.cpython-39.pyc new file mode 100644 index 0000000..2b58457 Binary files /dev/null and b/users/migrations/__pycache__/0003_alter_profile_photo.cpython-39.pyc differ diff --git a/users/migrations/__pycache__/0004_alter_profile_photo.cpython-39.pyc b/users/migrations/__pycache__/0004_alter_profile_photo.cpython-39.pyc new file mode 100644 index 0000000..ca70f40 Binary files /dev/null and b/users/migrations/__pycache__/0004_alter_profile_photo.cpython-39.pyc differ diff --git a/users/migrations/__pycache__/0005_alter_profile_photo.cpython-39.pyc b/users/migrations/__pycache__/0005_alter_profile_photo.cpython-39.pyc new file mode 100644 index 0000000..2e703fe Binary files /dev/null and b/users/migrations/__pycache__/0005_alter_profile_photo.cpython-39.pyc differ diff --git a/users/migrations/__pycache__/__init__.cpython-39.pyc b/users/migrations/__pycache__/__init__.cpython-39.pyc new file mode 100644 index 0000000..e590738 Binary files /dev/null and b/users/migrations/__pycache__/__init__.cpython-39.pyc differ diff --git a/users/models.py b/users/models.py new file mode 100644 index 0000000..85f501d --- /dev/null +++ b/users/models.py @@ -0,0 +1,86 @@ +from django.contrib.auth.base_user import BaseUserManager +from django.contrib.auth.models import AbstractUser +from django.core.exceptions import ValidationError +from django.core.validators import FileExtensionValidator +from django.db import models + +from helpers.files import ValidateFileSize + + +class UserManager(BaseUserManager): + def create_user(self, email, first_name, password, **kwargs): + email = self.normalize_email(email=email) + user = self.model(email=email, first_name=first_name, **kwargs) + user.set_password(password) + user.save() + return user + + def create_superuser(self, email, first_name, **kwargs): + kwargs.setdefault("is_staff", True) + kwargs.setdefault("is_superuser", True) + kwargs.setdefault("is_active", True) + return self.create_user(email=email, first_name=first_name, **kwargs) + + +class User(AbstractUser): + """ + This model is used to store user details. + """ + + username = None + email = models.EmailField(max_length=256, unique=True) + first_name = models.CharField(max_length=256) + last_name = models.CharField(max_length=256, null=True, blank=True) + is_seller = models.BooleanField(default=False) + is_buyer = models.BooleanField(default=False) + + USERNAME_FIELD = "email" + REQUIRED_FIELDS = ["first_name"] + + objects = UserManager() + + class Meta: + verbose_name = "User" + verbose_name_plural = "Users" + + def __str__(self): + return self.email + + @property + def full_name(self): + return (self.first_name.strip() + " " + (self.last_name or "").strip()).rstrip() + + def validate_user_type(self): + if not (self.is_seller or self.is_buyer): + raise ValidationError("User must be seller or buyer.") + + def clean(self): + self.validate_user_type() + + +class Profile(models.Model): + MAX_FILE_SIZE_ALLOWED = 5 # MB + EXTENSIONS_ALLOWED = ("jpeg", "png", "jpg") + + user = models.OneToOneField(User, on_delete=models.CASCADE) + photo = models.ImageField( + validators=[ + ValidateFileSize(max_file_size=MAX_FILE_SIZE_ALLOWED), + FileExtensionValidator(allowed_extensions=EXTENSIONS_ALLOWED), + ], + null=True, + blank=True, + ) + pan_card = models.CharField(null=True, blank=True, max_length=10) + aadhar_card = models.CharField(null=True, blank=True, max_length=12) + address = models.TextField() + + def __str__(self): + return f"{self.user.email}'s Profile" + + def clean(self) -> None: + return super().clean() + + class Meta: + verbose_name = "Profile" + verbose_name_plural = "Profiles" diff --git a/users/profile_pics/keshavastro.com.png b/users/profile_pics/keshavastro.com.png new file mode 100644 index 0000000..99d62dc Binary files /dev/null and b/users/profile_pics/keshavastro.com.png differ diff --git a/users/profile_pics/maheshastro.com.png b/users/profile_pics/maheshastro.com.png new file mode 100644 index 0000000..36f7c3e Binary files /dev/null and b/users/profile_pics/maheshastro.com.png differ diff --git a/users/profile_pics/rahulastro.com.png b/users/profile_pics/rahulastro.com.png new file mode 100644 index 0000000..38c3eb5 Binary files /dev/null and b/users/profile_pics/rahulastro.com.png differ diff --git a/users/serializers.py b/users/serializers.py new file mode 100644 index 0000000..8ef658a --- /dev/null +++ b/users/serializers.py @@ -0,0 +1,8 @@ +from rest_framework import serializers +from users.models import User + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ("email", "first_name", "last_name") diff --git a/users/signals.py b/users/signals.py new file mode 100644 index 0000000..5a8f1a4 --- /dev/null +++ b/users/signals.py @@ -0,0 +1,10 @@ +from django.conf import settings +from django.db.models.signals import post_save +from django.dispatch import receiver +from rest_framework.authtoken.models import Token + + +@receiver(post_save, sender=settings.AUTH_USER_MODEL) +def create_auth_token(instance, created=False, **kwargs): + if created: + Token.objects.create(user=instance) 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..6b506ad --- /dev/null +++ b/users/urls.py @@ -0,0 +1,19 @@ +from django.urls import path +from rest_framework.authtoken import views as auth_views + +from users import views + +urlpatterns = [ + path("create/", views.UserAPI.as_view({"post": "create"}), name="user-create"), + path("update/", views.UserAPI.as_view({"put": "update"}), name="user-update"), + path( + "login/", + views.UserAuthenticationAPI().login, + name="user-login", + ), + path( + "logout/", + views.UserAuthenticationAPI.as_view({"delete": "logout"}), + name="user-logout", + ), +] diff --git a/users/views.py b/users/views.py new file mode 100644 index 0000000..632aa48 --- /dev/null +++ b/users/views.py @@ -0,0 +1,125 @@ +from django.contrib.auth.password_validation import validate_password +from django.core.exceptions import ValidationError +from django.db import IntegrityError +from rest_framework import serializers +from rest_framework.authtoken.models import Token +from rest_framework.decorators import permission_classes +from rest_framework.permissions import IsAuthenticated +from rest_framework.response import Response +from rest_framework.viewsets import ViewSet + +from users.models import User +from users.serializers import UserSerializer + + +class UserAPI(ViewSet): + class InputSerializer(serializers.Serializer): + email = serializers.EmailField() + first_name = serializers.CharField() + last_name = serializers.CharField(required=False) + password = serializers.CharField() + confirm_password = serializers.CharField() + + def validate(self, attrs): + if bool(attrs.get("password")) ^ bool(attrs.get("confirm_password")): + raise serializers.ValidationError( + "Confirm password is required when password is provided." + ) + + if attrs.get("password") != attrs.get("confirm_password"): + raise serializers.ValidationError( + "Password and confirm password do not match." + ) + validate_password(attrs["password"]) + return attrs + + class OutputSerializer(UserSerializer): + message = serializers.CharField() + + class Meta: + model = User + fields = ("email", "first_name", "last_name", "message") + + def create(self, request, *args, **kwargs): + data = request.data + serializer = self.InputSerializer(data=data) + if not serializer.is_valid(): + return Response(serializer.errors, status=400) + + validated_data = serializer.validated_data + validated_data.pop("confirm_password") + try: + user = User.objects.create_user(**validated_data) + except (ValidationError, IntegrityError) as error: + return Response(str(error), status=400) + user.message = "Congratulations, You are registered." + return Response(self.OutputSerializer(instance=user).data, status=201) + + def update(self, request, *args, **kwargs): + data = request.data + if "email" in data: + return Response( + data="To update the email you have to send a update request at care@bidders.com", + status=400, + ) + serializer = self.InputSerializer(data=data, partial=True) + if not serializer.is_valid(): + return Response(serializer.errors, status=400) + + validated_data = serializer.validated_data + validated_data.pop("confirm_password") + user = request.user + for field, value in validated_data.items(): + setattr(user, field, value) + try: + user.save() + except (ValidationError, IntegrityError) as error: + return Response(str(error), status=400) + user.message = "User updated successfully." + + return Response(self.OutputSerializer(instance=user).data, status=201) + + +from django.shortcuts import render + + +class UserAuthenticationAPI(ViewSet): + class InputSerializer(serializers.Serializer): + email = serializers.EmailField() + password = serializers.CharField() + + class OutputSerializer(UserSerializer): + class Meta: + model = User + fields = ("email", "first_name", "last_name") + + def login(self, request, *args, **kwargs): + if request.method == "GET": + return render(request, "login.html") + data = request.POST.data + serializer = self.InputSerializer(data=data) + if not serializer.is_valid(): + return Response(serializer.errors, status=400) + + validated_data = serializer.validated_data + email = validated_data["email"] + password = validated_data["password"] + try: + user = User.objects.get(email=email) + if not user.check_password(password): + raise User.DoesNotExist + except User.DoesNotExist: + return Response("User not found. Please register yourself before login.") + token, _created = Token.objects.get_or_create(user=user) + response = { + "token": token.key, + "payload": self.OutputSerializer(instance=user).data, + } + return Response(response, status=200) + + @permission_classes(IsAuthenticated) + def logout(self, request, *args, **kwargs): + token = Token.objects.get(user=request.user) + token.delete() + + return Response(status=204)