OYO Clone: A Complete Django Hotel Booking System

Last Updated : 5 Jan, 2026

The OYO Clone Django Project aims to recreate the core features of the OYO hotel booking platform. It helps users search, book, and manage hotel stays through a smooth and scalable web application built with Django.

  • User Authentication: Secure account creation and login with email and OTP verification for added safety.
  • Hotel Listings: Displays a detailed list of hotels with location, amenities, room types, and pricing. Users can filter results by location, price, rating, and more.
  • Booking Management: Allows users to select room types, choose booking dates, add extra services, and complete the reservation easily.
  • Payment Processing: Supports secure online payments using cards, mobile wallets, or payment gateways, with encrypted transactions for privacy.
  • Confirmation & Notifications: Sends instant booking confirmation via email or SMS along with updates such as payment status, check-in details, and reminders.

This project supports two types of users, each with different roles and permissions for interacting with the system.

  • Regular Users (HotelUser): Can browse, search, filter, and book hotels.
  • Vendors (HotelVendor): Can register, manage hotel details, upload images, and view bookings.

Step 1: Environment Setup

Create a directory for your project and set up a virtual environment:

mkdir oyo_clone_project
cd oyo_clone_project
python -m venv venv

Activate Virtual Environment:

On Windows:

venv\Scripts\activate

On macOS/Linux:

source venv/bin/activate

Install Required Packages:

pip install django==5.0.7
pip install python-decouple
pip install django-debug-toolbar

Step 2: Creating Django Project

Once Django is installed, create a new Django project:

django-admin startproject oyo_clone
cd oyo_clone

Test if the project was created successfully:

python manage.py runserver

Visit http://127.0.0.1:8000/ to check Django welcome page.

Step 3: Creating Django Apps

Our project will have two main apps:

Create Accounts App

This app handles all authentication and user-related functionality:

python manage.py startapp accounts

Create Home App

This app handles the main website functionality:

python manage.py startapp home

Step 4: Project Configuration

Open oyo_clone/settings.py and configure the following:

INSTALLED_APPS: Add your apps and debug toolbar:

Python
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'home',
    'accounts',
    'debug_toolbar',
]

MIDDLEWARE: Add debug toolbar middleware at the top:

Python
MIDDLEWARE = [
    "debug_toolbar.middleware.DebugToolbarMiddleware",
    # ... other middleware
]

Static and Media Files Configuration:

Python
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
STATICFILES_DIRS = [
    os.path.join(BASE_DIR, 'static'),
]

MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')

Email Configuration (for email verification):

Python
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = config("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = config("EMAIL_HOST_PASSWORD")

Create a .env file in the project root with your email credentials:

EMAIL_HOST_USER=your_email@gmail.com
EMAIL_HOST_PASSWORD=your_app_password

You can get your own email credentials by enabling 2-Step Verification on your Gmail account and generating an App Password to use in the project's email settings.

project-folder-structure
Folder Structure

Step 5: Database Models Design

Let's design our database schema. Open accounts/models.py and create the following models:

User Models

HotelUser: Extends Django's User model for regular customers

  • Additional fields: profile_picture, phone_number, email_token, otp, is_verified
  • Used for customer authentication and bookings

HotelVendor: Extends Django's User model for hotel owners

  • Additional fields: phone_number, profile_picture, email_token, otp, business_name, is_verified
  • Used for vendor authentication and hotel management

Core Models

Ameneties: Store hotel amenities (WiFi, Pool, Gym, etc.)

  • Fields: amenetie_name, icon

Hotel: The main hotel model

  • Fields: hotel_name, hotel_description, hotel_slug, hotel_owner (FK), ameneties (M2M), hotel_price, hotel_offer_price, hotel_location, is_active
  • Relationships: Owner → HotelVendor, Ameneties → ManyToMany

HotelImages: Store multiple images for each hotel

  • Fields: hotel (FK), image

HotelManager: Store manager details for hotels

  • Fields: hotel (FK), manager_name, manager_contact

HotelBooking: Store booking information

  • Fields: hotel (FK), booking_user (FK), booking_start_date, booking_end_date, booking_price
Python
from django.db import models
from django.contrib.auth.models import User
from django.forms.models import model_to_dict

class HotelUser(User):
    profile_picture = models.ImageField(upload_to='profile')
    phone_number = models.CharField(unique=True, max_length=20)
    email_token = models.CharField(max_length=100, null=True, blank=True)
    otp = models.CharField(max_length=10, null=True, blank=True)
    is_verified = models.BooleanField(default=False)
    
    class Meta:
        db_table = "hotel_user"

class HotelVendor(User):
    phone_number = models.CharField(unique=True, max_length=20)
    profile_picture = models.ImageField(upload_to='profile')
    email_token = models.CharField(max_length=100, null=True, blank=True)
    otp = models.CharField(max_length=10, null=True, blank=True)
    business_name = models.CharField(max_length=100)
    is_verified = models.BooleanField(default=False)

    class Meta:
        db_table = "hotel_vendor"


class Ameneties(models.Model):
    amenetie_name = models.CharField(max_length=500)
    icon = models.ImageField(upload_to="hotels")

    def __str__(self):
        return self.amenetie_name

class Hotel(models.Model):
    hotel_name = models.CharField(max_length=100)
    hotel_description = models.TextField()
    hotel_slug = models.SlugField(max_length=200, unique=True)
    hotel_owner = models.ForeignKey(HotelVendor, on_delete=models.CASCADE, related_name="hotels")
    ameneties = models.ManyToManyField(Ameneties)
    hotel_price = models.FloatField()
    hotel_offer_price = models.FloatField()
    hotel_location = models.TextField()
    is_active = models.BooleanField(default=True)

class HotelImages(models.Model):
    hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="hotel_images")
    image = models.ImageField(upload_to="hotels")

class HotelManager(models.Model):
    hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="hotel_managers")
    manager_name = models.CharField(max_length=100)
    manager_contact = models.CharField(max_length=100)

class HotelBooking(models.Model):
    hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, related_name="bookings")
    booking_user = models.ForeignKey(HotelUser, on_delete=models.CASCADE)
    booking_start_date = models.DateField()
    booking_end_date = models.DateField()
    booking_price = models.FloatField()

Step 6: Migrations and Database Setup

After defining models, create migration files:

python manage.py makemigrations

Apply migrations to create database tables:

python manage.py migrate

Register Models in Admin

Open accounts/admin.py and register models to access them via Django admin:

Python
from django.contrib import admin
from .models import *

admin.site.register(HotelUser)
admin.site.register(HotelVendor)
admin.site.register(Ameneties)

@admin.register(Hotel)
class HotelAdmin(admin.ModelAdmin):
    list_display = ('hotel_name', 'hotel_owner', 'hotel_location', 'hotel_price', 'hotel_offer_price', 'is_active')
    list_filter = ('is_active', 'hotel_owner')
    search_fields = ('hotel_name', 'hotel_location', 'hotel_slug')
    readonly_fields = ('hotel_slug',)
    list_editable = ('is_active',)
    
@admin.register(HotelImages)
class HotelImagesAdmin(admin.ModelAdmin):
    list_display = ('hotel', 'image')
    list_filter = ('hotel',)
    search_fields = ('hotel__hotel_name',)

@admin.register(HotelBooking)
class HotelBookingAdmin(admin.ModelAdmin):
    list_display = ('hotel', 'booking_user', 'booking_start_date', 'booking_end_date', 'booking_price')
    list_filter = ('booking_start_date', 'booking_end_date')
    search_fields = ('hotel__hotel_name', 'booking_user__email', 'booking_user__first_name', 'booking_user__last_name')
    date_hierarchy = 'booking_start_date'
  • HotelAdmin: Custom admin view for hotels with list display, search, filters, inline status editing, and a read-only slug field.
  • HotelImagesAdmin: Admin configuration to manage hotel images with filtering and search by hotel.
  • HotelBookingAdmin: Admin interface for hotel bookings with date-based filtering, search, and hierarchical navigation.

Create Superuser

Create an admin user to access Django admin panel:

python manage.py createsuperuser

Follow prompts to set username, email, and password.

Step 7: URL Configuration

In oyo_clone/urls.py include app URLs and configure media/static files:

Python
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from debug_toolbar.toolbar import debug_toolbar_urls

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('home.urls')),
    path('accounts/', include('accounts.urls')),
] + debug_toolbar_urls()

if settings.DEBUG:
    urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
    urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)


Create accounts/urls.py and define authentication and vendor routes:

Python
from django.urls import path
from accounts import views

urlpatterns = [
    path('login/', views.login_view, name='login'),
    path('login-with-otp/', views.login_with_otp_view, name="login-otp-page"),
    path('login-with-otp/<str:email>/', views.login_otp_enter_view, name="login-otp-enter" ),
    path('<str:email>/verify-otp/<int:otp>/',views.verify_otp_view, name="verify-otp" ),
    path('logout/', views.logout_view, name='logout'),
    path('register/', views.register_view, name='register'),
    path('verify-account/<token>', views.verify_email_view, name='verify-account'),
    # Vendor routes
    path('vendor-login/', views.vendor_login_view, name="vendor-login"),
    path('vendor-register/', views.vendor_register_view, name="vendor-register"),
    path('vendor-dashboard/', views.vendor_dashboard_view, name="vendor-dashboard"),
    path('add-hotel/', views.add_hotel_view, name="add-hotel"),
    path('view-bookings/', views.view_bookings_view, name='view-bookings'),
    # Dynamic routes
    path('<slug>/upload-image', views.upload_images_view, name="upload-image"),
    path('<id>/delete-image/', views.delete_images_view, name="delete-image"),
    path('<slug>/edit-hotel-details/', views.edit_hotel_view, name='edit-hotel'),
    path('<slug>/delete-hotel/', views.delete_hotel_view, name='delete-hotel'),
]

Create home/urls.py for main website routes:

Python
from django.urls import path
from home import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<slug>/hotel-details', views.hotel_details_view, name="hotel-details")
]

Step 8: Views and Business Logic

In accounts/views.py:

Python
from django.shortcuts import render, redirect, HttpResponse
from django.db.models import Q
from django.contrib import messages 
from accounts.models import HotelUser, HotelVendor, Hotel, Ameneties, HotelImages, HotelBooking
from .templates.utils.sendEmail import send_test_email, generateToken, send_email_with_otp, generate_slug
from django.contrib.auth import authenticate, login, logout
import random
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseRedirect

def login_view(request):
    if request.method == 'POST':
        user_email = request.POST.get('email')
        password = request.POST.get('password')
        next_url = request.GET.get('next') or request.POST.get('next')

        try:
            hotel_user = HotelUser.objects.get(email=user_email)
        except HotelUser.DoesNotExist:
            messages.warning(request, "Incorrect email address")
            if next_url:
                return redirect(f"/accounts/login/?next={next_url}")
            return redirect("/accounts/login/")
        
        if not hotel_user.is_verified:
            messages.warning(request, "Please verify your account, Check your email inbox.")
            if next_url:
                return redirect(f"/accounts/login/?next={next_url}")
            return redirect("/accounts/login/")

        if not hotel_user.check_password(password):
            messages.warning(request, "Incorrect password")
            if next_url:
                return redirect(f"/accounts/login/?next={next_url}")
            return redirect("/accounts/login/")
        
        if request.user.is_authenticated:
            try:
                HotelVendor.objects.get(id=request.user.id)
                logout(request)
                messages.info(request, "You were logged out from vendor account. Now logged in as customer.")
            except HotelVendor.DoesNotExist:
                pass
        
        login(request, hotel_user)
        
        if next_url:
            return redirect(next_url)
        return redirect('index')

    return render(request, 'login.html')

def login_with_otp_view(request):
    context = {
        "show_email":True,
        "show_otp":True,
        "display_otp":"d-none",
        "display":None,
        "email":""
    }
    return render(request, "login_otp.html", context)

def login_otp_enter_view(request, email):
    hotel_user = HotelUser.objects.filter(email = email)
    if not hotel_user:
        messages.warning(request, "Invalid email address, Please register if not..")
        return redirect("/accounts/login/")
    otp = random.randint(1111, 9999)
    hotel_user = HotelUser.objects.get(email=email)
    hotel_user.otp = otp
    hotel_user.save()
    send_email_with_otp(email, otp)
    context = {
        "show_email":False,
        "show_otp":True,
        "display":"d-none",
        "display_otp":"d-block",
        "email":email
    }
    return render(request, "login_otp.html", context)

def verify_otp_view(request, email, otp):
    hotel_user = HotelUser.objects.get(email=email)
    if str(hotel_user.otp) != str(otp):
        messages.warning(request, "Wrong OTP, re-enter correct OTP")
        context = {
            "show_email": False,
            "show_otp": True,
            "display": "d-none",
            "display_otp": "d-block",
            "email": email
        }
        return render(request, "login_otp.html", context)
    login(request, hotel_user)
    return redirect('index')
    
def logout_view(request):
    logout(request)
    return redirect("/accounts/login/")

def register_view(request):
    if request.method == 'POST':
        first_name = request.POST.get('first_name')
        last_name = request.POST.get('last_name')
        email = request.POST.get('email')
        password = request.POST.get('password')
        phone_number = request.POST.get('phone_number')

        hotel_user = HotelUser.objects.filter(
            Q(email=email) | Q(username=phone_number) 
        )
        if hotel_user:
            messages.warning(request, "An account exists with this email or phone try another one")
            return redirect('/accounts/register/')
        
        hotel_user = HotelUser.objects.create(
            username = phone_number,
            first_name = first_name,
            last_name = last_name,
            email = email,
            phone_number = phone_number,
            email_token = generateToken()
        )
        hotel_user.set_password(password)
        hotel_user.save()
        send_test_email(hotel_user.email, hotel_user.email_token)
        messages.success(request, f"A verification email sent to you registered email:{hotel_user.email}")

    return render(request, 'register.html')
    
def verify_email_view(request, token):
    try:
        hotel_user = None
        user = False
        vendor = False

        try:
            hotel_user = HotelUser.objects.get(email_token=token)
            user = True
        except HotelUser.DoesNotExist:
            try:
                hotel_user = HotelVendor.objects.get(email_token=token)
                vendor = True
            except HotelVendor.DoesNotExist:
                return HttpResponse("Invalid Token")

        hotel_user.is_verified = True
        hotel_user.save()

        if user:
            messages.success(request, "Email successfully verified")
            return redirect('/accounts/login/')
        elif vendor:
            messages.success(request, "Email successfully verified")
            return redirect('/accounts/vendor-login/')

    except Exception as e:
        return HttpResponse("Something went wrong")

def vendor_login_view(request):
    if request.method == 'POST':
        user_email = request.POST.get('email')
        password = request.POST.get('password')

        try:
            vendor = HotelVendor.objects.get(email=user_email)
        except HotelVendor.DoesNotExist:
            messages.warning(request, "Incorrect email address")
            return redirect("/accounts/vendor-login/")

        if not vendor.is_verified:
            messages.warning(
                request,
                "Please verify your account. Check your email inbox."
            )
            return redirect("/accounts/vendor-login/")

        user = authenticate(
            request,
            username=vendor.username,
            password=password
        )

        if user is None:
            messages.warning(request, "Incorrect password")
            return redirect("/accounts/vendor-login/")

        # If user is already logged in as a customer, logout first
        if request.user.is_authenticated:
            try:
                # Check if currently logged in as customer
                HotelUser.objects.get(id=request.user.id)
                logout(request)
                messages.info(request, "You were logged out from customer account. Now logged in as vendor.")
            except HotelUser.DoesNotExist:
                pass  # Not a customer, or not logged in

        login(request, user)
        return redirect("/accounts/vendor-dashboard/")

    return render(request, 'vendor/vendor_login.html')

def vendor_register_view(request):
    if request.method == 'POST':
        first_name = request.POST.get('first_name')
        last_name = request.POST.get('last_name')
        business_name = request.POST.get('business_name')
        email = request.POST.get('email')
        password = request.POST.get('password')
        phone_number = request.POST.get('phone_number')

        if HotelVendor.objects.filter(
            Q(username=phone_number) |
            Q(email=email) |
            Q(phone_number=phone_number)
        ).exists():
            messages.warning(
                request,
                "An account already exists with this email or phone number"
            )
            return redirect('/accounts/vendor-register/')

        hotel_user = HotelVendor.objects.create_user(
            username=phone_number,     # MUST be unique
            email=email,
            password=password,
            first_name=first_name,
            last_name=last_name,
            phone_number=phone_number,
            business_name=business_name,
        )

        hotel_user.email_token = generateToken()
        hotel_user.save()

        send_test_email(hotel_user.email, hotel_user.email_token)

        messages.success(
            request,
            f"A verification email has been sent to {hotel_user.email}"
        )
        return redirect('/accounts/vendor-login/')

    return render(request, 'vendor/vendor_register.html')


def setImages(hotels):
    for hotel in hotels:
        # Get the first image for this hotel
        first_image = hotel.hotel_images.first()
        if first_image:
            hotel.image_url = first_image.image.url
        else:
            hotel.image_url = None
    return hotels

@login_required(login_url="/accounts/vendor-login/")
def vendor_dashboard_view(request):
    hotels = Hotel.objects.filter(hotel_owner=request.user.id).prefetch_related('hotel_images')
    hotels = setImages(hotels)
    context = {
        'hotels':hotels[:10]
    }
    return render(request, "vendor/vendor_dashboard.html", context)

@login_required(login_url="/accounts/vendor-login/")
def add_hotel_view(request):
    if request.method == 'POST':
        try:
            hotel_name = request.POST.get('name')
            hotel_description = request.POST.get('description')
            ameneties_ids = request.POST.getlist('ameneties')
            hotel_price = request.POST.get('hotel_price')
            hotel_offer_price = request.POST.get('hotel_offer_price')
            hotel_location = request.POST.get('location')
            
            # Validate required fields
            if not hotel_name or not hotel_location or not hotel_price or not hotel_offer_price:
                messages.error(request, "Please fill in all required fields (Name, Location, Hotel Price, Offer Price).")
                return redirect("add-hotel")
            
            hotel_slug = generate_slug(hotel_name)

            # Get the vendor - with multi-table inheritance, we need to query HotelVendor
            try:
                hotel_vendor = HotelVendor.objects.get(id=request.user.id)
            except HotelVendor.DoesNotExist:
                messages.error(request, "Only vendors can add hotels. Please login as a vendor.")
                return redirect("/accounts/vendor-login/")

            hotel_obj = Hotel.objects.create(
                hotel_name=hotel_name,
                hotel_description=hotel_description,
                hotel_slug=hotel_slug,
                hotel_owner=hotel_vendor,
                hotel_price=float(hotel_price),
                hotel_offer_price=float(hotel_offer_price),
                hotel_location=hotel_location,
            )
            
            # Add amenities if selected
            if ameneties_ids:
                amenities = Ameneties.objects.filter(id__in=ameneties_ids)
                hotel_obj.ameneties.add(*amenities)

            # Handle image uploads
            images = request.FILES.getlist('images')
            if images:
                for image in images:
                    HotelImages.objects.create(hotel=hotel_obj, image=image)
                messages.success(request, f"Hotel created successfully with {len(images)} image(s).")
            else:
                messages.success(request, "Hotel created successfully. You can add images later.")

            return redirect("vendor-dashboard")
            
        except Exception as e:
            messages.error(request, f"Error creating hotel: {str(e)}")
            return redirect("add-hotel")

    ameneties = Ameneties.objects.all()
    context = {
        "hotel_ameneties": ameneties
    }
    return render(request, "vendor/add_hotel.html", context)

@login_required(login_url="/accounts/vendor-login/")
def upload_images_view(request, slug):
    hotel_obj = Hotel.objects.get(hotel_slug = slug)
    if request.method == 'POST':
        image = request.FILES['image']
        HotelImages.objects.create(
            hotel = hotel_obj,
            image = image
        )

        return HttpResponseRedirect(request.path_info)
    return render(request, "vendor/upload_image.html", context = {'images' : hotel_obj.hotel_images.all()})

@login_required(login_url="/accounts/vendor-login/")
def delete_images_view(request, id):
    hotel_image_obj = HotelImages.objects.filter(id = id)
    if hotel_image_obj:
        hotel_image_obj[0].delete()
        return HttpResponseRedirect(request.path_info)
    return HttpResponseRedirect("/accounts/vendor-dashboard/")

@login_required(login_url="/accounts/vendor-login/")
def edit_hotel_view(request, slug):
    hotel_obj = Hotel.objects.get(hotel_slug = slug)
    if request.user.id != hotel_obj.hotel_owner.id:
        return HttpResponse("You are not authorized")
    
    if request.method == 'POST':
        hotel_name = request.POST.get('name')
        hotel_description = request.POST.get('description')
        print("this is description: ", hotel_description)
        ameneties = request.POST.getlist('ameneties')
        hotel_price = request.POST.get('hotel_price')
        hotel_offer_price = request.POST.get('hotel_offer_price')
        hotel_location = request.POST.get('location')

        hotel_obj.hotel_name = hotel_name
        hotel_obj.hotel_description = hotel_description
        hotel_obj.hotel_price = hotel_price
        hotel_obj.hotel_offer_price = hotel_offer_price
        hotel_obj.hotel_location = hotel_location
        hotel_obj.save()   

        messages.success(request, "Hotel Details Updated")
        return HttpResponseRedirect(request.path_info)
    
    hotel_ameneties = Ameneties.objects.all()
    context = {
        'hotel_obj' : hotel_obj,
        'hotel_ameneties':hotel_ameneties
    }
    
    return render(request, "vendor/edit_hotel_details.html", context)

@login_required(login_url="/accounts/vendor-login/")
def delete_hotel_view(request, slug):
    """View for vendors to delete their hotels"""
    try:
        hotel_obj = Hotel.objects.get(hotel_slug=slug)
    except Hotel.DoesNotExist:
        messages.error(request, "Hotel not found.")
        return redirect('vendor-dashboard')
    
    # Check if the logged-in vendor owns this hotel
    try:
        vendor = HotelVendor.objects.get(id=request.user.id)
    except HotelVendor.DoesNotExist:
        messages.error(request, "Only vendors can delete hotels.")
        return redirect('vendor-dashboard')
    
    if hotel_obj.hotel_owner.id != request.user.id:
        messages.error(request, "You can only delete your own hotels.")
        return redirect('vendor-dashboard')
    
    # Delete the hotel (this will cascade delete related images and bookings)
    hotel_name = hotel_obj.hotel_name
    hotel_obj.delete()
    messages.success(request, f"Hotel '{hotel_name}' has been deleted successfully.")
    return redirect('vendor-dashboard')

from datetime import date, datetime
@login_required(login_url="/accounts/vendor-login/")
def view_bookings_view(request):
    """View for vendors to see bookings for their hotels"""
    # Get bookings for hotels owned by this vendor
    bookings = HotelBooking.objects.filter(
        hotel__hotel_owner__id=request.user.id
    ).select_related('hotel', 'booking_user').order_by('-booking_start_date')
    
    # Calculate booking days for each booking
    for booking in bookings:
        booking_start_date = str(booking.booking_start_date)
        booking_end_date = str(booking.booking_end_date)
        start_date = datetime.strptime(booking_start_date, '%Y-%m-%d')
        end_date = datetime.strptime(booking_end_date, '%Y-%m-%d')
        days_count = (end_date - start_date).days
        booking.total_booking_days = days_count
    
    context = {
        'bookings': bookings
    }
    return render(request, "vendor/view_bookings.html", context)
  • login_view: Authenticates a regular hotel user using email and password after verifying the account status.
  • login_with_otp_view: Displays the initial OTP login page UI for users.
  • login_otp_enter_view: Generates and emails an OTP to the user for OTP-based login.
  • verify_otp_view: Verifies the OTP and logs the user in if the OTP matches.
  • logout_view: Logs out the currently authenticated user and redirects to the login page.
  • register_view: Registers a new hotel user and sends an email verification link.
  • verify_email_view: Verifies email tokens for both users and vendors and activates their accounts.
  • vendor_login_view: Authenticates a hotel vendor using email and password after verification.
  • vendor_register_view: Registers a new hotel vendor and sends an email verification link.
  • setImages: Attaches the first available hotel image URL to each hotel object for display.
  • vendor_dashboard_view: Displays the vendor dashboard showing hotels owned by the logged-in vendor.
  • add_hotel_view – Allows vendors to add a new hotel with amenities and optional image uploads.
  • upload_images_view: Uploads additional images for a specific hotel.
  • delete_images_view: Deletes a specific hotel image from the vendor dashboard.
  • edit_hotel_view: Allows vendors to edit hotel details such as name, price, location, and amenities.
  • view_bookings_view: Displays all bookings for hotels owned by the logged-in vendor along with booking duration.

In home/views.py:

Python
from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from accounts.models import *
import random
from datetime import date, datetime
from django.contrib import messages
from django.http import HttpResponseRedirect
from django.views.decorators.cache import cache_page
from django.db.models import Prefetch

def setImages(hotels):
    for hotel in hotels:
        # Get the first image for this hotel
        first_image = hotel.hotel_images.first()
        if first_image:
            hotel.image_url = first_image.image.url
        else:
            hotel.image_url = None
    return hotels

@login_required
@cache_page(60*2)
def index(request):
    search = request.GET.get('search')
    sort_by = request.GET.get('sort_by')

    hotels = Hotel.objects.filter(is_active=True).select_related('hotel_owner').prefetch_related(
        Prefetch('hotel_images'),
        Prefetch('ameneties')
    )

    if search:
        hotels = hotels.filter(hotel_name__icontains=search)

    if sort_by == 'sort_low':
        hotels = hotels.order_by('hotel_offer_price')
    elif sort_by == 'sort_high':
        hotels = hotels.order_by('-hotel_offer_price')

    hotels = setImages(hotels)

    context = {'hotels': hotels}
    return render(request, 'index.html', context)

import math
def hotel_details_view(request, slug):
    today = date.today().isoformat()  # Format: YYYY-MM-DD
    try:
        hotel_details = Hotel.objects.prefetch_related('hotel_images', 'ameneties').get(hotel_slug=slug, is_active=True)
    except Hotel.DoesNotExist:
        messages.error(request, "Hotel not found or is no longer available.")
        return redirect('index')

    if request.method == "POST":
        start_date = request.POST.get('start-date')
        end_date = request.POST.get('end-date')
        
        # Validate that dates are provided
        if not start_date or not end_date:
            messages.warning(request, "Please select both start and end dates for booking.")
            return HttpResponseRedirect(request.path_info)
        
        try:
            start_date = datetime.strptime(start_date, '%Y-%m-%d')
            end_date = datetime.strptime(end_date, '%Y-%m-%d')
        except ValueError:
            messages.warning(request, "Invalid date format. Please select valid dates.")
            return HttpResponseRedirect(request.path_info)
        
        # Validate that end date is after start date
        if end_date <= start_date:
            messages.warning(request, "End date must be after start date.")
            return HttpResponseRedirect(request.path_info)
        
        days_count = (end_date-start_date).days
        total_price = hotel_details.hotel_offer_price * days_count
        # Round to 2 decimal places for currency
        booking_price = round(total_price, 2)

        # Check if user is a HotelUser (not a vendor)
        try:
            booking_user = HotelUser.objects.get(id=request.user.id)
        except HotelUser.DoesNotExist:
            messages.error(request, "Only customers can book hotels. Vendors cannot make bookings.")
            return HttpResponseRedirect(request.path_info)

        # Check for overlapping bookings (same hotel, same user, overlapping dates)
        overlapping_bookings = HotelBooking.objects.filter(
            hotel=hotel_details,
            booking_user=booking_user
        ).filter(
            # Booking overlaps if: new_start <= existing_end AND new_end >= existing_start
            booking_start_date__lte=end_date,
            booking_end_date__gte=start_date
        )
        
        if overlapping_bookings.exists():
            messages.error(request, "You already have a booking for this hotel on the selected dates. Please choose different dates.")
            return HttpResponseRedirect(request.path_info)

        HotelBooking.objects.create(
            hotel = hotel_details,
            booking_user = booking_user,
            booking_start_date = start_date,
            booking_end_date = end_date,
            booking_price = booking_price
        )
        messages.success(request, "Booking is Successfull...")
        return redirect('my-bookings')

    context = {     
        'hotel_details':hotel_details,
        'today_date':today
    }
    return render(request, "hotel_details.html", context)

@login_required
def my_bookings_view(request):
    bookings = HotelBooking.objects.filter(booking_user=request.user).order_by('-booking_start_date')
    
    # Calculate booking days for each booking
    for booking in bookings:
        booking_start_date = str(booking.booking_start_date)
        booking_end_date = str(booking.booking_end_date)
        start_date = datetime.strptime(booking_start_date, '%Y-%m-%d')
        end_date = datetime.strptime(booking_end_date, '%Y-%m-%d')
        days_count = (end_date - start_date).days
        booking.total_booking_days = days_count
    
    context = {
        'bookings': bookings
    }
    return render(request, "my_bookings.html", context)

@login_required
def cancel_booking_view(request, booking_id):
    try:
        booking = HotelBooking.objects.get(id=booking_id, booking_user=request.user)
        hotel_name = booking.hotel.hotel_name
        booking.delete()
        messages.success(request, f"Booking for {hotel_name} has been cancelled successfully.")
    except HotelBooking.DoesNotExist:
        messages.error(request, "Booking not found or you don't have permission to cancel it.")
    
    return redirect('my-bookings')
  • index: Displays the home page with active hotels, supporting search, price-based sorting, caching, and optimized queries with prefetching.
  • hotel_details_view: Shows detailed information of a hotel and handles hotel booking with date validation, price calculation, and overlap checks.
  • my_bookings_view: Lists all bookings made by the logged-in user along with the total number of booking days.
  • cancel_booking_view: Allows a user to cancel their own hotel booking securely.
  • setImages: Attaches the first available hotel image URL to each hotel object for efficient frontend rendering.

Step 9: Templates and Frontend

Template Structure:


accounts/templates/
├── login.html
├── register.html
├── login_otp.html
└── vendor/
├── vendor_login.html
├── vendor_register.html
├── vendor_dashboard.html
├── add_hotel.html
├── edit_hotel_details.html
├── upload_image.html
└── view_bookings.html

home/templates/
├── index.html
├── my_bookings.html
├── hotel_details.html
└── utils/
├── base.html
├── navbar.html
└── alerts.html

  • base.html: Base template for customer pages; includes Bootstrap, navbar, footer, and a start block for child templates.
  • navbar.html: Customer navigation bar with logo, Home link, and conditional Login/Register or My Bookings/Logout based on authentication.
  • vendor_base.html: Base template for vendor pages; includes Bootstrap, vendor navbar, footer, and a start block for child templates.
  • vendor/navbar.html: Vendor navigation bar with logo, Dashboard link, and conditional Login/Register or Logout based on authentication.
  • alerts.html: Displays Django messages (success, error, warning) as Bootstrap alerts.
  • index.html: Homepage listing hotels with search, sort, hotel cards (image, name, amenities, prices), and "View details" links.
  • hotel_details.html: Hotel detail page showing images gallery, amenities, prices, and a booking form with date inputs (requires login).
  • my_bookings.html: Customer bookings page displaying bookings in a table with hotel info, dates, price, and cancel buttons.
  • login.html: Login form with email, password, and a "Login with OTP" button.
  • register.html: Registration form collecting first name, last name, email, phone number, and password.
  • vendor_dashboard.html: Vendor dashboard listing the vendor's hotels with images, amenities, prices, and links to upload images or edit.
  • add_hotel.html: Form for vendors to add a hotel: name, description, amenities (multi-select), prices, location, and optional image uploads.
  • view_bookings.html: Vendor page showing bookings for the vendor's hotels in a table with customer name, hotel, dates, and price.
  • edit_hotel_details.html: Form for vendors to edit hotel details, pre-filled with current hotel data.
  • upload_image.html: Page for vendors to upload additional images for a hotel, showing existing images with delete options.
  • vendor_login.html: Login form for vendors with email and password fields.
  • vendor_register.html: Registration form for vendors collecting first name, last name, business name, email, phone number, and password.
  • login_otp.html: OTP login page with email input and OTP input fields, toggled based on the login step.

You may create all the required application files, define their functionality, and apply appropriate styling based on your design choices, or alternatively use the existing ZIP file that contains all the HTML files. Click here to access the existing file.

Step 9: Testing

Once the setup is complete, we can proceed to test the OYO clone.

User dashboad

Once the user completes registration and logs in, they will be redirected to the dashboard:

image5
Snapshot of User Dashboard

User Hotel details and booking page

When the user selects a hotel, they are taken to its detail page to proceed with the booking:

image
Snapshot of Hotel details and booking page

User Booking detail page

The user can view their booking details by navigating to My Bookings:

image1
Snapshot of Hotel booking details

Vendor Dashboard

Once a vendor completes registration and logs in, they will be redirected to the dashboard:

image2
Snapshot of vendor dashboard after Sign in

Vendor Hotel add

Vendors can add a hotel by providing the required details:

image3
Snapshot of vender adding hotel

Vendor Dashboard with hotel listed:

Once the vendor adds a hotel, it becomes visible in the hotel listings:

image4
Snapshot of Vendor Dashboard with hotel listed

All other functionalities and features are demonstrated in this video link.

Comment