diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..1d5c207add --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,59 @@ +name: 'Lint Code' + +on: + push: + branches: [master, main] + pull_request: + branches: [master, main] + +jobs: + lint_python: + name: Lint Python Files + runs-on: ubuntu-latest + + steps: + + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.12 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 + + - name: Print working directory + run: pwd + + - name: Run Linter + run: | + pwd + # This command finds all Python files recursively and runs flake8 on them + find . -name "*.py" -exec flake8 {} + + echo "Linted all the python files successfully" + + lint_js: + name: Lint JavaScript Files + runs-on: ubuntu-latest + + steps: + - name: Checkout Repository + uses: actions/checkout@v3 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 14 + + - name: Install JSHint + run: npm install jshint --global + + - name: Run Linter + run: | + # This command finds all JavaScript files recursively and runs JSHint on them + find ./server/database -name "*.js" -exec jshint {} + + echo "Linted all the js files successfully" diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000000..6bc8f189e8 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,3 @@ +{ + "esversion": 8 +} diff --git a/LICENSE b/LICENSE index 45cd6221b7..42635561d5 100644 --- a/LICENSE +++ b/LICENSE @@ -4,7 +4,7 @@ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - 1. Definitions. + 1. Definitions as below: "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. diff --git a/README.md b/README.md index 5884e26a5b..f34746c452 100644 --- a/README.md +++ b/README.md @@ -1 +1,92 @@ -# coding-project-template \ No newline at end of file +# ๐Ÿš— Car Dealership Network - Full Stack Application + +A **full-stack car dealership platform** built with Django, React, Node.js, and MongoDB featuring dealership discovery, customer reviews, and AI-powered sentiment analysis. + +![Django](https://img.shields.io/badge/Django-5.0+-green.svg) +![React](https://img.shields.io/badge/React-18+-blue.svg) +![Node.js](https://img.shields.io/badge/Node.js-18+-brightgreen.svg) +![MongoDB](https://img.shields.io/badge/MongoDB-7.0+-darkgreen.svg) +![Python](https://img.shields.io/badge/Python-3.9+-yellow.svg) +![Docker](https://img.shields.io/badge/Docker-24+-blue.svg) + +## ๐Ÿš€ Project Overview + +Full-stack microservices application that enables users to explore car dealerships, read and post reviews, and discover vehicle inventory across different locations. Features Django REST API, React frontend, Node.js microservice, and AI-powered sentiment analysis. + +**Key Technologies:** Django | React | Node.js | MongoDB | NLTK | Docker + +## ๐Ÿ“‹ Features + +**Core Functionality:** +- ๐Ÿข **Dealership Directory** - 50+ dealerships across multiple states with location-based search +- ๐Ÿš— **Vehicle Inventory** - Car catalog with make/model management and search filters +- ๐Ÿ“ **Review System** - Customer reviews with AI sentiment analysis (NLTK/VADER) +- ๐Ÿ‘ค **User Management** - Registration, authentication, and profile management +- ๐Ÿ“ฑ **Responsive Design** - Mobile-optimized interface with Bootstrap styling + +## ๐Ÿƒโ€โ™‚๏ธ Quick Start + +**Prerequisites:** Python 3.9+, Node.js 18+, MongoDB 7.0+ + +```bash +# Clone repository +git clone https://github.com/RitaJind/xrwvm-fullstack_developer_capstone.git +cd xrwvm-fullstack_developer_capstone + +# Start services (in separate terminals) +# Django backend +cd server && pip install -r requirements.txt && python manage.py runserver + +# Node.js database service +cd server/database && npm install && docker-compose up -d && node app.js + +# Sentiment analysis +cd server/djangoapp/microservices && pip install -r requirements.txt && python app.py + +# React frontend +cd server/frontend && npm install && npm start +``` + +**Endpoints:** Frontend: http://localhost:3000 | Backend: http://localhost:8000 | Database: http://localhost:3001 + +## Application Flow + +**User Journey:** Homepage โ†’ Dealership Listings โ†’ Dealer Details โ†’ Vehicle Inventory โ†’ Review System โ†’ User Account + +**Admin Features:** Dealership Management, Inventory Control, Review Moderation, User Management, Analytics Dashboard + +## ๐Ÿ—๏ธ Architecture + +**Microservices:** React SPA โ†” Django API โ†” Node.js API โ†” MongoDB + Sentiment Analysis Service (Flask/NLTK) + +**Tech Stack:** Django 5.0+ | React 18+ | Node.js 18+ | MongoDB 7.0+ | Python 3.9+ | Docker + +## API Documentation + +**Key Endpoints:** +- `GET /djangoapp/get_dealers/` - Fetch all dealerships +- `GET /djangoapp/get_dealers/{state}/` - Fetch dealerships by state +- `POST /djangoapp/add_review/` - Submit a new review +- `POST /djangoapp/login/` - User authentication +- `GET /dealerships` - Node.js: Get all dealerships +- `POST /reviews` - Node.js: Add new review +- `POST /analyze/{text}` - Sentiment analysis + +## ๐Ÿงช Testing + +```bash +# Backend testing +cd server && python manage.py test djangoapp + +# Frontend testing +cd server/frontend && npm test + +# API testing +curl -X GET http://localhost:8000/djangoapp/get_dealers/ +``` + +## ๐Ÿ‘จโ€๐Ÿ’ป About the Developer + +**Rita Jindal** - Full Stack Developer + +*Passionate about building modern, user-friendly web applications with clean code and efficient architecture. Experienced in React ecosystem, state management, and responsive design principles.* diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000000..294e258b0c --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,22 @@ +FROM python:3.12.0-slim-bookworm + +ENV PYTHONBUFFERED 1 +ENV PYTHONWRITEBYTECODE 1 + +WORKDIR /app + +# Install the requirements +COPY server/requirements.txt /app/ +RUN pip3 install -r requirements.txt + +# Copy the rest of the files +COPY . /app/ + +EXPOSE 8000 + +# Create entrypoint script +RUN echo '#!/bin/bash\ncd server\npython manage.py migrate\npython manage.py collectstatic --noinput\nexec "$@"' > /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +ENTRYPOINT ["/app/entrypoint.sh"] +CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "--chdir", "server", "djangoproj.wsgi"] diff --git a/server/build.sh b/server/build.sh new file mode 100644 index 0000000000..abd5cd08f6 --- /dev/null +++ b/server/build.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +# exit on error +set -o errexit + +cd server +pip install -r requirements.txt + +python manage.py collectstatic --no-input +python manage.py migrate diff --git a/server/database/app.js b/server/database/app.js index 00f52b2008..ebe6f95f03 100644 --- a/server/database/app.js +++ b/server/database/app.js @@ -1,11 +1,11 @@ const express = require('express'); const mongoose = require('mongoose'); const fs = require('fs'); -const cors = require('cors') -const app = express() +const cors = require('cors'); +const app = express(); const port = 3030; -app.use(cors()) +app.use(cors()); app.use(require('body-parser').urlencoded({ extended: false })); const reviews_data = JSON.parse(fs.readFileSync("reviews.json", 'utf8')); @@ -19,13 +19,13 @@ const Reviews = require('./review'); const Dealerships = require('./dealership'); try { - Reviews.deleteMany({}).then(()=>{ - Reviews.insertMany(reviews_data['reviews']); + Reviews.deleteMany({}).then(() => { + Reviews.insertMany(reviews_data.reviews); }); - Dealerships.deleteMany({}).then(()=>{ - Dealerships.insertMany(dealerships_data['dealerships']); + Dealerships.deleteMany({}).then(() => { + Dealerships.insertMany(dealerships_data.dealerships); }); - + } catch (error) { res.status(500).json({ error: 'Error fetching documents' }); } @@ -33,7 +33,7 @@ try { // Express route to home app.get('/', async (req, res) => { - res.send("Welcome to the Mongoose API") + res.send("Welcome to the Mongoose API"); }); // Express route to fetch all reviews @@ -58,36 +58,51 @@ app.get('/fetchReviews/dealer/:id', async (req, res) => { // Express route to fetch all dealerships app.get('/fetchDealers', async (req, res) => { -//Write your code here + try { + const documents = await Dealerships.find(); + res.json(documents); + } catch (error) { + res.status(500).json({ error: 'Error fetching dealerships' }); + } }); // Express route to fetch Dealers by a particular state app.get('/fetchDealers/:state', async (req, res) => { -//Write your code here + try { + const documents = await Dealerships.find({ state: req.params.state }); + res.json(documents); + } catch (error) { + res.status(500).json({ error: 'Error fetching dealerships by state' }); + } }); - + // Express route to fetch dealer by a particular id app.get('/fetchDealer/:id', async (req, res) => { -//Write your code here + try { + const document = await Dealerships.find({ id: parseInt(req.params.id) }); + res.json(document); + } catch (error) { + res.status(500).json({ error: 'Error fetching dealer by id' }); + } }); - + //Express route to insert review app.post('/insert_review', express.raw({ type: '*/*' }), async (req, res) => { - data = JSON.parse(req.body); - const documents = await Reviews.find().sort( { id: -1 } ) - let new_id = documents[0]['id']+1 + const data = JSON.parse(req.body); + const documents = await Reviews.find().sort({ id: -1 }); + let new_id = documents[0].id + 1; const review = new Reviews({ - "id": new_id, - "name": data['name'], - "dealership": data['dealership'], - "review": data['review'], - "purchase": data['purchase'], - "purchase_date": data['purchase_date'], - "car_make": data['car_make'], - "car_model": data['car_model'], - "car_year": data['car_year'], - }); + id: new_id, + name: data.name, + dealership: data.dealership, + review: data.review, + purchase: data.purchase, + purchase_date: data.purchase_date, + car_make: data.car_make, + car_model: data.car_model, + car_year: data.car_year, + }); try { const savedReview = await review.save(); @@ -100,5 +115,5 @@ app.post('/insert_review', express.raw({ type: '*/*' }), async (req, res) => { // Start the Express server app.listen(port, () => { - console.log(`Server is running on http://localhost:${port}`); + console.log('Server is running on http://localhost:' + port); }); diff --git a/server/database/data/reviews.json b/server/database/data/reviews.json index d7b0e6a808..bf98921eb4 100644 --- a/server/database/data/reviews.json +++ b/server/database/data/reviews.json @@ -532,7 +532,7 @@ "id": 49, "name": "Jo-anne Szwandt", "dealership": 7, - "review": "Self-enabling maximized focus group", + "review": "Excellent Dealership! Highly recommended.", "purchase": true, "purchase_date": "01/26/2021", "car_make": "Mercedes-Benz", diff --git a/server/database/xrwvm-fullstack_developer_capstone b/server/database/xrwvm-fullstack_developer_capstone new file mode 160000 index 0000000000..d63ce73408 --- /dev/null +++ b/server/database/xrwvm-fullstack_developer_capstone @@ -0,0 +1 @@ +Subproject commit d63ce73408ee2b45cebe652a7607481acf45addd diff --git a/server/deployment.yaml b/server/deployment.yaml new file mode 100644 index 0000000000..7cf8d3a71b --- /dev/null +++ b/server/deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + run: dealership + name: dealership +spec: + replicas: 1 + selector: + matchLabels: + run: dealership + strategy: + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + type: RollingUpdate + template: + metadata: + labels: + run: dealership + spec: + containers: + - image: us.icr.io/your-name-space/dealership:latest + imagePullPolicy: Always + name: dealership + ports: + - containerPort: 8000 + protocol: TCP + restartPolicy: Always diff --git a/server/djangoapp/.env b/server/djangoapp/.env index 01822e542a..96a0db825f 100644 --- a/server/djangoapp/.env +++ b/server/djangoapp/.env @@ -1,2 +1,2 @@ -backend_url =your backend url -sentiment_analyzer_url=your code engine deployment url +backend_url=https://rjindal-3030.theiadockernext-1-labs-prod-theiak8s-4-tor01.proxy.cognitiveclass.ai +sentiment_analyzer_url=https://sentianalyzer.207iqmm6nwv7.us-south.codeengine.appdomain.cloud/ \ No newline at end of file diff --git a/server/djangoapp/__init__.py b/server/djangoapp/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/server/djangoapp/admin.py b/server/djangoapp/admin.py index 433657fc64..6218f25b0e 100644 --- a/server/djangoapp/admin.py +++ b/server/djangoapp/admin.py @@ -1,8 +1,9 @@ -# from django.contrib import admin -# from .models import related models +from django.contrib import admin +from .models import CarMake, CarModel - -# Register your models here. +# Registering models with their respective admins +admin.site.register(CarMake) +admin.site.register(CarModel) # CarModelInline class diff --git a/server/djangoapp/models.py b/server/djangoapp/models.py index eb101a68c8..2910d34dcb 100644 --- a/server/djangoapp/models.py +++ b/server/djangoapp/models.py @@ -1,25 +1,36 @@ -# Uncomment the following imports before adding the Model code - -# from django.db import models -# from django.utils.timezone import now -# from django.core.validators import MaxValueValidator, MinValueValidator +from django.db import models +from django.core.validators import MaxValueValidator, MinValueValidator # Create your models here. -# Create a Car Make model `class CarMake(models.Model)`: -# - Name -# - Description -# - Any other fields you would like to include in car make model -# - __str__ method to print a car make object - - -# Create a Car Model model `class CarModel(models.Model):`: -# - Many-To-One relationship to Car Make model (One Car Make has many -# Car Models, using ForeignKey field) -# - Name -# - Type (CharField with a choices argument to provide limited choices -# such as Sedan, SUV, WAGON, etc.) -# - Year (IntegerField) with min value 2015 and max value 2023 -# - Any other fields you would like to include in car model -# - __str__ method to print a car make object + +class CarMake(models.Model): + + name = models.CharField(max_length=100) + description = models.TextField() + + def __str__(self): + return self.name + + +class CarModel(models.Model): + car_make = models.ForeignKey(CarMake, on_delete=models.CASCADE) + name = models.CharField(max_length=100) + CAR_TYPES = [ + ('SEDAN', 'Sedan'), + ('SUV', 'SUV'), + ('WAGON', 'Wagon'), + # Add more choices as required + ] + type = models.CharField(max_length=10, choices=CAR_TYPES, default='SUV') + year = models.IntegerField( + default=2023, + validators=[ + MaxValueValidator(2023), + MinValueValidator(2015) + ] + ) + + def __str__(self): + return self.name diff --git a/server/djangoapp/populate.py b/server/djangoapp/populate.py index 1927e09e18..e9483ae2cd 100644 --- a/server/djangoapp/populate.py +++ b/server/djangoapp/populate.py @@ -1,2 +1,123 @@ +from .models import CarMake, CarModel + + def initiate(): - print("Populate not implemented. Add data manually") + car_make_data = [ + {"name": "NISSAN", "description": "Great cars. Japanese technology"}, + {"name": "Mercedes", "description": "Great cars. German technology"}, + {"name": "Audi", "description": "Great cars. German technology"}, + {"name": "Kia", "description": "Great cars. Korean technology"}, + {"name": "Toyota", "description": "Great cars. Japanese technology"}, + ] + + car_make_instances = [] + for data in car_make_data: + car_make_instances.append( + CarMake.objects.create( + name=data['name'], + description=data['description'] + ) + ) + + # Create CarModel instances with the corresponding CarMake instances + car_model_data = [ + { + "name": "Pathfinder", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[0], + }, + { + "name": "Qashqai", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[0], + }, + { + "name": "XTRAIL", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[0], + }, + { + "name": "A-Class", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[1], + }, + { + "name": "C-Class", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[1], + }, + { + "name": "E-Class", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[1], + }, + { + "name": "A4", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[2], + }, + { + "name": "A5", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[2], + }, + { + "name": "A6", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[2], + }, + { + "name": "Sorrento", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[3], + }, + { + "name": "Carnival", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[3], + }, + { + "name": "Cerato", + "type": "Sedan", + "year": 2023, + "car_make": car_make_instances[3], + }, + { + "name": "Corolla", + "type": "Sedan", + "year": 2023, + "car_make": car_make_instances[4], + }, + { + "name": "Camry", + "type": "Sedan", + "year": 2023, + "car_make": car_make_instances[4], + }, + { + "name": "Kluger", + "type": "SUV", + "year": 2023, + "car_make": car_make_instances[4], + }, + # Add more CarModel instances as needed + ] + + for data in car_model_data: + CarModel.objects.create( + name=data['name'], + car_make=data['car_make'], + type=data['type'], + year=data['year'] + ) diff --git a/server/djangoapp/restapis.py b/server/djangoapp/restapis.py index 90709d9e3b..5ef0d23257 100644 --- a/server/djangoapp/restapis.py +++ b/server/djangoapp/restapis.py @@ -1,5 +1,5 @@ # Uncomment the imports below before you add the function code -# import requests +import requests import os from dotenv import load_dotenv @@ -11,12 +11,47 @@ 'sentiment_analyzer_url', default="http://localhost:5050/") -# def get_request(endpoint, **kwargs): + +def get_request(endpoint, **kwargs): + params = "" + if kwargs: + for key, value in kwargs.items(): + params = params + key + "=" + value + "&" + + request_url = backend_url + endpoint + "?" + params + + print("GET from {} ".format(request_url)) + try: + # Call get method of requests library with URL and parameters + response = requests.get(request_url) + return response.json() + except Exception as e: + # If any error occurs + print(f"Network exception occurred: {e}") + # Add code for get requests to back end -# def analyze_review_sentiments(text): + +def analyze_review_sentiments(text): + request_url = sentiment_analyzer_url + "analyze/" + text + try: + # Call get method of requests library with URL and parameters + response = requests.get(request_url) + return response.json() + except Exception as err: + print(f"Unexpected {err=}, {type(err)=}") + print("Network exception occurred") + # request_url = sentiment_analyzer_url+"analyze/"+text # Add code for retrieving sentiments -# def post_review(data_dict): + +def post_review(data_dict): + request_url = backend_url + "/insert_review" + try: + response = requests.post(request_url, json=data_dict) + print(response.json()) + return response.json() + except Exception as e: + print(f"Network exception occurred: {e}") # Add code for posting review diff --git a/server/djangoapp/urls.py b/server/djangoapp/urls.py index 0edc274f90..f02139bd1f 100644 --- a/server/djangoapp/urls.py +++ b/server/djangoapp/urls.py @@ -1,18 +1,31 @@ -# Uncomment the imports before you add the code -# from django.urls import path +""" Uncomment the imports before you add the code """ +from django.urls import path from django.conf.urls.static import static from django.conf import settings -# from . import views +from . import views app_name = 'djangoapp' urlpatterns = [ - # # path for registration - + # path for registration # path for login - # path(route='login', view=views.login_user, name='login'), - - # path for dealer reviews view + path(route='login', view=views.login_user, name='login'), + path('logout/', views.logout_user, name='logout'), + path('register/', views.registration, name='register'), + path(route='get_cars', view=views.get_cars, name='getcars'), + path(route='get_dealers/', view=views.get_dealerships, name='get_dealers'), + path( + route='get_dealers/', + view=views.get_dealerships, + name='get_dealers_by_state'), + path( + route='dealer/', + view=views.get_dealer_details, + name='dealer_details'), + path( + route='reviews/dealer/', + view=views.get_dealer_reviews, + name='dealer_details'), + path(route='add_review', view=views.add_review, name='add_review'), # path for add a review view - -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/server/djangoapp/views.py b/server/djangoapp/views.py index b16409f419..8293ed2056 100644 --- a/server/djangoapp/views.py +++ b/server/djangoapp/views.py @@ -1,43 +1,89 @@ # Uncomment the required imports before adding the code - -# from django.shortcuts import render -# from django.http import HttpResponseRedirect, HttpResponse -# from django.contrib.auth.models import User -# from django.shortcuts import get_object_or_404, render, redirect -# from django.contrib.auth import logout -# from django.contrib import messages -# from datetime import datetime - +from django.contrib.auth.models import User +from django.contrib.auth import logout, login, authenticate from django.http import JsonResponse -from django.contrib.auth import login, authenticate +from django.views.decorators.csrf import csrf_exempt +from .populate import initiate +from .restapis import get_request, analyze_review_sentiments, post_review +from .models import CarMake, CarModel import logging import json -from django.views.decorators.csrf import csrf_exempt -# from .populate import initiate # Get an instance of a logger logger = logging.getLogger(__name__) +# Get list of cars + + +def get_cars(request): + count = CarMake.objects.filter().count() + if count == 0: + initiate() + car_models = CarModel.objects.select_related('car_make') + cars = [] + for car_model in car_models: + cars.append({"CarModel": car_model.name, + "CarMake": car_model.car_make.name}) + return JsonResponse({"CarModels": cars}) -# Create your views here. # Create a `login_request` view to handle sign in request + @csrf_exempt def login_user(request): - # Get username and password from request.POST dictionary data = json.loads(request.body) username = data['userName'] password = data['password'] - # Try to check if provide credential can be authenticated user = authenticate(username=username, password=password) - data = {"userName": username} + resp_data = {"userName": username} if user is not None: - # If user is valid, call login method to login current user login(request, user) - data = {"userName": username, "status": "Authenticated"} - return JsonResponse(data) + resp_data = {"userName": username, "status": "Authenticated"} + return JsonResponse(resp_data) + +def logout_user(request): + logout(request) + return JsonResponse({"userName": ""}) + + +@csrf_exempt +def registration(request): + data = json.loads(request.body) + username = data['userName'] + password = data['password'] + first_name = data['firstName'] + last_name = data['lastName'] + email = data['email'] + username_exist = False + try: + User.objects.get(username=username) + username_exist = True + except Exception: + logger.debug( + f"{username} is new user" + ) + if not username_exist: + user = User.objects.create_user( + username=username, + first_name=first_name, + last_name=last_name, + password=password, + email=email + ) + login(request, user) + return JsonResponse({ + "userName": username, + "status": "Authenticated" + }) + else: + return JsonResponse({ + "userName": username, + "error": "Already Registered" + }) + +# # Create a `logout_request` view to handle sign out request # def logout_request(request): # ... @@ -47,19 +93,68 @@ def login_user(request): # def registration(request): # ... -# # Update the `get_dealerships` view to render the index page with + +# Update the `get_dealerships` render list of dealerships +# all by default, particular state if state is passed + + +def get_dealerships(request, state="All"): + if state == "All": + endpoint = "/fetchDealers" + else: + endpoint = f"/fetchDealers/{state}" + dealerships = get_request(endpoint) + return JsonResponse({"status": 200, "dealers": dealerships}) + # a list of dealerships # def get_dealerships(request): # ... -# Create a `get_dealer_reviews` view to render the reviews of a dealer + +def get_dealer_reviews(request, dealer_id): + if dealer_id: + endpoint = f"/fetchReviews/dealer/{dealer_id}" + reviews = get_request(endpoint) + for review_detail in reviews: + review_detail['sentiment'] = ( + analyze_review_sentiments(review_detail['review'])['sentiment'] + ) + return JsonResponse({"status": 200, "reviews": reviews}) + else: + return JsonResponse({"status": 400, "message": "Bad Request"}) + + # def get_dealer_reviews(request,dealer_id): # ... -# Create a `get_dealer_details` view to render the dealer details + +def get_dealer_details(request, dealer_id): + if dealer_id: + endpoint = f"/fetchDealer/{dealer_id}" + dealership = get_request(endpoint) + return JsonResponse({"status": 200, "dealer": dealership}) + else: + return JsonResponse({"status": 400, "message": "Bad Request"}) + + # def get_dealer_details(request, dealer_id): # ... # Create a `add_review` view to submit a review -# def add_review(request): + +def add_review(request): + if not request.user.is_anonymous: + data = json.loads(request.body) + try: + post_review(data) + return JsonResponse({"status": 200}) + except Exception: + return JsonResponse({ + "status": 401, + "message": "Error in posting review" + }) + else: + return JsonResponse({"status": 403, "message": "Unauthorized"}) + + # ... diff --git a/server/djangoproj/settings.py b/server/djangoproj/settings.py index e0b1092a5c..50961afc04 100644 --- a/server/djangoproj/settings.py +++ b/server/djangoproj/settings.py @@ -1,18 +1,15 @@ """ Django settings for djangoproj project. - Generated by 'django-admin startproject' using Django 3.2.5. - For more information on this file, see https://docs.djangoproject.com/en/3.2/topics/settings/ - For the full list of settings and their values, see https://docs.djangoproject.com/en/3.2/ref/settings/ """ import os from pathlib import Path - +import dj_database_url # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -22,15 +19,26 @@ # See https://docs.djangoproject.com/en/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY =\ - 'django-insecure-ccow$tz_=9%dxu4(0%^(z%nx32#s@(zt9$ih@)5l54yny)wm-0' +SECRET_KEY = os.environ.get('SECRET_KEY', 'django-insecure-ccow$tz_=9%dxu4(0%^(z%nx32#s@(zt9$ih@)5l54yny)wm-0') # SECURITY WARNING: don't run with debug turned on in production! -DEBUG = True - -ALLOWED_HOSTS = [] -CSRF_TRUSTED_ORIGINS = [] - +DEBUG = os.environ.get('DEBUG', 'True').lower() == 'true' + +ALLOWED_HOSTS = [ + 'localhost', + '127.0.0.1', + 'https://rjindal-8000.' + 'theianext-1-labs-prod-misc-tools-us-east-0.' + 'proxy.cognitiveclass.ai', + '.onrender.com' +] +CSRF_TRUSTED_ORIGINS = [ + 'https://rjindal-8000.theianext-1-labs-prod-misc-tools-us-east-0.' + 'proxy.cognitiveclass.ai', + 'https://rjindal-8000.theiadockernext-0-labs-prod-theiak8s-4-tor01.' + 'proxy.cognitiveclass.ai', + 'https://*.onrender.com' +] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [], } @@ -46,7 +54,6 @@ 'django.contrib.messages', 'django.contrib.staticfiles', ] - MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -55,13 +62,16 @@ 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] - ROOT_URLCONF = 'djangoproj.urls' TEMPLATES = [ { 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], + 'DIRS': [ + os.path.join(BASE_DIR, 'frontend/static'), + os.path.join(BASE_DIR, 'frontend/build'), + os.path.join(BASE_DIR, 'frontend/build/static'), + ], 'APP_DIRS': True, 'OPTIONS': { 'context_processors': [ @@ -73,7 +83,6 @@ }, }, ] - WSGI_APPLICATION = 'djangoproj.wsgi.application' @@ -81,16 +90,18 @@ # https://docs.djangoproject.com/en/3.2/ref/settings/#databases DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': BASE_DIR / 'db.sqlite3', - } + 'default': dj_database_url.config( + default='sqlite:///' + str(BASE_DIR / 'db.sqlite3'), + conn_max_age=600, + conn_health_checks=True, + ) } AUTH_PASSWORD_VALIDATORS = [ { 'NAME': - 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + 'django.contrib.auth.password_validation.' + 'UserAttributeSimilarityValidator', }, { 'NAME': @@ -105,11 +116,8 @@ 'django.contrib.auth.password_validation.NumericPasswordValidator', }, ] - - # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ - LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' @@ -134,5 +142,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' -STATICFILES_DIRS = [] - +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'frontend/static'), + os.path.join(BASE_DIR, 'frontend/build/static'), +] diff --git a/server/djangoproj/urls.py b/server/djangoproj/urls.py index 6808da9141..2d6b6c9a3f 100644 --- a/server/djangoproj/urls.py +++ b/server/djangoproj/urls.py @@ -23,4 +23,25 @@ path('admin/', admin.site.urls), path('djangoapp/', include('djangoapp.urls')), path('', TemplateView.as_view(template_name="Home.html")), + path('about/', TemplateView.as_view(template_name="About.html")), + path('contact/', TemplateView.as_view(template_name="Contact.html")), + path('login/', TemplateView.as_view(template_name="index.html")), + path('register/', TemplateView.as_view(template_name="index.html")), + path('dealers/', TemplateView.as_view(template_name="index.html")), + path( + 'dealer/', + TemplateView.as_view(template_name="index.html") + ), + path( + 'postreview/', + TemplateView.as_view(template_name="index.html") + ), + path( + 'dealer/', + TemplateView.as_view(template_name="index.html") + ), + path( + 'postreview/', + TemplateView.as_view(template_name="index.html") + ), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100644 index 0000000000..863742d778 --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +# Make migrations and migrate the database. +echo "Making migrations and migrating the database. " +python manage.py makemigrations --noinput +python manage.py migrate --noinput +python manage.py collectstatic --noinput +exec "$@" diff --git a/server/frontend/package-lock.json b/server/frontend/package-lock.json index 0797425307..bdb21fad35 100644 --- a/server/frontend/package-lock.json +++ b/server/frontend/package-lock.json @@ -16,6 +16,9 @@ "react-router-dom": "^6.19.0", "react-scripts": "5.0.1", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -646,9 +649,18 @@ } }, "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "version": "7.21.11", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.11.tgz", + "integrity": "sha512-0QZ8qP/3RLDVBwBFoWAwCtgcDZJVwA5LUJRZU8x2YFfKNuFq161wK3cuGrALu5yiPu+vzwTAg/sMWVNeWeNyaw==", + "deprecated": "This proposal has been merged to the ECMAScript standard and thus this plugin is no longer maintained. Please use @babel/plugin-transform-private-property-in-object instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.18.6", + "@babel/helper-create-class-features-plugin": "^7.21.0", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, "engines": { "node": ">=6.9.0" }, @@ -1891,6 +1903,18 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/preset-env/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", diff --git a/server/frontend/src/App.js b/server/frontend/src/App.js index aceac6974d..8ff9ecc565 100644 --- a/server/frontend/src/App.js +++ b/server/frontend/src/App.js @@ -1,10 +1,18 @@ import LoginPanel from "./components/Login/Login" import { Routes, Route } from "react-router-dom"; +import Register from './components/Register/Register'; +import Dealers from './components/Dealers/Dealers'; +import Dealer from "./components/Dealers/Dealer" +import PostReview from "./components/Dealers/PostReview" function App() { return ( } /> + } /> + } /> + } /> + } /> ); } diff --git a/server/frontend/src/components/Register/Register.jsx b/server/frontend/src/components/Register/Register.jsx new file mode 100644 index 0000000000..104366157f --- /dev/null +++ b/server/frontend/src/components/Register/Register.jsx @@ -0,0 +1,101 @@ +import React, { useState } from "react"; +import "./Register.css"; +import user_icon from "../assets/person.png" +import email_icon from "../assets/email.png" +import password_icon from "../assets/password.png" +import close_icon from "../assets/close.png" + +const Register = () => { +// State variables for form inputs + const [userName, setUserName] = useState(""); + const [password, setPassword] = useState(""); + const [email, setEmail] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setlastName] = useState(""); + +// Redirect to home + const gohome = ()=> { + window.location.href = window.location.origin; + } + +// Handle form submission + const register = async (e) => { + e.preventDefault(); + + let register_url = window.location.origin+"/djangoapp/register/"; + +// Send POST request to register endpoint + const res = await fetch(register_url, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + "userName": userName, + "password": password, + "firstName":firstName, + "lastName":lastName, + "email":email + }), + }); + + const json = await res.json(); + if (json.status) { + // Save username in session and reload home + sessionStorage.setItem('username', json.userName); + window.location.href = window.location.origin; + } + else if (json.error === "Already Registered") { + alert("The user with same username is already registered"); + window.location.href = window.location.origin; + } +}; + + return( +
+ + +
+
+
+ Username + setUserName(e.target.value)}/> +
+
+ First Name + setFirstName(e.target.value)}/> +
+ +
+ Last Name + setlastName(e.target.value)}/> +
+ +
+ Email + setEmail(e.target.value)}/> +
+ +
+ password + setPassword(e.target.value)}/> +
+ +
+
+ +
+
+
+ ) +} + +export default Register; \ No newline at end of file diff --git a/server/frontend/static/About.html b/server/frontend/static/About.html index 484efd960f..d048b8c0c0 100644 --- a/server/frontend/static/About.html +++ b/server/frontend/static/About.html @@ -1,6 +1,7 @@ - + +