diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 05ca50d23b..b8431f1bf6 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -5,6 +5,7 @@ on: branches: [master, main] pull_request: branches: [master, main] + workflow_dispatch: jobs: # list of things to do lint_function_js: diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 0000000000..f79740cfdf --- /dev/null +++ b/server/Dockerfile @@ -0,0 +1,28 @@ +FROM python:3.8.2 + +ENV PYTHONBUFFERED 1 +ENV PYTHONWRITEBYTECODE 1 + +RUN apt-get update \ + && apt-get install -y netcat + +# Set the working directory to /app +ENV APP=/app +WORKDIR $APP + +# Copy the requirements file into the container +COPY requirements.txt $APP + +# Install the requirements +RUN pip3 install -r requirements.txt + +# Copy the rest of the project files into the container +COPY . $APP + +EXPOSE 8000 + +RUN chmod +x /app/entrypoint.sh + +ENTRYPOINT ["/bin/bash", "/app/entrypoint.sh"] + +CMD ["gunicorn", "--bind", ":8000", "--workers", "3", "djangobackend.wsgi"] diff --git a/server/db.sqlite3 b/server/db.sqlite3 new file mode 100644 index 0000000000..3b1ddaa920 Binary files /dev/null and b/server/db.sqlite3 differ diff --git a/server/deployment.yaml b/server/deployment.yaml new file mode 100644 index 0000000000..953e6d1eb9 --- /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-marekmarkiew/dealership:latest + imagePullPolicy: Always + name: dealership + ports: + - containerPort: 8000 + protocol: TCP + restartPolicy: Always \ No newline at end of file diff --git a/server/djangoapp/admin.py b/server/djangoapp/admin.py index b1039e16b8..9764fe7f61 100644 --- a/server/djangoapp/admin.py +++ b/server/djangoapp/admin.py @@ -1,13 +1,22 @@ from django.contrib import admin # from .models import related models +from .models import CarMake, CarModel -# Register your models here. - # CarModelInline class +class CarModelInline(admin.StackedInline): + model = CarModel + extra = 1 -# CarModelAdmin class +# Register your models here. +@admin.register(CarMake) +class CarMakeAdmin(admin.ModelAdmin): + inlines = [CarModelInline] +# CarModelAdmin class +@admin.register(CarModel) +class CarModelAdmin(admin.ModelAdmin): + pass # CarMakeAdmin class with CarModelInline # Register models here diff --git a/server/djangoapp/models.py b/server/djangoapp/models.py index 27d96f4eff..6299df7623 100644 --- a/server/djangoapp/models.py +++ b/server/djangoapp/models.py @@ -1,6 +1,66 @@ -from django.db import models from django.utils.timezone import now +from django.db import models + +class CarMake(models.Model): + name = models.CharField(max_length=100) + description = models.TextField() + + def __str__(self): + return self.name + +class CarModel(models.Model): + make = models.ForeignKey(CarMake, on_delete=models.CASCADE) + dealer_id = models.IntegerField() + name = models.CharField(max_length=100) + + TYPE_CHOICES = ( + ('Sedan', 'Sedan'), + ('SUV', 'SUV'), + ('WAGON', 'WAGON'), + # Add more choices here + ) + + type = models.CharField(max_length=10, choices=TYPE_CHOICES) + year = models.DateField() + + def __str__(self): + return self.name + +class CarDealer(models.Model): + address = models.CharField(max_length=200) + city = models.CharField(max_length=100) + full_name = models.CharField(max_length=100) + id = models.IntegerField(primary_key=True) + lat = models.DecimalField(max_digits=9, decimal_places=6) + long = models.DecimalField(max_digits=9, decimal_places=6) + short_name = models.CharField(max_length=50) + st = models.CharField(max_length=2) + zip = models.CharField(max_length=10) + + def __str__(self): + return "Dealer name: " + self.full_name + +class DealerReview(models.Model): + dealership = models.CharField(max_length=100) + name = models.CharField(max_length=100) + purchase = models.BooleanField() + review = models.TextField() + purchase_date = models.DateField() + car_make = models.CharField(max_length=100) + car_model = models.CharField(max_length=100) + car_year = models.IntegerField() + + SENTIMENT_CHOICES = ( + ('Positive', 'Positive'), + ('Negative', 'Negative'), + ('Neutral', 'Neutral'), + ) + + sentiment = models.CharField(max_length=10, choices=SENTIMENT_CHOICES) + id = models.AutoField(primary_key=True) + def __str__(self): + return self.name # Create your models here. diff --git a/server/djangoapp/restapis.py b/server/djangoapp/restapis.py index b4d13f596a..3bb4518ab9 100644 --- a/server/djangoapp/restapis.py +++ b/server/djangoapp/restapis.py @@ -1,13 +1,31 @@ import requests import json # import related models here +from .models import CarDealer, DealerReview from requests.auth import HTTPBasicAuth +WATSON_NLU_API_KEY = 'NgRnMOGD9aZZbFEGJI9B_6EoOc4lYsHbjHXRPEOcKAyx' # Create a `get_request` to make HTTP GET requests # e.g., response = requests.get(url, params=params, headers={'Content-Type': 'application/json'}, # auth=HTTPBasicAuth('apikey', api_key)) - +def get_request(url, api_key=None, **kwargs): + print(kwargs) + print("GET from {} ".format(url)) + try: + if api_key: + # Call GET method with basic authentication + response = requests.get(url, headers={'Content-Type': 'application/json'}, params=kwargs, auth=HTTPBasicAuth('apikey', api_key)) + else: + # Call GET method without authentication + response = requests.get(url, headers={'Content-Type': 'application/json'}, params=kwargs) + except: + # If any error occurs + print("Network exception occurred") + status_code = response.status_code + print("With status {} ".format(status_code)) + json_data = json.loads(response.text) + return json_data # Create a `post_request` to make HTTP POST requests # e.g., response = requests.post(url, params=kwargs, json=payload) @@ -18,12 +36,57 @@ # - Call get_request() with specified arguments # - Parse JSON results into a CarDealer object list +def get_dealers_from_cf(url, **kwargs): + results = [] + # Call get_request with a URL parameter + json_result = get_request(url) + if json_result: + # Get the row list in JSON as dealers + dealers = json_result["rows"] + # For each dealer object + for dealer in dealers: + # Get its content in `doc` object + dealer_doc = dealer["doc"] + # Create a CarDealer object with values in `doc` object + dealer_obj = CarDealer(address=dealer_doc["address"], city=dealer_doc["city"], full_name=dealer_doc["full_name"], + id=dealer_doc["id"], lat=dealer_doc["lat"], long=dealer_doc["long"], + short_name=dealer_doc["short_name"], + st=dealer_doc["st"], zip=dealer_doc["zip"]) + results.append(dealer_obj) + return results + # Create a get_dealer_reviews_from_cf method to get reviews by dealer id from a cloud function # def get_dealer_by_id_from_cf(url, dealerId): # - Call get_request() with specified arguments # - Parse JSON results into a DealerView object list +def get_dealer_reviews_from_cf(url, dealer_id, api_key): + results = [] + + # Call get_request with a URL parameter + json_result = get_request(url, dealerId=dealer_id, api_key=api_key) + + if json_result: + # Get the list of reviews + reviews = json_result + + for review in reviews: + review_obj = DealerReview( + dealership=review["dealership"], + name=review["name"], + purchase=review["purchase"], + review=review["review"], + purchase_date=review["purchase_date"], + car_make=review["car_make"], + car_model=review["car_model"], + car_year=review["car_year"], + sentiment=analyze_review_sentiments(review["review"]), # Analyze sentiment + id=review["id"] + ) + results.append(review_obj) + + return results # Create an `analyze_review_sentiments` method to call Watson NLU and analyze text # def analyze_review_sentiments(text): @@ -31,4 +94,27 @@ # - Get the returned sentiment label such as Positive or Negative +def analyze_review_sentiments(text): + url = "https://api.us-south.natural-language-understanding.watson.cloud.ibm.com/instances/b47940d0-c41e-43b6-8430-b92ee938ce46/v1/analyze?version=2020-08-01" + + params = { + "text": text, + "version": "2020-08-01", + "features": "sentiment", + "return_analyzed_text": True + } + + response = requests.get(url, params=params, headers={'Content-Type': 'application/json'}, auth=HTTPBasicAuth('apikey', WATSON_NLU_API_KEY)) + + if response.status_code == 200: + json_response = response.json() + if "sentiment" in json_response: + return json_response["sentiment"]["document"]["label"] + else: + return "Unknown" + else: + return "API request failed with status code: " + str(response.status_code) + + + diff --git a/server/djangoapp/templates/djangoapp/about.html b/server/djangoapp/templates/djangoapp/about.html new file mode 100644 index 0000000000..ceb7b5fe42 --- /dev/null +++ b/server/djangoapp/templates/djangoapp/about.html @@ -0,0 +1,12 @@ + + + + + About Us - Dealership Review + + +

Welcome to Best Cars dealership

+

Home to the best cars in North America. We sell domestic and imported cars at reasonable prices.

+ + + diff --git a/server/djangoapp/templates/djangoapp/add_review.html b/server/djangoapp/templates/djangoapp/add_review.html index 768ddf508c..d4ddbc215a 100644 --- a/server/djangoapp/templates/djangoapp/add_review.html +++ b/server/djangoapp/templates/djangoapp/add_review.html @@ -12,5 +12,37 @@ +
+

Submit a Review

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ + +
+
\ No newline at end of file diff --git a/server/djangoapp/templates/djangoapp/contact.html b/server/djangoapp/templates/djangoapp/contact.html new file mode 100644 index 0000000000..36c92577d8 --- /dev/null +++ b/server/djangoapp/templates/djangoapp/contact.html @@ -0,0 +1,12 @@ + + + + + Contact Us - Dealership Review + + +

Contact Us

+

Address: 123 Street, City, Country

+

Telephone: +1-123-456-7890

+ + diff --git a/server/djangoapp/templates/djangoapp/dealer_details.html b/server/djangoapp/templates/djangoapp/dealer_details.html index 25bd9a223d..a5946b03bd 100644 --- a/server/djangoapp/templates/djangoapp/dealer_details.html +++ b/server/djangoapp/templates/djangoapp/dealer_details.html @@ -10,9 +10,72 @@ - + + + + + + Dealership Review + {% load static %} + + + + + + + +
+

Dealer Details

+ + +
+
+
{{ dealer.full_name }}
+

Address: {{ dealer.address }}, {{ dealer.city }}, {{ dealer.st }} {{ dealer.zip }}

+ +
+
+ +

Dealer Reviews

+ + +
+ {% for review in reviews %} +
+
+
+

{{ review.review }}

+
+
+ +
+
+
+
+
+ {% endfor %} +
+ + +
+ {% for review in reviews %} +
+ Sentiment Emoji +
+
{{ review.car_make }} - {{ review.car_model }}
+

Purchase Year: {{ review.car_year }}

+

{{ review.review }}

+
+
+ {% endfor %} +
+
+ \ No newline at end of file diff --git a/server/djangoapp/templates/djangoapp/index.html b/server/djangoapp/templates/djangoapp/index.html index 1a9ee6e39a..3b9c51db0e 100644 --- a/server/djangoapp/templates/djangoapp/index.html +++ b/server/djangoapp/templates/djangoapp/index.html @@ -14,12 +14,95 @@ - - This is the index page of your Django app! + + + + +
+

List of Dealerships

+ + + + + + + + + + + + + {% for dealer in dealerships %} + + + + + + + + + {% endfor %} + +
IDDealer NameCityAddressZipState
{{ dealer.id }}{{ dealer.full_name }}{{ dealer.city }}{{ dealer.address }}{{ dealer.zip }}{{ dealer.st }}
+
+ diff --git a/server/djangoapp/templates/djangoapp/login.html b/server/djangoapp/templates/djangoapp/login.html new file mode 100644 index 0000000000..e79cf2681c --- /dev/null +++ b/server/djangoapp/templates/djangoapp/login.html @@ -0,0 +1,15 @@ + + + + + Login + + +

Login

+
+ {% csrf_token %} + {{ form.as_p }} + +
+ + diff --git a/server/djangoapp/templates/djangoapp/logout.html b/server/djangoapp/templates/djangoapp/logout.html new file mode 100644 index 0000000000..7db12df29c --- /dev/null +++ b/server/djangoapp/templates/djangoapp/logout.html @@ -0,0 +1,11 @@ + + + + + Logout + + +

Logout

+

You have been successfully logged out. Thank you for visiting.

+ + diff --git a/server/djangoapp/templates/djangoapp/registration.html b/server/djangoapp/templates/djangoapp/registration.html index ae11ea4b71..d6cadba3b4 100644 --- a/server/djangoapp/templates/djangoapp/registration.html +++ b/server/djangoapp/templates/djangoapp/registration.html @@ -5,7 +5,32 @@ {% load static %} - - - - \ No newline at end of file + + +

Sign Up :)

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + diff --git a/server/djangoapp/urls.py b/server/djangoapp/urls.py index 37b1c89d01..92daa24e36 100644 --- a/server/djangoapp/urls.py +++ b/server/djangoapp/urls.py @@ -9,20 +9,33 @@ # view refers to the view function # name the URL + # path for index + path(route='', view=views.index, name='index'), + # path for about view + path(route='about/', view=views.about, name='about'), # path for contact us view + path(route='contact/', view=views.contact, name='contact'), # path for registration + path(route='registration/', view=views.signup, name='registration'), # path for login + path(route='login/', view=views.login_request, name='login_view'), # path for logout - path(route='', view=views.get_dealerships, name='index'), + path(route='logout/', view=views.logout_request, name='logout'), + + path(route='dealerships/', view=views.get_dealerships, name='dealerships'), # path for dealer reviews view + path(route='dealer//', view=views.get_dealer_details, name='dealer_details'), + # path for add a review view + path('add_review//', views.add_review, name='add_review'), + ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/server/djangoapp/views.py b/server/djangoapp/views.py index 61cc664da0..0a781458f9 100644 --- a/server/djangoapp/views.py +++ b/server/djangoapp/views.py @@ -1,10 +1,15 @@ -from django.shortcuts import render +from django.shortcuts import render, redirect from django.http import HttpResponseRedirect, HttpResponse from django.contrib.auth.models import User from django.shortcuts import get_object_or_404, render, redirect # from .models import related models +from .models import CarDealer, CarModel, DealerReview # from .restapis import related methods +from .restapis import get_dealer_reviews_from_cf, get_dealers_from_cf + from django.contrib.auth import login, logout, authenticate +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.contrib.auth.decorators import login_required # For authentication checks from django.contrib import messages from datetime import datetime import logging @@ -16,31 +21,73 @@ # Create your views here. +def index(request): + return render(request, 'djangoapp/index.html') # Create an `about` view to render a static about page -# def about(request): -# ... +def about(request): + return render(request, 'djangoapp/about.html') # Create a `contact` view to return a static contact page -#def contact(request): +def contact(request): + return render(request, 'djangoapp/contact.html') # Create a `login_request` view to handle sign in request # def login_request(request): # ... +def login_request(request): + if request.method == 'POST': + form = AuthenticationForm(data=request.POST) + if form.is_valid(): + username = form.cleaned_data.get('username') + password = form.cleaned_data.get('password') + user = authenticate(request, username=username, password=password) + if user is not None: + login(request, user) + messages.success(request, "Logged in successfully.") + return redirect('djangoapp:index') # Redirect to the home page after successful login + else: + messages.error(request, "Invalid login credentials. Please try again.") + else: + form = AuthenticationForm() + + return render(request, 'djangoapp/login.html', {'form': form}) # Create a `logout_request` view to handle sign out request -# def logout_request(request): -# ... - +def logout_request(request): + logout(request) + messages.success(request, "Logged out successfully.") + return render(request, 'djangoapp/logout.html') # Create a `registration_request` view to handle sign up request # def registration_request(request): -# ... + +def signup(request): + if request.method == 'POST': + form = UserCreationForm(request.POST) + if form.is_valid(): + form.save() + username = form.cleaned_data.get('username') + messages.success(request, f"Account created for {username}. You can now log in.") + return redirect('djangoapp:login_view') + else: + # Debugging output + print(form.errors) # You can also log this information + else: + form = UserCreationForm() + return render(request, 'djangoapp/registration.html', {'form': form}) # Update the `get_dealerships` view to render the index page with a list of dealerships def get_dealerships(request): - context = {} if request.method == "GET": + url = "https://us-south.functions.appdomain.cloud/api/v1/web/b81143fb-ebf7-455a-b7a0-fe558100017e/default/getDealerships" + # Get dealers from the URL + dealerships = CarDealer.objects.all() + # Concat all dealer's short name + context = { + "dealerships": dealerships # Updated context variable name + } + return render(request, 'djangoapp/index.html', context) @@ -48,7 +95,68 @@ def get_dealerships(request): # def get_dealer_details(request, dealer_id): # ... + + +def get_dealer_details(request, dealer_id): + api_key = 'NgRnMOGD9aZZbFEGJI9B_6EoOc4lYsHbjHXRPEOcKAyx' + url = "https://us-south.functions.appdomain.cloud/api/v1/web/b81143fb-ebf7-455a-b7a0-fe558100017e/default/getReviewsForDealership?dealerId=13" + + if request.method == "GET": + # Get reviews for the dealer with the specified ID + reviews = get_dealer_reviews_from_cf(url, dealer_id, api_key) + + context = { + "reviews": reviews + } + + return render(request, 'djangoapp/dealer_details.html', context) + + # Create a `add_review` view to submit a review # def add_review(request, dealer_id): # ... +def add_review(request, dealer_id): + if request.method == 'GET': + # Query cars with the dealer id to be reviewed + cars = CarModel.objects.filter(dealer_id=dealer_id) + + # Append the queried cars into context + context = { + 'cars': cars, + 'dealer_id': dealer_id, + } + return render(request, 'djangoapp/add_review.html', context) + + elif request.method == 'POST': + dealer_id = request.POST.get('dealer_id') # You may need to retrieve dealer_id from the form or URL + review_content = request.POST.get('content') + purchased = request.POST.get('purchasecheck') + car_id = request.POST.get('car') + purchasedate = request.POST.get('purchasedate') + + # Use datetime.utcnow().isoformat() to format the review time + review_time = datetime.utcnow().isoformat() + + # Use car.year.strftime("%Y") to get the year from the date field + car = CarModel.objects.get(id=car_id) + purchase_year = car.year.strftime("%Y") + + # Create a DealerReview object and save it to the database + review = DealerReview( + dealership=dealer_id, + name="Your User Name", # Replace with user authentication if available + purchase=(purchased == 'on'), # Convert checkbox input to a boolean + review=review_content, + purchase_date=purchasedate, + car_make=car.make.name, + car_model=car.name, + car_year=purchase_year, + sentiment="Unknown", + ) + review.save() + + # Redirect to the dealer details page for the dealer_id + return redirect('djangoapp:dealer_details', dealer_id=dealer_id) + + return HttpResponse("Invalid request method") \ No newline at end of file diff --git a/server/entrypoint.sh b/server/entrypoint.sh new file mode 100755 index 0000000000..1a48bf8972 --- /dev/null +++ b/server/entrypoint.sh @@ -0,0 +1,17 @@ + #!/bin/sh + + if [ "$DATABASE" = "postgres" ]; then + echo "Waiting for postgres..." + + while ! nc -z $DATABASE_HOST $DATABASE_PORT; do + sleep 0.1 + done + + echo "PostgreSQL started" + fi + + # Make migrations and migrate the database. + echo "Making migrations and migrating the database. " + python manage.py makemigrations main --noinput + python manage.py migrate --noinput + exec "$@" \ No newline at end of file