diff --git a/backend/RateMyCourse/urls.py b/backend/RateMyCourse/urls.py index 9cbc84d..00a147e 100644 --- a/backend/RateMyCourse/urls.py +++ b/backend/RateMyCourse/urls.py @@ -18,10 +18,26 @@ from django.urls import re_path,path,include from django.conf.urls import url from django.contrib.staticfiles.views import serve from django.views.generic import RedirectView +from postings import views urlpatterns = [ # / routes to index.html - url(r'^$', serve, kwargs={'path': 'index.html'}), + path('', views.index, name='homepage'), + + # /reviews routes to the endpoint for POSTing new reviews. + path('reviews', views.post_review, name='post_review'), + + # /universities routes to a list of universities. + path('universities', views.universities, name='universities_list'), + + # /universities/ routes to a specific university. + path('universities/', views.university_entity, name='university entity'), + + # /courses routes to a list of courses. + path('courses', views.courses, name='courses_list'), + + # /courses/ routes to a specific course. + path('courses/', views.course_entity, name='course entity'), # static files (*.css, *.js, *.jpg etc.) served on / # (assuming Django uses /static/ and /media/ for static/media urls) diff --git a/backend/db.sqlite3 b/backend/db.sqlite3 index 232a12e..823e972 100644 Binary files a/backend/db.sqlite3 and b/backend/db.sqlite3 differ diff --git a/backend/postings/admin.py b/backend/postings/admin.py index 5961a97..f33fcd3 100644 --- a/backend/postings/admin.py +++ b/backend/postings/admin.py @@ -9,4 +9,5 @@ admin.site.register(RateableEntity) admin.site.register(ReviewHelpfulVote) admin.site.register(University) admin.site.register(Professor) -admin.site.register(Course) \ No newline at end of file +admin.site.register(Course) +admin.site.register(User) \ No newline at end of file diff --git a/backend/postings/forms.py b/backend/postings/forms.py new file mode 100644 index 0000000..99d8065 --- /dev/null +++ b/backend/postings/forms.py @@ -0,0 +1,12 @@ +from django import forms + +# The form for creating a review for any sort of rateable entity. +class EntityReviewForm(forms.Form): + # The integer rating from 1 to 5. + rating = forms.IntegerField(min_value=1, max_value=5) + # The title of the review. + title = forms.CharField(max_length=128) + # The textual content of the review. + content = forms.CharField(widget=forms.Textarea) + # The id of the entity for which the review is created. + entity_id = forms.IntegerField() \ No newline at end of file diff --git a/backend/postings/migrations/0002_auto_20181002_1338.py b/backend/postings/migrations/0002_auto_20181002_1338.py new file mode 100644 index 0000000..04aacfd --- /dev/null +++ b/backend/postings/migrations/0002_auto_20181002_1338.py @@ -0,0 +1,19 @@ +# Generated by Django 2.1.1 on 2018-10-02 13:38 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('postings', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='review', + name='author', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to='postings.User'), + ), + ] diff --git a/backend/postings/migrations/0003_auto_20181002_1355.py b/backend/postings/migrations/0003_auto_20181002_1355.py new file mode 100644 index 0000000..dc0361f --- /dev/null +++ b/backend/postings/migrations/0003_auto_20181002_1355.py @@ -0,0 +1,25 @@ +# Generated by Django 2.1.1 on 2018-10-02 13:55 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('postings', '0002_auto_20181002_1338'), + ] + + operations = [ + migrations.AddField( + model_name='reviewhelpfulvote', + name='user', + field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='postings.User'), + preserve_default=False, + ), + migrations.AlterField( + model_name='review', + name='author', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='postings.User'), + ), + ] diff --git a/backend/postings/models.py b/backend/postings/models.py index 70049a5..d998984 100644 --- a/backend/postings/models.py +++ b/backend/postings/models.py @@ -7,6 +7,10 @@ class User(models.Model): # The user's birth date. birth_date = models.DateField() + # Returns the name as the string representation of the user. + def __str__(self): + return self.name + # Represents any object for which reviews can be made. (Universities, Professors, etc.) class RateableEntity(models.Model): # Constants defined for types of rateable entities. @@ -26,6 +30,20 @@ class RateableEntity(models.Model): # The type of entity this is. entity_type = models.SmallIntegerField(choices=TYPE_CHOICES) + # Gets the average of all the reviews. + def getAverageRating(self): + reviews = self.review_set.select_related() + rating_sum = 0 + for review in reviews: + rating_sum += review.rating + if reviews.count() == 0: + return None + return rating_sum / reviews.count() + + # Simply returns the name as the string representation. + def __str__(self): + return self.name + # A review represents any single data entry to the database. class Review(models.Model): # An integer rating in the domain [1, 5] @@ -41,7 +59,15 @@ class Review(models.Model): # The date and time at which the last modification to this review was published. last_updated_date = models.DateTimeField(auto_now=True) # A reference to the person who created this review. - author = models.ForeignKey('postings.User', on_delete=models.PROTECT) + author = models.ForeignKey('postings.User', on_delete=models.PROTECT, null=True, blank=True) + + # Gets the total number of votes which marked this review as 'helpful'. + def getHelpfulVoteCount(self): + ReviewHelpfulVote.objects.filter(pk=self.pk, helpful=True).count() + + # Gets the total number of votes which marked this review as 'unhelpful'. + def getUnhelpfulVoteCount(self): + ReviewHelpfulVote.objects.filter(pk=self.pk, helpful=False).count() # A vote for a review as either positive or negative. class ReviewHelpfulVote(models.Model): @@ -49,7 +75,8 @@ class ReviewHelpfulVote(models.Model): review = models.ForeignKey('postings.Review', on_delete=models.CASCADE) # Whether or not the referenced review was helpful. helpful = models.BooleanField() - # TODO: Add a reference to the user who voted. The whole purpose of a separate vote object is to track who votes for what. + # The user who made this vote. + user = models.ForeignKey('postings.User', on_delete=models.CASCADE) # A RateableEntity for universities. class University(RateableEntity): diff --git a/backend/postings/templates/postings/collections/collection.html b/backend/postings/templates/postings/collections/collection.html new file mode 100644 index 0000000..90d7f3b --- /dev/null +++ b/backend/postings/templates/postings/collections/collection.html @@ -0,0 +1,20 @@ +{% extends "postings/generic_page.html" %} + +{# Represents a generic collection of entities. #} + +{% block content %} + +{% block collection_name %} +{% endblock %} + +
    + {% for entity in entities %} +
  • + {% block entity %} + {{ entity.name }} + {% endblock %} +
  • + {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/backend/postings/templates/postings/collections/courses.html b/backend/postings/templates/postings/collections/courses.html new file mode 100644 index 0000000..0fc63cb --- /dev/null +++ b/backend/postings/templates/postings/collections/courses.html @@ -0,0 +1,11 @@ +{% extends "postings/collections/collection.html" %} + +{# Represents a list of university entities. #} + +{% block collection_name %} +

Courses

+{% endblock %} + +{% block entity %} +

{{ entity.name }}

+{% endblock %} \ No newline at end of file diff --git a/backend/postings/templates/postings/collections/universities.html b/backend/postings/templates/postings/collections/universities.html new file mode 100644 index 0000000..bb5374d --- /dev/null +++ b/backend/postings/templates/postings/collections/universities.html @@ -0,0 +1,11 @@ +{% extends "postings/collections/collection.html" %} + +{# Represents a list of university entities. #} + +{% block collection_name %} +

Universities

+{% endblock %} + +{% block entity %} +

{{ entity.name }}

+{% endblock %} \ No newline at end of file diff --git a/backend/postings/templates/postings/entity_pages/course.html b/backend/postings/templates/postings/entity_pages/course.html new file mode 100644 index 0000000..8508196 --- /dev/null +++ b/backend/postings/templates/postings/entity_pages/course.html @@ -0,0 +1,11 @@ +{% extends "postings/entity_pages/entity.html" %} + +{% block entity_info %} + Taught at: {{ entity.taught_at_university.name }} +

Professors

+
    + {% for professor in entity.professors.all %} +
  • {{ professor.name }}
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/backend/postings/templates/postings/entity_pages/entity.html b/backend/postings/templates/postings/entity_pages/entity.html new file mode 100644 index 0000000..49aa6ff --- /dev/null +++ b/backend/postings/templates/postings/entity_pages/entity.html @@ -0,0 +1,50 @@ +{% extends "postings/generic_page.html" %} + +{# Represents a single entity's detail page. #} + +{% block content %} + +

Name: {{ entity.name }}

Average rating: {{ entity.average_rating|floatformat:"-2" }} + +{# Child templates can redefine this block for displaying data pertaining to that specific entity. #} +{% block entity_info %} +{% endblock %} + +{# This section displays all reviews for a given entity. #} +
+

Reviews

+
    + {% for review in entity.review_set.all %} +
  • +

    {{ review.title }}

    {{ review.rating }} +

    {{ review.content }} +

  • + {% endfor %} +
+
+ +{# This section is where the user can write a review for a particular entity and submit it. #} +
+

Write a Review

+
+ + +
+ + + +
+ + + +
+ + {# The following csrf_token and input fields are hidden values needed for form submission. #} + {% csrf_token %} + + + +
+
+ +{% endblock %} \ No newline at end of file diff --git a/backend/postings/templates/postings/entity_pages/university.html b/backend/postings/templates/postings/entity_pages/university.html new file mode 100644 index 0000000..0ea0b8c --- /dev/null +++ b/backend/postings/templates/postings/entity_pages/university.html @@ -0,0 +1,16 @@ +{% extends "postings/entity_pages/entity.html" %} + +{% block entity_info %} +

Courses

+ +

Professors

+
    + {% for professor in entity.professor_set.all %} +
  • {{ professor.name }}
  • + {% endfor %} +
+{% endblock %} \ No newline at end of file diff --git a/backend/postings/templates/postings/generic_page.html b/backend/postings/templates/postings/generic_page.html new file mode 100644 index 0000000..f49084f --- /dev/null +++ b/backend/postings/templates/postings/generic_page.html @@ -0,0 +1,23 @@ +{# This page represents the base template that all others will extend from. #} +{# It will contain a universal navigation bar, script tags, footers, and other things needed on every page. #} + + + + + + {% block title %}RateMyCourse{% endblock %} + + + + +
+

RateMyCourse

+
+ + {# All of a page's content to display should be placed in here. #} +
+ {% block content %} + {% endblock %} +
+ + \ No newline at end of file diff --git a/backend/postings/templates/postings/index.html b/backend/postings/templates/postings/index.html new file mode 100644 index 0000000..6b4fc7b --- /dev/null +++ b/backend/postings/templates/postings/index.html @@ -0,0 +1,29 @@ +{% extends "postings/generic_page.html" %} + +{# The homepage for the website. #} + +{% block content %} + {# First section for searching our database. #} +
+
+ + +
+ +
+ + {# Second section for displaying results, or whatever should be shown first. #} + {% if results %} +
+
    + {% for entity in results %} +
  • {{ entity.name }}
  • + {% endfor %} +
+
+ {% endif %} + +{% endblock %} \ No newline at end of file diff --git a/backend/postings/views.py b/backend/postings/views.py index 91ea44a..4eb60cb 100644 --- a/backend/postings/views.py +++ b/backend/postings/views.py @@ -1,3 +1,75 @@ from django.shortcuts import render +from django.http import HttpResponse, Http404, HttpResponseBadRequest, HttpResponseRedirect +from postings.models import * +from postings.forms import * # Create your views here. + +# The view for the homepage, or index.html +# There is an optional 'search_query GET parameter, which, if provided, gives the template a 'results' variable. +def index(request): + search_query = request.GET.get('search_query', None) + results = None + if search_query: + results = RateableEntity.objects.filter(name__icontains=search_query) + + return render(request, 'postings/index.html', {'results': results}) + +# The view for a listing of universities. +def universities(request): + universities_list = University.objects.all() + context = {'entities': universities_list} + return render(request, 'postings/collections/universities.html', context) + +# The view for /universities/ Displays one university entity. +def university_entity(request, university_id): + try: + university = University.objects.get(pk=university_id) + university.average_rating = university.getAverageRating() + except University.DoesNotExist: + raise Http404("University does not exist") + return render(request, 'postings/entity_pages/university.html', {'entity': university}) + +# The view for a listing of courses. +def courses(request): + courses_list = Course.objects.all() + context = {'entities': courses_list} + return render(request, 'postings/collections/courses.html', context) + +# The view for a specific course entity. +def course_entity(request, course_id): + try: + course = Course.objects.get(pk=course_id) + except Course.DoesNotExist: + raise Http404("Course does not exist") + return render(request, 'postings/entity_pages/course.html', {'entity': course}) + +# The view for receiving POST requests for new reviews. +def post_review(request): + if request.method == 'POST': + form = EntityReviewForm(request.POST) + if form.is_valid(): + # Only if the request is a POST and the form is valid do we do anything. + rating = form.cleaned_data['rating'] + title = form.cleaned_data['title'] + content = form.cleaned_data['content'] + entity_id = form.cleaned_data['entity_id'] + entity = RateableEntity.objects.get(pk=entity_id) + + # Creates the new Review object from the posted data. + review = Review.objects.create( + rating=rating, + title=title, + content=content, + rateable_entity=entity + ) + + # Send the user back to the entity they were viewing. + redirect_path = '/' + if entity.entity_type == RateableEntity.UNIVERSITY: + redirect_path = '/universities/' + str(entity_id) + elif entity.entity_type == RateableEntity.COURSE: + redirect_path = '/courses/' + str(entity_id) + return HttpResponseRedirect(redirect_path) + + return HttpResponseBadRequest("Bad Request") \ No newline at end of file