Skip to content

Commit 8a1f742

Browse files
author
Bojan Jovanovic
committed
Adds login screen and very basic cfp rating system
1 parent 87ebf1b commit 8a1f742

File tree

10 files changed

+198
-43
lines changed

10 files changed

+198
-43
lines changed

pyconbalkan/cfp/const.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
TALK = 1
2+
WORKSHOP = 2
3+
TYPE_CFP = (
4+
(TALK, 'Talk'),
5+
(WORKSHOP, 'Workshop'),
6+
)

pyconbalkan/cfp/forms.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from django import forms
2+
from django.conf import settings
3+
from django.forms import ModelForm
4+
5+
from pyconbalkan.cfp.models import CFPRating
6+
from .models import Cfp
7+
from . import const
8+
9+
10+
class CfpForm(ModelForm):
11+
name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Name', 'class': 'form-control'}),
12+
max_length=256, error_messages={'required': 'Please, enter your name.'}, label='')
13+
company = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Company', 'class': 'form-control'}),
14+
max_length=100, required=False, label='')
15+
email = forms.EmailField(widget=forms.TextInput(attrs={'placeholder': 'Email', 'class': 'form-control'}),
16+
error_messages={'required': 'Please, enter a valid email address.',
17+
'invalid': 'Please enter a valid email address.'}, label='')
18+
personal_website = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Personal Website (URL)', 'class': 'form-control'}),
19+
max_length=100, required=False, label='')
20+
linkedin = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Linkedin (URL)', 'class': 'form-control'}),
21+
max_length=100, required=False, label='')
22+
title = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Title of your proposal', 'class': 'form-control'}),
23+
error_messages={'required': 'Please, enter the title.'}, label='')
24+
duration = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Duration (E.g. 2h, 30min, 1:30, etc.)', 'class': 'form-control'}),
25+
error_messages={'required': 'Please, enter the duration.'}, label='')
26+
description= forms.CharField(widget=forms.Textarea(attrs={'placeholder': 'Description of your proposal', 'class': 'form-control'}),
27+
error_messages={'required': 'Please, enter the description of your proposal.'}, label='')
28+
type = forms.ChoiceField(choices=const.TYPE_CFP, widget=forms.Select())
29+
30+
class Meta:
31+
model = Cfp
32+
fields = (
33+
'name',
34+
'company',
35+
'email',
36+
'personal_website',
37+
'linkedin',
38+
'title',
39+
'duration',
40+
'description',
41+
'type',
42+
)
43+
44+
45+
class RateForm(ModelForm):
46+
class Meta:
47+
model = CFPRating
48+
fields = (
49+
'mark',
50+
)
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Generated by Django 2.0.5 on 2018-08-07 09:44
2+
3+
from django.conf import settings
4+
import django.core.validators
5+
from django.db import migrations, models
6+
import django.db.models.deletion
7+
import taggit.managers
8+
9+
10+
class Migration(migrations.Migration):
11+
12+
dependencies = [
13+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
14+
('taggit', '0002_auto_20150616_2121'),
15+
('cfp', '0005_auto_20180629_1257'),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name='CFPRating',
21+
fields=[
22+
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
23+
('mark', models.IntegerField(validators=[django.core.validators.MaxValueValidator(10, 'Maximum rating is 10'), django.core.validators.MinValueValidator(0, 'Minimum Rating is 10')])),
24+
('cfp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ratings', to='cfp.Cfp')),
25+
('tags', taggit.managers.TaggableManager(help_text='A comma-separated list of tags.', through='taggit.TaggedItem', to='taggit.Tag', verbose_name='Tags')),
26+
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
27+
],
28+
),
29+
migrations.AlterUniqueTogether(
30+
name='cfprating',
31+
unique_together={('user', 'cfp')},
32+
),
33+
]

pyconbalkan/cfp/models.py

Lines changed: 21 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,16 @@
11
import random
22
import string
3+
from statistics import median, StatisticsError
34

5+
from django.conf import settings
6+
from django.core.validators import MaxValueValidator, MinValueValidator
47
from django.db import models
5-
from django import forms
6-
from django.forms import ModelForm
8+
from django.db.models import CASCADE
79
from markdownx.models import MarkdownxField
810
from slugify import slugify
11+
from taggit.managers import TaggableManager
12+
from . import const
913

10-
TALK = 1
11-
WORKSHOP = 2
12-
TYPE_CFP = (
13-
(TALK, 'Talk'),
14-
(WORKSHOP, 'Workshop'),
15-
)
1614

1715
class Cfp(models.Model):
1816
name = models.CharField(max_length=256)
@@ -24,9 +22,16 @@ class Cfp(models.Model):
2422
description = MarkdownxField()
2523
accepted = models.BooleanField(default=False)
2624
slug = models.CharField(unique=True, blank=True, max_length=100)
27-
type = models.IntegerField(choices=TYPE_CFP, default=TALK)
25+
type = models.IntegerField(choices=const.TYPE_CFP, default=const.TALK)
2826
duration = models.CharField(max_length=100, null=True)
2927

28+
@property
29+
def rating(self):
30+
try:
31+
return median(self.ratings.all().values_list('mark'))
32+
except StatisticsError:
33+
return "N/A"
34+
3035
def __str__(self):
3136
return '{}: "{}" by {} - [{}]'.format(self.get_type_display(), self.title, self.name, 'Accepted' if self.accepted else 'Pending')
3237

@@ -38,36 +43,12 @@ def save(self, *args, **kwargs):
3843
super(Cfp, self).save(*args, **kwargs)
3944

4045

41-
class CfpForm(ModelForm):
42-
name = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Name', 'class': 'form-control'}),
43-
max_length=256, error_messages={'required': 'Please, enter your name.'}, label='')
44-
company = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Company', 'class': 'form-control'}),
45-
max_length=100, required=False, label='')
46-
email = forms.EmailField(widget=forms.TextInput(attrs={'placeholder': 'Email', 'class': 'form-control'}),
47-
error_messages={'required': 'Please, enter a valid email address.',
48-
'invalid': 'Please enter a valid email address.'}, label='')
49-
personal_website = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Personal Website (URL)', 'class': 'form-control'}),
50-
max_length=100, required=False, label='')
51-
linkedin = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Linkedin (URL)', 'class': 'form-control'}),
52-
max_length=100, required=False, label='')
53-
title = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Title of your proposal', 'class': 'form-control'}),
54-
error_messages={'required': 'Please, enter the title.'}, label='')
55-
duration = forms.CharField(widget=forms.TextInput(attrs={'placeholder': 'Duration (E.g. 2h, 30min, 1:30, etc.)', 'class': 'form-control'}),
56-
error_messages={'required': 'Please, enter the duration.'}, label='')
57-
description= forms.CharField(widget=forms.Textarea(attrs={'placeholder': 'Description of your proposal', 'class': 'form-control'}),
58-
error_messages={'required': 'Please, enter the description of your proposal.'}, label='')
59-
type = forms.ChoiceField(choices=TYPE_CFP, widget=forms.Select())
46+
class CFPRating(models.Model):
47+
mark = models.IntegerField(validators=[MaxValueValidator(10, "Maximum rating is 10"), MinValueValidator(0, "Minimum Rating is 10")])
48+
cfp = models.ForeignKey(Cfp, related_name="ratings", on_delete=CASCADE)
49+
user = models.ForeignKey(getattr(settings, "AUTH_USER_MODEL"), on_delete=CASCADE)
50+
tags = TaggableManager()
6051

6152
class Meta:
62-
model = Cfp
63-
fields = (
64-
'name',
65-
'company',
66-
'email',
67-
'personal_website',
68-
'linkedin',
69-
'title',
70-
'duration',
71-
'description',
72-
'type',
73-
)
53+
unique_together = (("user", "cfp",),)
54+

pyconbalkan/cfp/templates/cfp_detail.html

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ <h2>by {{ cfp.name }} {% if cfp.company %}({{ cfp.company }}){% endif %}</h2>
1414
<p class="cfp__description">
1515
{{ cfp.description|safe }}
1616
</p>
17+
18+
{% if success %}
19+
<h2 class="success-message">
20+
{{ success }}
21+
</h2>
22+
{% endif %}
23+
24+
<form class="form" action="{% url 'cfp_detail' cfp.slug %}" method="POST" enctype="multipart/form-data" novalidate>
25+
<div class="form-group">
26+
{% csrf_token %}
27+
{{ form.as_p }}
28+
<input class="button button--blue button--fullwidth mb-xs-40" type="submit" value="Submit" />
29+
</div>
30+
</form>
1731
</div>
1832
</div>
1933

pyconbalkan/cfp/templates/cfp_list.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
{% for cfp in cfps %}
1111
<li>
1212
<a href="{% url 'cfp_detail' slug=cfp.slug %}">
13-
{{ cfp.get_type_display }}: {{ cfp.title }} by {{ cfp.name }}
13+
{{ cfp.get_type_display }}: {{ cfp.title }} by {{ cfp.name }} - {{ cfp.my_rating }} {% if request.user.is_superuser %} - {{ cfp.rating }}{% endif %}
1414
</a>
1515
</li>
1616
{% endfor %}

pyconbalkan/cfp/views.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
from django.contrib.auth.decorators import login_required
22
from django.core.mail import EmailMessage
3+
from django.db.models import F, Subquery, OuterRef
34
from django.shortcuts import render, get_object_or_404
45
from rest_framework import viewsets
56
from rest_framework.permissions import DjangoModelPermissions
67

7-
from pyconbalkan.cfp.models import Cfp, CfpForm
8+
from pyconbalkan.cfp.forms import CfpForm, RateForm
9+
from pyconbalkan.cfp.models import Cfp, CFPRating
810
from pyconbalkan.cfp.serializers import CfpSerializer
911
from pyconbalkan.conference.models import Conference
1012

@@ -44,7 +46,10 @@ def cfp_view(request):
4446
@login_required
4547
def cfp_list(request):
4648
conference = Conference.objects.filter(active=True)
47-
cfps = Cfp.objects.all()
49+
cfps = Cfp.objects.annotate(
50+
my_rating=Subquery(CFPRating.objects.filter(cfp=OuterRef('pk'), user=request.user).values('mark'))
51+
)
52+
4853
context = {
4954
'cfps': cfps,
5055
'conference': conference.first() if conference else None,
@@ -60,4 +65,24 @@ def cfp_detail(request, slug):
6065
'cfp': cfp,
6166
'conference': conference.first() if conference else None,
6267
}
68+
69+
initial = {
70+
'user': request.user,
71+
'cfp': cfp
72+
}
73+
74+
try:
75+
rating_instance = CFPRating.objects.get(**initial)
76+
except CFPRating.DoesNotExist:
77+
rating_instance = CFPRating(**initial)
78+
79+
if request.method == 'POST':
80+
form = RateForm(request.POST, instance=rating_instance)
81+
if form.is_valid():
82+
form.save()
83+
else:
84+
form = RateForm(instance=rating_instance)
85+
86+
context['form'] = form
87+
6388
return render(request, 'cfp_detail.html', context)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{% extends "base.html" %}
2+
3+
{% block main_content %}
4+
5+
{% if form.errors %}
6+
<p>Your username and password didn't match. Please try again.</p>
7+
{% endif %}
8+
9+
{% if next %}
10+
{% if user.is_authenticated %}
11+
<p>Your account doesn't have access to this page. To proceed,
12+
please login with an account that has access.</p>
13+
{% else %}
14+
<p>Please login to see this page.</p>
15+
{% endif %}
16+
{% endif %}
17+
18+
<form method="post" action="{% url 'login' %}">
19+
{% csrf_token %}
20+
21+
<div>
22+
<td>{{ form.username.label_tag }}</td>
23+
<td>{{ form.username }}</td>
24+
</div>
25+
<div>
26+
<td>{{ form.password.label_tag }}</td>
27+
<td>{{ form.password }}</td>
28+
</div>
29+
30+
<div>
31+
<input type="submit" value="login" />
32+
<input type="hidden" name="next" value="{{ next }}" />
33+
</div>
34+
</form>
35+
36+
{# Assumes you setup the password_reset view in your URLconf #}
37+
<p><a href="{% url 'password_reset' %}">Lost password?</a></p>
38+
39+
{% endblock %}

pyconbalkan/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@
7373
]
7474

7575
ROOT_URLCONF = 'pyconbalkan.urls'
76+
# Redirect to home URL after login (Default redirects to /accounts/profile/)
77+
LOGIN_REDIRECT_URL = '/cfps'
7678

7779
TEMPLATES = [
7880
{

pyconbalkan/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,8 @@
5858
path('markdownx/', include(markdownx)),
5959
path('timetable/', timetable_view, name='timetable')
6060
]
61+
62+
# Add Django site authentication urls (for login, logout, password management)
63+
urlpatterns += [
64+
path('accounts/', include('django.contrib.auth.urls')),
65+
]

0 commit comments

Comments
 (0)