Merge pull request #9 from andrewlalis/al/template_homepage

Implemented basic template functionality.
This commit is contained in:
Andrew Lalis 2018-10-03 21:03:19 +01:00 committed by GitHub
commit af89d0cd3f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 347 additions and 4 deletions

View File

@ -18,10 +18,26 @@ from django.urls import re_path,path,include
from django.conf.urls import url from django.conf.urls import url
from django.contrib.staticfiles.views import serve from django.contrib.staticfiles.views import serve
from django.views.generic import RedirectView from django.views.generic import RedirectView
from postings import views
urlpatterns = [ urlpatterns = [
# / routes to index.html # / 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/<pk> routes to a specific university.
path('universities/<int:university_id>', views.university_entity, name='university entity'),
# /courses routes to a list of courses.
path('courses', views.courses, name='courses_list'),
# /courses/<pk> routes to a specific course.
path('courses/<int:course_id>', views.course_entity, name='course entity'),
# static files (*.css, *.js, *.jpg etc.) served on / # static files (*.css, *.js, *.jpg etc.) served on /
# (assuming Django uses /static/ and /media/ for static/media urls) # (assuming Django uses /static/ and /media/ for static/media urls)

Binary file not shown.

View File

@ -10,3 +10,4 @@ admin.site.register(ReviewHelpfulVote)
admin.site.register(University) admin.site.register(University)
admin.site.register(Professor) admin.site.register(Professor)
admin.site.register(Course) admin.site.register(Course)
admin.site.register(User)

12
backend/postings/forms.py Normal file
View File

@ -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()

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -7,6 +7,10 @@ class User(models.Model):
# The user's birth date. # The user's birth date.
birth_date = models.DateField() 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.) # Represents any object for which reviews can be made. (Universities, Professors, etc.)
class RateableEntity(models.Model): class RateableEntity(models.Model):
# Constants defined for types of rateable entities. # Constants defined for types of rateable entities.
@ -26,6 +30,20 @@ class RateableEntity(models.Model):
# The type of entity this is. # The type of entity this is.
entity_type = models.SmallIntegerField(choices=TYPE_CHOICES) 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. # A review represents any single data entry to the database.
class Review(models.Model): class Review(models.Model):
# An integer rating in the domain [1, 5] # 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. # The date and time at which the last modification to this review was published.
last_updated_date = models.DateTimeField(auto_now=True) last_updated_date = models.DateTimeField(auto_now=True)
# A reference to the person who created this review. # 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. # A vote for a review as either positive or negative.
class ReviewHelpfulVote(models.Model): class ReviewHelpfulVote(models.Model):
@ -49,7 +75,8 @@ class ReviewHelpfulVote(models.Model):
review = models.ForeignKey('postings.Review', on_delete=models.CASCADE) review = models.ForeignKey('postings.Review', on_delete=models.CASCADE)
# Whether or not the referenced review was helpful. # Whether or not the referenced review was helpful.
helpful = models.BooleanField() 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. # A RateableEntity for universities.
class University(RateableEntity): class University(RateableEntity):

View File

@ -0,0 +1,20 @@
{% extends "postings/generic_page.html" %}
{# Represents a generic collection of entities. #}
{% block content %}
{% block collection_name %}
{% endblock %}
<ul>
{% for entity in entities %}
<li>
{% block entity %}
{{ entity.name }}
{% endblock %}
</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "postings/collections/collection.html" %}
{# Represents a list of university entities. #}
{% block collection_name %}
<h2>Courses</h2>
{% endblock %}
{% block entity %}
<h3><a href="/courses/{{ entity.pk }}">{{ entity.name }}</a></h3>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "postings/collections/collection.html" %}
{# Represents a list of university entities. #}
{% block collection_name %}
<h2>Universities</h2>
{% endblock %}
{% block entity %}
<h3><a href="/universities/{{ entity.pk }}">{{ entity.name }}</a></h3>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% extends "postings/entity_pages/entity.html" %}
{% block entity_info %}
Taught at: <a href="/universities/{{ entity.taught_at_university.pk }}">{{ entity.taught_at_university.name }}</a>
<h4>Professors</h4>
<ul>
{% for professor in entity.professors.all %}
<li>{{ professor.name }}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -0,0 +1,50 @@
{% extends "postings/generic_page.html" %}
{# Represents a single entity's detail page. #}
{% block content %}
<h2>Name: {{ entity.name }}</h2> 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. #}
<section>
<h3>Reviews</h3>
<ul>
{% for review in entity.review_set.all %}
<li>
<h4>{{ review.title }}</h4> {{ review.rating }}
<p>{{ review.content }}
</li>
{% endfor %}
</ul>
</section>
{# This section is where the user can write a review for a particular entity and submit it. #}
<section>
<h3>Write a Review</h3>
<form method="post" action="/reviews">
<label for="rating_input">Rating:</label>
<input id="rating_input" name="rating" type="number" step="1" min="1" max="5" required>
<br>
<label for="title_input">Title:</label>
<input id="title_input" name="title" type="text" required>
<br>
<label for="content_input">Content:</label>
<textarea id="content_input" name="content" required></textarea>
<br>
{# The following csrf_token and input fields are hidden values needed for form submission. #}
{% csrf_token %}
<input type="hidden" name="entity_id" value="{{ entity.pk }}">
<button type="submit">Submit Review</button>
</form>
</section>
{% endblock %}

View File

@ -0,0 +1,16 @@
{% extends "postings/entity_pages/entity.html" %}
{% block entity_info %}
<h4>Courses</h4>
<ul>
{% for course in entity.course_set.all %}
<li><a href="/courses/{{ course.pk }}">{{ course.name }}</a></li>
{% endfor %}
</ul>
<h4>Professors</h4>
<ul>
{% for professor in entity.professor_set.all %}
<li>{{ professor.name }}</li>
{% endfor %}
</ul>
{% endblock %}

View File

@ -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. #}
<!doctype HTML>
<html lang="en">
<head>
<title>
{% block title %}RateMyCourse{% endblock %}</title>
</title>
</head>
<body>
<header>
<h1><a href="/">RateMyCourse</a></h1>
</header>
{# All of a page's content to display should be placed in here. #}
<div id="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,29 @@
{% extends "postings/generic_page.html" %}
{# The homepage for the website. #}
{% block content %}
{# First section for searching our database. #}
<section>
<form method="GET" action="/">
<input type="text" name="search_query">
<button type="submit">Search</button>
</form>
<nav>
<a href="/universities">Universities</a>
<a href="/courses">Courses</a>
</nav>
</section>
{# Second section for displaying results, or whatever should be shown first. #}
{% if results %}
<section>
<ul>
{% for entity in results %}
<li>{{ entity.name }}</li>
{% endfor %}
</ul>
</section>
{% endif %}
{% endblock %}

View File

@ -1,3 +1,75 @@
from django.shortcuts import render 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. # 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/<pk> 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")