diff --git a/.github/workflows/create_main.yml b/.github/workflows/create_main.yml new file mode 100644 index 0000000000..1d5c207add --- /dev/null +++ b/.github/workflows/create_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/.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/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000000..6795963570 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,25 @@ + FROM python:3.12.0-slim-bookworm + + ENV PYTHONBUFFERED 1 + ENV PYTHONWRITEBYTECODE 1 + + ENV APP=/app + + # Change the workdir. + WORKDIR $APP + + # Install the requirements + COPY requirements.txt $APP + + RUN pip3 install -r requirements.txt + + # Copy the rest of the files + COPY . $APP + + EXPOSE 8000 + + RUN chmod +x /app/entrypoint.sh + + ENTRYPOINT ["/bin/bash","/app/entrypoint.sh"] + + CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "djangoproj.wsgi"] \ No newline at end of file diff --git a/server/database/app.js b/server/database/app.js index 00f52b2008..2e4f53b6ee 100644 --- a/server/database/app.js +++ b/server/database/app.js @@ -1,42 +1,44 @@ +/* jshint node: true, esversion: 8 */ +'use strict'; + const express = require('express'); const mongoose = require('mongoose'); const fs = require('fs'); -const cors = require('cors') -const app = express() -const port = 3030; +const cors = require('cors'); +const bodyParser = require('body-parser'); -app.use(cors()) -app.use(require('body-parser').urlencoded({ extended: false })); +const app = express(); +const port = 3030; -const reviews_data = JSON.parse(fs.readFileSync("reviews.json", 'utf8')); -const dealerships_data = JSON.parse(fs.readFileSync("dealerships.json", 'utf8')); +app.use(cors()); +app.use(bodyParser.urlencoded({ extended: false })); -mongoose.connect("mongodb://mongo_db:27017/",{'dbName':'dealershipsDB'}); +const reviewsData = JSON.parse(fs.readFileSync('reviews.json', 'utf8')); +const dealershipsData = JSON.parse(fs.readFileSync('dealerships.json', 'utf8')); +mongoose.connect('mongodb://mongo_db:27017/', { dbName: 'dealershipsDB' }); const Reviews = require('./review'); - const Dealerships = require('./dealership'); -try { - Reviews.deleteMany({}).then(()=>{ - Reviews.insertMany(reviews_data['reviews']); - }); - Dealerships.deleteMany({}).then(()=>{ - Dealerships.insertMany(dealerships_data['dealerships']); - }); - -} catch (error) { - res.status(500).json({ error: 'Error fetching documents' }); -} - - -// Express route to home -app.get('/', async (req, res) => { - res.send("Welcome to the Mongoose API") +// Seed data +(async () => { + try { + await Reviews.deleteMany({}); + await Reviews.insertMany(reviewsData.reviews); + + await Dealerships.deleteMany({}); + await Dealerships.insertMany(dealershipsData.dealerships); + } catch (error) { + console.error('Error seeding database:', error); + } +})(); + +// Routes +app.get('/', (req, res) => { + res.send('Welcome to the Mongoose API'); }); -// Express route to fetch all reviews app.get('/fetchReviews', async (req, res) => { try { const documents = await Reviews.find(); @@ -46,59 +48,75 @@ app.get('/fetchReviews', async (req, res) => { } }); -// Express route to fetch reviews by a particular dealer app.get('/fetchReviews/dealer/:id', async (req, res) => { try { - const documents = await Reviews.find({dealership: req.params.id}); + const documents = await Reviews.find({ dealership: req.params.id }); res.json(documents); } catch (error) { res.status(500).json({ error: 'Error fetching documents' }); } }); -// Express route to fetch all dealerships app.get('/fetchDealers', async (req, res) => { -//Write your code here + try { + const dealers = await Dealerships.find(); + res.json(dealers); + } catch (error) { + res.status(500).json({ error: 'Error fetching dealers' }); + } }); -// Express route to fetch Dealers by a particular state app.get('/fetchDealers/:state', async (req, res) => { -//Write your code here + try { + const state = req.params.state; + const dealers = await Dealerships.find({ state }); + res.json(dealers); + } catch (error) { + res.status(500).json({ error: 'Error fetching dealers by state' }); + } }); -// Express route to fetch dealer by a particular id app.get('/fetchDealer/:id', async (req, res) => { -//Write your code here + try { + const dealerId = parseInt(req.params.id, 10); + const dealer = await Dealerships.findOne({ id: dealerId }); + + if (!dealer) { + return res.status(404).json({ error: 'Dealer not found' }); + } + + res.json(dealer); + } 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 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'], - }); - try { + const data = JSON.parse(req.body); + const documents = await Reviews.find().sort({ id: -1 }); + const newId = documents.length > 0 ? documents[0].id + 1 : 1; + + const review = new Reviews({ + id: newId, + 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 + }); + const savedReview = await review.save(); res.json(savedReview); } catch (error) { - console.log(error); + console.error('Insert review error:', error); res.status(500).json({ error: 'Error inserting review' }); } }); -// Start the Express server app.listen(port, () => { console.log(`Server is running on http://localhost:${port}`); }); diff --git a/server/database/dealership.js b/server/database/dealership.js index b10d6b4730..9c715196b1 100644 --- a/server/database/dealership.js +++ b/server/database/dealership.js @@ -1,43 +1,48 @@ -const mongoose = require('mongoose'); - -const Schema = mongoose.Schema; - -const dealerships = new Schema({ - id: { - type: Number, - required: true, - }, - city: { - type: String, - required: true - }, - state: { - type: String, - required: true - }, - address: { - type: String, - required: true - }, - zip: { - type: String, - required: true - }, - lat: { - type: String, - required: true - }, - long: { - type: String, - required: true - }, - short_name: { - type: String, - }, - full_name: { - type: String, - required: true - } -}); - -module.exports = mongoose.model('dealerships', dealerships); +/* jshint node: true, esversion: 6 */ +(function () { + 'use strict'; + + var mongoose = require('mongoose'); + var Schema = mongoose.Schema; + + var dealershipSchema = new Schema({ + id: { + type: Number, + required: true + }, + city: { + type: String, + required: true + }, + state: { + type: String, + required: true + }, + address: { + type: String, + required: true + }, + zip: { + type: String, + required: true + }, + lat: { + type: String, + required: true + }, + long: { + type: String, + required: true + }, + short_name: { + type: String + }, + full_name: { + type: String, + required: true + } + }); + + module.exports = mongoose.model('Dealership', dealershipSchema); + })(); + \ No newline at end of file diff --git a/server/database/inventory.js b/server/database/inventory.js index 2c22fd092c..c1ad0cbd49 100644 --- a/server/database/inventory.js +++ b/server/database/inventory.js @@ -1,33 +1,37 @@ -const { Int32 } = require('mongodb'); -const mongoose = require('mongoose'); - -const Schema = mongoose.Schema; - -const cars = new Schema({ -dealer_id: { - type: Number, - required: true -}, -make: { - type: String, - required: true - }, -model: { - type: String, - required: true - }, -bodyType: { - type: String, - required: true - }, -year: { - type: Number, - required: true - }, -mileage: { - type: Number, - required: true - } -}); - -module.exports = mongoose.model('cars', cars); +/* jshint node: true, esversion: 6 */ +(function () { + 'use strict'; + + const mongoose = require('mongoose'); + const { Schema } = mongoose; + + const carSchema = new Schema({ + dealer_id: { + type: Number, + required: true + }, + make: { + type: String, + required: true + }, + model: { + type: String, + required: true + }, + bodyType: { + type: String, + required: true + }, + year: { + type: Number, + required: true + }, + mileage: { + type: Number, + required: true + } + }); + + module.exports = mongoose.model('Car', carSchema); + })(); + \ No newline at end of file diff --git a/server/database/review.js b/server/database/review.js index 4759725a3a..a994a90b12 100644 --- a/server/database/review.js +++ b/server/database/review.js @@ -1,44 +1,49 @@ -const mongoose = require('mongoose'); - -const Schema = mongoose.Schema; - -const reviews = new Schema({ - id: { - type: Number, - required: true, - }, - name: { - type: String, - required: true - }, - dealership: { - type: Number, - required: true, - }, - review: { - type: String, - required: true - }, - purchase: { - type: Boolean, - required: true - }, - purchase_date: { - type: String, - required: true - }, - car_make: { - type: String, - required: true - }, - car_model: { - type: String, - required: true - }, - car_year: { - type: Number, - required: true - }, -}); - -module.exports = mongoose.model('reviews', reviews); +/* jshint node: true, esversion: 6 */ +(function () { + 'use strict'; + + const mongoose = require('mongoose'); + const { Schema } = mongoose; + + const reviewSchema = new Schema({ + id: { + type: Number, + required: true + }, + name: { + type: String, + required: true + }, + dealership: { + type: Number, + required: true + }, + review: { + type: String, + required: true + }, + purchase: { + type: Boolean, + required: true + }, + purchase_date: { + type: String, + required: true + }, + car_make: { + type: String, + required: true + }, + car_model: { + type: String, + required: true + }, + car_year: { + type: Number, + required: true + } + }); + + module.exports = mongoose.model('Review', reviewSchema); + })(); + \ No newline at end of file diff --git a/server/deployment.yaml b/server/deployment.yaml new file mode 100644 index 0000000000..17d713529b --- /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/sn-labs-madhubalasel/dealership:latest + imagePullPolicy: Always + name: dealership + ports: + - containerPort: 8000 + protocol: TCP + restartPolicy: Always \ No newline at end of file diff --git a/server/djangoapp/.env b/server/djangoapp/.env index 01822e542a..a57c3ed7d2 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://madhubalasel-3030.theiadockernext-1-labs-prod-theiak8s-4-tor01.proxy.cognitiveclass.ai +sentiment_analyzer_url=https://sentianalyzer.1vhcizaztzl9.us-south.codeengine.appdomain.cloud/ 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..cc58c50606 100644 --- a/server/djangoapp/models.py +++ b/server/djangoapp/models.py @@ -1,25 +1,47 @@ -# Uncomment the following imports before adding the Model code +from django.db import models +from django.core.validators import MaxValueValidator, MinValueValidator +from datetime import datetime -# from django.db import models -# from django.utils.timezone import now -# from django.core.validators import MaxValueValidator, MinValueValidator +class CarMake(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + # Optional additional fields + country = models.CharField(max_length=100, default="Unknown", blank=True) + founded_year = models.IntegerField( + default=1900, + validators=[ + MinValueValidator(1800), + MaxValueValidator(datetime.now().year), + ], + ) -# Create your models here. + def __str__(self): + return self.name -# 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 +class CarModel(models.Model): + car_make = models.ForeignKey(CarMake, on_delete=models.CASCADE) + name = models.CharField(max_length=100) -# 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 + CAR_TYPES = [ + ('SEDAN', 'Sedan'), + ('SUV', 'SUV'), + ('WAGON', 'Wagon'), + ('COUPE', 'Coupe'), + ('TRUCK', 'Truck'), + ('CONVERTIBLE', 'Convertible'), + ] + + type = models.CharField(max_length=15, choices=CAR_TYPES, default='SUV') + year = models.IntegerField( + default=datetime.now().year, + validators=[ + MinValueValidator(1990), + MaxValueValidator(datetime.now().year), + ], + ) + dealer_id = models.IntegerField() # Refers to a dealer in Cloudant DB + + def __str__(self): + return f"{self.car_make.name} {self.name} ({self.year})" diff --git a/server/djangoapp/populate.py b/server/djangoapp/populate.py index 1927e09e18..239af4eaca 100644 --- a/server/djangoapp/populate.py +++ b/server/djangoapp/populate.py @@ -1,2 +1,106 @@ +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 = CarMake.objects.create( + name=data['name'], + description=data['description'] + ) + car_make_instances.append(car_make) + + 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] + }, + ] + + for data in car_model_data: + CarModel.objects.create( + name=data['name'], + car_make=data['car_make'], + type=data['type'], + year=data['year'], + dealer_id=1 + ) diff --git a/server/djangoapp/restapis.py b/server/djangoapp/restapis.py index 90709d9e3b..6fd9b0438c 100644 --- a/server/djangoapp/restapis.py +++ b/server/djangoapp/restapis.py @@ -1,22 +1,56 @@ -# Uncomment the imports below before you add the function code -# import requests import os +import requests from dotenv import load_dotenv load_dotenv() backend_url = os.getenv( - 'backend_url', default="http://localhost:3030") + 'backend_url', default='http://localhost:3030' +) sentiment_analyzer_url = os.getenv( 'sentiment_analyzer_url', - default="http://localhost:5050/") + default='http://localhost:5050/' +) -# def get_request(endpoint, **kwargs): -# Add code for get requests to back end -# def analyze_review_sentiments(text): -# request_url = sentiment_analyzer_url+"analyze/"+text -# Add code for retrieving sentiments +def get_request(endpoint, **kwargs): + """Make a GET request to the backend service.""" + params = '' + if kwargs: + for key, value in kwargs.items(): + params = f'{params}{key}={value}&' -# def post_review(data_dict): -# Add code for posting review + request_url = f'{backend_url}{endpoint}?{params}' + print(f'GET from {request_url}') + + try: + response = requests.get(request_url) + response.raise_for_status() + return response.json() or [] + except Exception as e: + print(f'Network exception occurred: {e}') + return [] + + +def analyze_review_sentiments(text): + """Call the sentiment analyzer service.""" + request_url = f'{sentiment_analyzer_url}analyze/{text}' + print(f'GET from {request_url}') + + try: + response = requests.get(request_url) + response.raise_for_status() + return response.json() + except Exception as e: + print(f'Unexpected error: {e}') + return {'sentiment': 'neutral'} + + +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: + print("Network exception occurred") diff --git a/server/djangoapp/urls.py b/server/djangoapp/urls.py index 0edc274f90..e88ae98550 100644 --- a/server/djangoapp/urls.py +++ b/server/djangoapp/urls.py @@ -1,18 +1,42 @@ -# Uncomment the imports before you add the code -# from django.urls import path +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 +urlpatterns = [ # path for login - # path(route='login', view=views.login_user, name='login'), + path(route='login', view=views.login_user, name='login'), + path(route='logout', view=views.logout_request, name='logout'), + path(route='register', view=views.registration, name='register'), + + # path for getting car models + path(route='get_cars', view=views.get_cars, name='getcars'), - # path for dealer reviews view + # path for fetching dealers + 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 for add a review view + # path for dealer details and reviews + path( + route='dealer/', + view=views.get_dealer_details, + name='dealer_details' + ), + path( + route='reviews/dealer/', + view=views.get_dealer_reviews, + name='dealer_reviews' + ), + path(route='reviews/dealer/', + view=views.get_dealer_reviews, + name='dealer_details'), + # path for adding a review + path(route='add_review', view=views.add_review, name='add_review'), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/server/djangoapp/views.py b/server/djangoapp/views.py index b16409f419..5ec5199059 100644 --- a/server/djangoapp/views.py +++ b/server/djangoapp/views.py @@ -1,65 +1,180 @@ -# 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 import logging import json -from django.views.decorators.csrf import csrf_exempt -# from .populate import initiate + +from .models import CarModel, CarMake +from .restapis import get_request, analyze_review_sentiments, post_review +from .populate import initiate # Get an instance of a logger logger = logging.getLogger(__name__) -# 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} - 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) - -# Create a `logout_request` view to handle sign out request -# def logout_request(request): -# ... - -# Create a `registration` view to handle sign up request -# @csrf_exempt -# def registration(request): -# ... - -# # Update the `get_dealerships` view to render the index page with -# 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): -# ... - -# Create a `get_dealer_details` view to render the dealer details -# def get_dealer_details(request, dealer_id): -# ... - -# Create a `add_review` view to submit a review -# def add_review(request): -# ... + if request.method == "POST": + try: + data = json.loads(request.body) + username = data.get('userName') + password = data.get('password') + + if not username or not password: + return JsonResponse( + {"error": "Username and password are required"}, status=400 + ) + + user = authenticate(username=username, password=password) + + if user is not None: + login(request, user) + return JsonResponse({ + "userName": username, + "status": "Authenticated" + }) + + else: + return JsonResponse( + {"userName": username, + "status": "Invalid credentials"}, + status=401 + ) + except json.JSONDecodeError: + return JsonResponse( + {"userName": username, + "error": "Username already registered"}, + status=400, + ) + + +@csrf_exempt +def logout_request(request): + if request.method == "POST": + logout(request) + return JsonResponse({"userName": ""}) + return JsonResponse({"error": "POST method required"}, status=405) + + +@csrf_exempt +def registration(request): + if request.method == "POST": + try: + data = json.loads(request.body) + username = data['userName'] + password = data['password'] + first_name = data['firstName'] + last_name = data['lastName'] + email = data['email'] + except (json.JSONDecodeError, KeyError): + return JsonResponse({"error": "Invalid input data"}, status=400) + + username_exist = False + email_exist = False + + try: + User.objects.get(username=username) + username_exist = True + except User.DoesNotExist: + logger.debug(f"{username} is a new user") + + if not username_exist: + try: + User.objects.get(email=email) + email_exist = True + except User.DoesNotExist: + logger.debug(f"{email} is not registered with any user") + + if username_exist: + return JsonResponse( + { + "userName": username, + "error": "Username already registered" + }, + status=400, + ) + elif email_exist: + return JsonResponse( + { + "email": email, + "error": "Email already registered" + }, + status=400, + ) + + try: + 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" + }) + + except Exception as e: + logger.error(f"Error during user creation: {str(e)}") + return JsonResponse({"error": "Error creating user"}, status=500) + + return JsonResponse({"error": "POST method required"}, status=405) + + +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}) + + +def get_cars(request): + count = CarMake.objects.filter().count() + print(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}) + + +def get_dealer_details(request, dealer_id): + if(dealer_id): + endpoint = "/fetchDealer/"+str(dealer_id) + dealership = get_request(endpoint) + return JsonResponse({"status":200,"dealer":dealership}) + else: + return JsonResponse({"status":400,"message":"Bad Request"}) + +def get_dealer_reviews(request, dealer_id): + # if dealer id has been provided + if(dealer_id): + endpoint = "/fetchReviews/dealer/"+str(dealer_id) + reviews = get_request(endpoint) + for review_detail in reviews: + response = analyze_review_sentiments(review_detail['review']) + print(response) + review_detail['sentiment'] = response['sentiment'] + return JsonResponse({"status":200,"reviews":reviews}) + else: + return JsonResponse({"status":400,"message":"Bad Request"}) + +def add_review(request): + if(request.user.is_anonymous == False): + data = json.loads(request.body) + try: + response = post_review(data) + return JsonResponse({"status":200}) + except: + return JsonResponse({"status":401,"message":"Error in posting review"}) + else: + return JsonResponse({"status":403,"message":"Unauthorized"}) \ No newline at end of file diff --git a/server/djangoproj/settings.py b/server/djangoproj/settings.py index e0b1092a5c..af33426249 100644 --- a/server/djangoproj/settings.py +++ b/server/djangoproj/settings.py @@ -13,23 +13,32 @@ import os 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/3.2/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY =\ +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 = [] +ALLOWED_HOSTS = [ + 'localhost', + 'madhubalasel-8000.theiadockernext-1-labs-prod-theiak8s-4-tor01.proxy.cognitiveclass.ai' +] + + +CSRF_TRUSTED_ORIGINS = [ + 'https://madhubalasel-8000.theiadockernext-1-labs-prod-theiak8s-4-tor01.proxy.cognitiveclass.ai' +] + + + REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [], @@ -61,7 +70,11 @@ 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': [ @@ -76,7 +89,6 @@ WSGI_APPLICATION = 'djangoproj.wsgi.application' - # Database # https://docs.djangoproject.com/en/3.2/ref/settings/#databases @@ -89,24 +101,31 @@ AUTH_PASSWORD_VALIDATORS = [ { - 'NAME': - 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + 'NAME': ( + 'django.contrib.auth.password_validation.' + 'UserAttributeSimilarityValidator' + ), }, { - 'NAME': - 'django.contrib.auth.password_validation.MinimumLengthValidator', + 'NAME': ( + 'django.contrib.auth.password_validation.' + 'MinimumLengthValidator' + ), }, { - 'NAME': - 'django.contrib.auth.password_validation.CommonPasswordValidator', + 'NAME': ( + 'django.contrib.auth.password_validation.' + 'CommonPasswordValidator' + ), }, { - 'NAME': - 'django.contrib.auth.password_validation.NumericPasswordValidator', + 'NAME': ( + 'django.contrib.auth.password_validation.' + 'NumericPasswordValidator' + ), }, ] - # Internationalization # https://docs.djangoproject.com/en/3.2/topics/i18n/ @@ -120,7 +139,6 @@ USE_TZ = True - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/3.2/howto/static-files/ @@ -129,10 +147,13 @@ MEDIA_ROOT = os.path.join(STATIC_ROOT, 'media') MEDIA_URL = '/media/' +STATICFILES_DIRS = [ + os.path.join(BASE_DIR, 'frontend/static'), + os.path.join(BASE_DIR, 'frontend/build'), + os.path.join(BASE_DIR, 'frontend/build/static'), +] + # Default primary key field type # https://docs.djangoproject.com/en/3.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - -STATICFILES_DIRS = [] - diff --git a/server/djangoproj/urls.py b/server/djangoproj/urls.py index 6808da9141..7f75fa1e38 100644 --- a/server/djangoproj/urls.py +++ b/server/djangoproj/urls.py @@ -18,9 +18,24 @@ from django.views.generic import TemplateView from django.conf.urls.static import static from django.conf import settings +from django.http import HttpResponse urlpatterns = [ 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 for contact page + path( + 'contact/', + lambda request: HttpResponse( + open('frontend/static/contact.html').read(), + content_type='text/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")), ] + 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..b24c7e80a4 --- /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 "$@" \ No newline at end of file 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..e1ca4705a6 100644 --- a/server/frontend/src/App.js +++ b/server/frontend/src/App.js @@ -1,10 +1,18 @@ import LoginPanel from "./components/Login/Login" +import RegisterPanel from "./components/Register/Register"; import { Routes, Route } from "react-router-dom"; +import Dealers from './components/Dealers/Dealers'; +import Dealer from "./components/Dealers/Dealer" +import PostReview from "./components/Dealers/PostReview" function App() { return ( } /> + } /> {/* ✅ Register route */} + } /> + } /> + } /> ); } diff --git a/server/frontend/src/components/Dealers/Dealer.jsx b/server/frontend/src/components/Dealers/Dealer.jsx index 4c162e8a1b..5a31ffd3fa 100644 --- a/server/frontend/src/components/Dealers/Dealer.jsx +++ b/server/frontend/src/components/Dealers/Dealer.jsx @@ -31,8 +31,12 @@ const Dealer = () => { const retobj = await res.json(); if(retobj.status === 200) { - let dealerobjs = Array.from(retobj.dealer) - setDealer(dealerobjs[0]) + if (Array.isArray(retobj.dealer)) { + setDealer(retobj.dealer[0]); + } else { + setDealer(retobj.dealer); + } + } } @@ -70,10 +74,22 @@ const Dealer = () => { return(
-
-

{dealer.full_name}{postReview}

-

{dealer['city']},{dealer['address']}, Zip - {dealer['zip']}, {dealer['state']}

+
+ {dealer && dealer.full_name ? ( +

{dealer.full_name}{postReview}

+ ) : ( +
Loading dealer information...
+ )} + +

+ {dealer?.city || "City not available"}, + {dealer?.address || " Address not available"}, + Zip - {dealer?.zip || "N/A"}, + {dealer?.state || " State not available"} +

+
+
{reviews.length === 0 && unreviewed === false ? ( Loading Reviews.... @@ -90,4 +106,4 @@ return( ) } -export default Dealer +export default Dealer \ No newline at end of file diff --git a/server/frontend/src/components/Register/Register.jsx b/server/frontend/src/components/Register/Register.jsx new file mode 100644 index 0000000000..9f1a290327 --- /dev/null +++ b/server/frontend/src/components/Register/Register.jsx @@ -0,0 +1,98 @@ +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 = () => { + + const [userName, setUserName] = useState(""); + const [password, setPassword] = useState(""); + const [email, setEmail] = useState(""); + const [firstName, setFirstName] = useState(""); + const [lastName, setlastName] = useState(""); + + + const gohome = ()=> { + window.location.href = window.location.origin; + } + + const register = async (e) => { + e.preventDefault(); + + let register_url = window.location.origin+"/djangoapp/register"; + + 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) { + 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..051b25f1dc 100644 --- a/server/frontend/static/About.html +++ b/server/frontend/static/About.html @@ -1,11 +1,12 @@ - + +
-