diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..7d8ec75cd3 --- /dev/null +++ b/.flake8 @@ -0,0 +1,13 @@ +[flake8] +max-line-length = 100 +extend-ignore = E203, W503 +exclude = + .git, + __pycache__, + venv, + env, + node_modules, + build, + dist, + **/migrations/*, + server/frontend diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000000..a24832cb1b --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,29 @@ +name: Lint Code + +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] + +jobs: + Lint_Python: + 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 flake8 + run: | + python -m pip install --upgrade pip + pip install flake8 + + - name: Lint with flake8 (non-blocking) + run: | + flake8 server/djangoapp server/djangoproj --count --show-source --statistics --exit-zero + diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000000..7644f99610 --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12.0-slim-bookworm + +ENV PYTHONUNBUFFERED=1 +ENV PYTHONWRITEBYTECODE=1 +ENV APP=/app + +WORKDIR $APP + +# Copy dependency file and install requirements +COPY requirements.txt $APP +RUN pip install --no-cache-dir -r requirements.txt + +# Copy project files +COPY . $APP + +EXPOSE 8000 + +# Make entrypoint script executable +RUN chmod +x /app/entrypoint.sh + +# Run DB migrations automatically and start Gunicorn +ENTRYPOINT ["/bin/bash","/app/entrypoint.sh"] +CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "djangoproj.wsgi"] diff --git a/server/database/app.js b/server/database/app.js index 00f52b2008..124a4783c7 100644 --- a/server/database/app.js +++ b/server/database/app.js @@ -58,17 +58,42 @@ 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 dealers = await Dealerships.find({}).lean(); + res.status(200).json(dealers); + } catch (err) { + console.error('fetchDealers error:', err); + 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({ + $or: [ + { state: new RegExp(`^${state}$`, 'i') }, // if your schema has `state` + { st: new RegExp(`^${state}$`, 'i') }, // or `st` (2-letter) + ], + }).lean(); + res.status(200).json(dealers); + } catch (err) { + console.error('fetchDealers/:state error:', err); + 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 id = Number(req.params.id); + const dealer = await Dealerships.findOne({ id }).lean(); + if (!dealer) return res.status(404).json({ error: 'Dealer not found' }); + res.status(200).json(dealer); + } catch (err) { + console.error('fetchDealer/:id error:', err); + res.status(500).json({ error: 'Error fetching dealer' }); + } }); //Express route to insert review diff --git a/server/database/docker-compose.yml b/server/database/docker-compose.yml index 95c4909643..7a2b1702b8 100644 --- a/server/database/docker-compose.yml +++ b/server/database/docker-compose.yml @@ -1,22 +1,49 @@ version: '3.9' +# services: +# # Mongodb service +# mongo_db: +# container_name: db_container +# image: mongo:latest +# ports: +# - 27017:27017 +# restart: always +# volumes: +# - mongo_data:/data/db + +# # Node api service +# api: +# image: nodeapp +# ports: +# - 3030:3030 +# depends_on: +# - mongo_db + +# volumes: +# mongo_data: {} + + +# docker-compose.yml services: - # Mongodb service mongo_db: container_name: db_container - image: mongo:latest + image: mongo:6-jammy # good for Intel/Apple Silicon ports: - - 27017:27017 - restart: always + - "27017:27017" + restart: unless-stopped volumes: - mongo_data:/data/db - # Node api service api: - image: nodeapp + build: + context: . # <— build from the current folder + dockerfile: Dockerfile + image: nodemap:latest # optional: name the built image ports: - - 3030:3030 - depends_on: + - "3030:3030" + environment: + - MONGO_URL=mongodb://mongo_db:27017/dealerships # <— use service name, not localhost + depends_on: - mongo_db volumes: diff --git a/server/deployment.yaml b/server/deployment.yaml new file mode 100644 index 0000000000..c25fc8b08f --- /dev/null +++ b/server/deployment.yaml @@ -0,0 +1,29 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: dealership + labels: + run: dealership +spec: + replicas: 1 + selector: + matchLabels: + run: dealership + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 25% + maxUnavailable: 25% + template: + metadata: + labels: + run: dealership + spec: + containers: + - name: dealership + image: us.icr.io/sn-labs-cnabolouri/dealership:latest + imagePullPolicy: Always + ports: + - containerPort: 8000 + protocol: TCP + restartPolicy: Always diff --git a/server/djangoapp/.env b/server/djangoapp/.env index 01822e542a..d6f0842317 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://cnabolouri-3030.theiadockernext-0-labs-prod-theiak8s-4-tor01.proxy.cognitiveclass.ai/ +sentiment_analyzer_url = https://sentianalyzer.22a78yv1gizq.us-south.codeengine.appdomain.cloud \ No newline at end of file diff --git a/server/djangoapp/admin.py b/server/djangoapp/admin.py index 433657fc64..eff17ad943 100644 --- a/server/djangoapp/admin.py +++ b/server/djangoapp/admin.py @@ -1,13 +1,12 @@ -# from django.contrib import admin -# from .models import related models +from django.contrib import admin +from .models import CarMake, CarModel -# Register your models here. +@admin.register(CarMake) +class CarMakeAdmin(admin.ModelAdmin): + list_display = ('name', 'description') -# CarModelInline class -# CarModelAdmin class - -# CarMakeAdmin class with CarModelInline - -# Register models here +@admin.register(CarModel) +class CarModelAdmin(admin.ModelAdmin): + list_display = ('name', 'make', 'type', 'year') diff --git a/server/djangoapp/migrations/0001_initial.py b/server/djangoapp/migrations/0001_initial.py new file mode 100644 index 0000000000..2aad06da4b --- /dev/null +++ b/server/djangoapp/migrations/0001_initial.py @@ -0,0 +1,34 @@ +# Generated by Django 5.2.7 on 2025-11-04 01:03 + +import django.core.validators +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='CarMake', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('description', models.TextField(blank=True)), + ], + ), + migrations.CreateModel( + name='CarModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('type', models.CharField(choices=[('SEDAN', 'Sedan'), ('SUV', 'SUV'), ('WAGON', 'Wagon'), ('COUPE', 'Coupe'), ('HATCH', 'Hatchback'), ('TRUCK', 'Truck')], max_length=20)), + ('year', models.IntegerField(validators=[django.core.validators.MinValueValidator(2015), django.core.validators.MaxValueValidator(2023)])), + ('make', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='models', to='djangoapp.carmake')), + ], + ), + ] diff --git a/server/djangoapp/migrations/__init__.py b/server/djangoapp/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/server/djangoapp/models.py b/server/djangoapp/models.py index eb101a68c8..34e6b53fd0 100644 --- a/server/djangoapp/models.py +++ b/server/djangoapp/models.py @@ -1,25 +1,33 @@ -# Uncomment the following imports before adding the Model code +from django.db import models +from django.core.validators import MinValueValidator, MaxValueValidator -# 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, unique=True) + description = models.TextField(blank=True) + def __str__(self): + return self.name + +class CarModel(models.Model): + SEDAN = "SEDAN" + SUV = "SUV" + WAGON = "WAGON" + COUPE = "COUPE" + HATCH = "HATCH" + TRUCK = "TRUCK" + TYPE_CHOICES = [ + (SEDAN, "Sedan"), + (SUV, "SUV"), + (WAGON, "Wagon"), + (COUPE, "Coupe"), + (HATCH, "Hatchback"), + (TRUCK, "Truck"), + ] -# Create your models here. + make = models.ForeignKey(CarMake, on_delete=models.CASCADE, related_name="models") + name = models.CharField(max_length=100) + type = models.CharField(max_length=20, choices=TYPE_CHOICES) + year = models.IntegerField(validators=[MinValueValidator(2015), MaxValueValidator(2023)]) -# 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 + def __str__(self): + return f"{self.make.name} {self.name} ({self.year})" diff --git a/server/djangoapp/populate.py b/server/djangoapp/populate.py index 1927e09e18..98e2512276 100644 --- a/server/djangoapp/populate.py +++ b/server/djangoapp/populate.py @@ -1,2 +1,31 @@ +from .models import CarMake, CarModel + + def initiate(): - print("Populate not implemented. Add data manually") + if CarMake.objects.exists() and CarModel.objects.exists(): + return + + makes = [ + {"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"}, + ] + + make_objs = {} + for m in makes: + make_objs[m["name"]] = CarMake.objects.create(**m) + + models = [ + {"name": "Pathfinder", "type": "SUV", "year": 2023, "make": make_objs["NISSAN"]}, + {"name": "Qashqai", "type": "SUV", "year": 2023, "make": make_objs["NISSAN"]}, + {"name": "A-Class", "type": "Sedan","year": 2023, "make": make_objs["Mercedes"]}, + {"name": "CLA", "type": "Coupe","year": 2023, "make": make_objs["Mercedes"]}, + {"name": "Q7", "type": "SUV", "year": 2023, "make": make_objs["Audi"]}, + {"name": "Sportage", "type": "SUV", "year": 2023, "make": make_objs["Kia"]}, + {"name": "Sorento", "type": "SUV", "year": 2023, "make": make_objs["Kia"]}, + {"name": "Camry", "type": "Sedan","year": 2023, "make": make_objs["Toyota"]}, + {"name": "Corolla", "type": "Sedan","year": 2023, "make": make_objs["Toyota"]}, + ] + CarModel.objects.bulk_create([CarModel(**row) for row in models]) diff --git a/server/djangoapp/restapis.py b/server/djangoapp/restapis.py index 90709d9e3b..ddd2f0c227 100644 --- a/server/djangoapp/restapis.py +++ b/server/djangoapp/restapis.py @@ -1,22 +1,48 @@ -# Uncomment the imports below before you add the function code -# import requests -import os +#server/djangoapp/restapis.py +import os, requests +from urllib.parse import quote_plus from dotenv import load_dotenv load_dotenv() -backend_url = os.getenv( - 'backend_url', default="http://localhost:3030") -sentiment_analyzer_url = os.getenv( - 'sentiment_analyzer_url', - default="http://localhost:5050/") +backend_url = (os.getenv("backend_url") or "").rstrip("/") +sentiment_analyzer_url = (os.getenv("sentiment_analyzer_url") or "").rstrip("/") -# def get_request(endpoint, **kwargs): -# Add code for get requests to back end +def get_request(endpoint, **kwargs): + url = f"{backend_url}{endpoint}" + try: + r = requests.get(url, params=kwargs, timeout=10) + r.raise_for_status() + return r.json() + except Exception as e: + print(f"[get_request] error calling {url}: {e}") + return [] -# def analyze_review_sentiments(text): -# request_url = sentiment_analyzer_url+"analyze/"+text -# Add code for retrieving sentiments +def analyze_review_sentiments(text: str) -> str: + """Return 'positive' | 'neutral' | 'negative' (default neutral on error).""" + if not text: + return "neutral" + if not sentiment_analyzer_url: + # No env var set; don’t break the page + return "neutral" -# def post_review(data_dict): -# Add code for posting review + url = f"{sentiment_analyzer_url}/analyze/{quote_plus(text)}" + try: + resp = requests.get(url, timeout=10) + resp.raise_for_status() + data = resp.json() + # adapt this line to your service's response shape if needed + return (data.get("label") or data.get("sentiment") or "neutral").lower() + except Exception as e: + print(f"[sentiment] error calling {url}: {e}") + return "neutral" + +def post_review(data_dict): + url = f"{backend_url}/insert_review" + try: + r = requests.post(url, json=data_dict, timeout=10) + r.raise_for_status() + return r.json() + except Exception as e: + print(f"[post_review] error calling {url}: {e}") + return {"error": str(e)} \ No newline at end of file diff --git a/server/djangoapp/urls.py b/server/djangoapp/urls.py index 0edc274f90..96fca641c8 100644 --- a/server/djangoapp/urls.py +++ b/server/djangoapp/urls.py @@ -1,18 +1,40 @@ -# 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 django.views.generic import TemplateView +from . import views + +app_name = "djangoapp" -app_name = 'djangoapp' urlpatterns = [ - # # path for registration + # Home / dealerships list + path("", views.get_dealerships, name="index"), + path("dealers/", views.get_dealerships, name="dealers"), + + # Contact page (uses a template called contact.html) + path("contact/", TemplateView.as_view(template_name="contact.html"), name="contact"), + + # Auth JSON endpoints + path("register", views.registration, name="register"), + path("login", views.login_user, name="login"), + path("logout", views.logout_request, name="logout"), + path("api/whoami", views.whoami, name="api-whoami"), - # path for login - # path(route='login', view=views.login_user, name='login'), + # Dealer details + reviews + path("dealer//", views.get_dealer_details, name="dealer_details"), + # path("dealer//reviews/", views.get_dealer_reviews, name="dealer_reviews"), + path(route='get_cars', view=views.get_cars, name ='getcars'), + # Add a review (requires login in the view) + # path("dealer//add-review/", views.add_review, name="add_review"), + path("add_review", views.add_review, name="add_review"), - # path for dealer reviews view + path("get_dealers", views.get_dealerships, name="get_dealers"), + path("get_dealers/", views.get_dealerships, name="get_dealers_by_state"), + path("dealer/", views.get_dealer_details, name="get_dealer_details"), + path("dealer//reviews", views.get_dealer_reviews, name="get_dealer_reviews"), + path("reviews/dealer/", views.get_dealer_reviews, name="get_dealer_reviews_alias"), - # path for add a review view +] -] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) +urlpatterns += static(settings.STATIC_URL, document_root=getattr(settings, "STATIC_ROOT", None)) +urlpatterns += static(settings.MEDIA_URL, document_root=getattr(settings, "MEDIA_ROOT", None)) diff --git a/server/djangoapp/views.py b/server/djangoapp/views.py index b16409f419..4316c5ab92 100644 --- a/server/djangoapp/views.py +++ b/server/djangoapp/views.py @@ -1,65 +1,265 @@ -# 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.http import JsonResponse -from django.contrib.auth import login, authenticate + +# views.py + +# --- Required imports (uncommented) --- +from django.shortcuts import render, redirect, get_object_or_404 +from django.http import HttpResponseRedirect, HttpResponse, JsonResponse +from django.contrib.auth.models import User +from django.contrib.auth import login, authenticate, logout +from django.contrib import messages +from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.decorators import login_required +from datetime import datetime import logging import json -from django.views.decorators.csrf import csrf_exempt -# from .populate import initiate - +from django.views.decorators.http import require_http_methods +from .models import CarMake, CarModel +from .populate import initiate +from .restapis import get_request, analyze_review_sentiments, post_review -# Get an instance of a logger logger = logging.getLogger(__name__) +# ----------------------------------------------------------------------------- +# TEMP STUBS so the views render without your cloud functions. +# Replace these with your actual helpers, e.g.: +# from .restapis import get_dealers_from_cf, get_dealer_reviews_from_cf, post_review +# ----------------------------------------------------------------------------- +def _stub_get_dealers_from_cf(): + # Minimal objects used by the templates. Replace with real data. + return [ + {"id": 1, "full_name": "Seattle Auto Plaza", "city": "Seattle", "state": "WA"}, + {"id": 2, "full_name": "Portland Motors", "city": "Portland", "state": "OR"}, + ] + +def _stub_get_dealer_by_id(dealer_id: int): + for d in _stub_get_dealers_from_cf(): + if d["id"] == int(dealer_id): + return d + return None -# Create your views here. +def _stub_get_dealer_reviews_from_cf(dealer_id: int): + return [ + {"name": "Sina", "review": "Great service!", "purchase": True, "purchase_date": "2024-08-12"}, + {"name": "Alex", "review": "Nice staff, quick process.", "purchase": False}, + ] -# Create a `login_request` view to handle sign in request +def _stub_post_review_to_cf(payload: dict): + logger.info("Pretend posting to cloud function: %s", payload) + return {"ok": True} +# ----------------------------------------------------------------------------- + + +# ------------------------- AUTH JSON ENDPOINTS -------------------------------- @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 + """ + JSON login endpoint. + Request: POST raw JSON: {"userName": "...", "password": "..."} + Response: {"userName": "...", "status": "Authenticated"} or {"userName": "..."} + """ + if request.method != "POST": + return JsonResponse({"detail": "Method not allowed"}, status=405) + + try: + data = json.loads(request.body or "{}") + username = data.get("userName", "") + password = data.get("password", "") + except json.JSONDecodeError: + return JsonResponse({"detail": "Invalid JSON"}, status=400) + + if not username or not password: + return JsonResponse({"detail": "Missing credentials"}, status=400) + 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) + return JsonResponse({"userName": username, "status": "Authenticated"}) + return JsonResponse({"userName": username, "status": "Unauthorized"}, status=401) -# 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): -# ... +# def logout_request(request): +# """ +# JSON logout endpoint. +# """ +# if request.method != "POST": +# return JsonResponse({"detail": "Method not allowed"}, status=405) +# logout(request) +# return JsonResponse({"status": "Logged out"}) + +@csrf_exempt +@require_http_methods(["GET", "POST"]) +def logout_request(request): + """ + Allow logging out via GET (for anchor links) or POST (for fetch/forms). + - If it's an AJAX/JSON request, return JSON. + - Otherwise redirect to homepage. + """ + logout(request) + + wants_json = "application/json" in request.headers.get("Accept", "") + if wants_json or request.headers.get("X-Requested-With") == "XMLHttpRequest": + return JsonResponse({"status": "Logged out"}) + + # Redirect after normal GET + return redirect("djangoapp:index") + + +@csrf_exempt +def registration(request): + """ + JSON registration endpoint. + Request: POST raw JSON: {"userName": "...", "password": "...", "firstName": "...", "lastName": "..."} + """ + if request.method != "POST": + return JsonResponse({"detail": "Method not allowed"}, status=405) + + try: + data = json.loads(request.body or "{}") + except json.JSONDecodeError: + return JsonResponse({"detail": "Invalid JSON"}, status=400) + + username = data.get("userName", "") + password = data.get("password", "") + first_name = data.get("firstName", "") + last_name = data.get("lastName", "") + + if not username or not password: + return JsonResponse({"detail": "Username and password required"}, status=400) + + if User.objects.filter(username=username).exists(): + return JsonResponse({"status": "User already exists"}, status=409) + + user = User.objects.create_user(username=username, password=password, + first_name=first_name, last_name=last_name) + login(request, user) + return JsonResponse({"userName": username, "status": "Registered"}) + + + +def whoami(request): + if request.user.is_authenticated: + return JsonResponse({"isAuthenticated": True, "userName": request.user.username}) + return JsonResponse({"isAuthenticated": False, "userName": ""}) + +# ----------------------------- PAGES ------------------------------------------ +# def get_dealerships(request, state="All"): +# endpoint = "/fetchDealers" if state == "All" else f"/fetchDealers/{state}" +# dealerships = get_request(endpoint) +# return JsonResponse({"status": 200, "dealers": dealerships}) + + +def get_dealerships(request, state="All"): + endpoint = "/fetchDealers" if state == "All" else f"/fetchDealers/{state}" + dealers = get_request(endpoint) + return JsonResponse({"status": 200, "dealers": dealers}) + +def get_dealer_details(request, dealer_id: int): + dealer = get_request(f"/fetchDealer/{dealer_id}") + return JsonResponse({"status": 200, "dealer": dealer}) + +def get_dealer_reviews(request, dealer_id: int): + reviews = get_request(f"/fetchReviews/dealer/{dealer_id}") + enriched = [] + for r in reviews or []: + item = { + "id": r.get("id"), + "name": r.get("name"), + "dealership": r.get("dealership"), + "review": r.get("review"), + "purchase": r.get("purchase"), + "purchase_date": r.get("purchase_date"), + "car_make": r.get("car_make"), + "car_model": r.get("car_model"), + "car_year": r.get("car_year"), + } + item["sentiment"] = analyze_review_sentiments(item["review"]) + enriched.append(item) + return JsonResponse({"status": 200, "reviews": enriched}) + + + +# @login_required(login_url="/login") +# def add_review(request, dealer_id: int): +# """ +# GET: render review form +# POST: submit review to backend (replace stub with your POST call) +# """ +# dealer = _stub_get_dealer_by_id(dealer_id) +# if not dealer: +# messages.error(request, "Dealer not found.") +# return redirect("index") + +# if request.method == "GET": +# return render(request, "add_review.html", {"dealer": dealer}) + +# # POST +# review_text = request.POST.get("review", "").strip() +# purchase = request.POST.get("purchase") == "on" +# purchase_date = request.POST.get("purchase_date") or None +# car_make = request.POST.get("car_make") or "" +# car_model = request.POST.get("car_model") or "" +# car_year = request.POST.get("car_year") or "" + +# payload = { +# "time": datetime.utcnow().isoformat(), +# "name": request.user.get_full_name() or request.user.username, +# "dealership": dealer_id, +# "review": review_text, +# "purchase": purchase, +# "purchase_date": purchase_date, +# "car_make": car_make, +# "car_model": car_model, +# "car_year": car_year, +# } + +# ok = _stub_post_review_to_cf(payload) # TODO: replace with real post_review() +# if ok and (ok.get("ok") or ok is True): +# messages.success(request, "Thanks! Your review was submitted.") +# else: +# messages.error(request, "Could not submit review. Please try again.") + +# return redirect("dealer_details", dealer_id=dealer_id) + +@csrf_exempt +def add_review(request): + """Authenticated users can post a dealer review.""" + if request.method != "POST": + return JsonResponse({"status": 405, "message": "Method not allowed"}, status=405) + + if not request.user.is_authenticated: + return JsonResponse({"status": 403, "message": "Unauthorized"}, status=403) + + try: + data = json.loads(request.body or "{}") + resp = post_review(data) + return JsonResponse({"status": 200, "result": resp}) + except Exception as e: + return JsonResponse({"status": 401, "message": f"Error in posting review: {e}"}, status=400) + + +# def get_cars(request): +# # populate only if empty -# # 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): -# ... +# # If your FK is named 'make' (as above): +# qs = CarModel.objects.select_related("make").all() -# Create a `get_dealer_details` view to render the dealer details -# def get_dealer_details(request, dealer_id): -# ... +# cars = [ +# {"CarModel": c.name, "CarMake": c.make.name, "Type": c.type, "Year": c.year} +# for c in qs +# ] +# return JsonResponse({"CarModels": cars}) -# Create a `add_review` view to submit a review -# def add_review(request): -# ... +def get_cars(request): + if CarMake.objects.count() == 0 or CarModel.objects.count() == 0: + initiate() + + cars = CarModel.objects.select_related("make").values( + "make__name", "name", "year" + ) + car_list = [ + {"CarMake": c["make__name"], "CarModel": c["name"], "CarYear": c["year"]} + for c in cars + ] + return JsonResponse({"CarModels": car_list}, safe=False) \ No newline at end of file diff --git a/server/djangoproj/settings.py b/server/djangoproj/settings.py index e0b1092a5c..4e46da8d20 100644 --- a/server/djangoproj/settings.py +++ b/server/djangoproj/settings.py @@ -61,7 +61,9 @@ 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': [ @@ -134,5 +136,12 @@ 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"), + os.path.join(BASE_DIR, "frontend/build/static"), +] + +ALLOWED_HOSTS=['localhost','127.0.0.1','https://cnabolouri-8000.theianext-0-labs-prod-misc-tools-us-east-0.proxy.cognitiveclass.ai'] +CSRF_TRUSTED_ORIGINS=['https://cnabolouri-8000.theianext-0-labs-prod-misc-tools-us-east-0.proxy.cognitiveclass.ai'] diff --git a/server/djangoproj/urls.py b/server/djangoproj/urls.py index 6808da9141..bc90ad3cc2 100644 --- a/server/djangoproj/urls.py +++ b/server/djangoproj/urls.py @@ -1,18 +1,3 @@ -"""djangoproj URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/3.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" from django.contrib import admin from django.urls import path, include from django.views.generic import TemplateView @@ -21,6 +6,15 @@ urlpatterns = [ path('admin/', admin.site.urls), + path('login/', TemplateView.as_view(template_name="index.html")), + path('register/', TemplateView.as_view(template_name="index.html"), name='spa-register'), + # serve SPA routes: + 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('djangoapp/', include('djangoapp.urls')), + path('about/', TemplateView.as_view(template_name="About.html")), + path('contact/', TemplateView.as_view(template_name="Contact.html")), path('', TemplateView.as_view(template_name="Home.html")), ] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100755 index 0000000000..ae4c0ba5e1 --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,10 @@ +#!/bin/sh +# entrypoint.sh — run migrations and start Gunicorn + +echo "Running Django migrations..." +python manage.py makemigrations --noinput +python manage.py migrate --noinput +python manage.py collectstatic --noinput + +echo "Starting Gunicorn server..." +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/public/index.html b/server/frontend/public/index.html index 116e901ec9..fd448396b6 100644 --- a/server/frontend/public/index.html +++ b/server/frontend/public/index.html @@ -14,7 +14,7 @@ manifest.json provides metadata used when your web app is installed on a user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ --> - + + +
-