Skip to content

Commit

Permalink
Merge pull request compserv#204 from alexander-zw/permissions
Browse files Browse the repository at this point in the history
Replace groups with permissions and add custom error pages
  • Loading branch information
alexander-zw authored Apr 6, 2020
2 parents 3ddd229 + bebbaa9 commit f2ff6d2
Show file tree
Hide file tree
Showing 16 changed files with 265 additions and 169 deletions.
17 changes: 3 additions & 14 deletions hknweb/candidate/static/candidate/style.css
Original file line number Diff line number Diff line change
@@ -1,8 +1,3 @@
.content {
padding-top: 1em;
padding-bottom: 1em;
}

.centered {
max-width: 1250px;
margin: 0 auto;
Expand Down Expand Up @@ -246,9 +241,10 @@ h2:first-child {
visibility: visible;
}

#confirm-text {
font-size: 1.4em;
.confirm-text {
font-size: 1.3em;
margin-left: 10%;
margin-right: 10%;
}

#sunny {
Expand All @@ -266,10 +262,3 @@ h2:first-child {
text-align: right;
float: right;
}

#hiding-cat {
width: 45%;
display: block; /* Center image */
margin-left: auto;
margin-right: auto;
}
9 changes: 5 additions & 4 deletions hknweb/candidate/templates/candidate/review_confirm.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
{% block content %}

<div class="parent">
<span id="confirm-text">
<div class="confirm-text">
You have successfully reviewed the challenge requested by {{ requester_name }}!
</span>
<br><br>
</div>
<br>

<img id="sunny" src="{% static 'candidate/seal_sunny.jpg'%}" alt="cute dog">
<span id="sunny-caption">
Expand All @@ -28,7 +28,7 @@

<div class="links-div link">
<a href="{% url 'candidate:offrequests' %}">Go to portal</a>
<a href="{% url 'candidate:detail' challenge.id %} class="right"">View this challenge</a>
<a href="{% url 'candidate:detail' challenge.id %}" class="right">View this challenge</a>
</div>
<br><br>

Expand All @@ -48,6 +48,7 @@
<li>I dreamed of dating (the 20 year-old) him last week. <br>
There was a hot blond but I still chose my shy Jay. </li>
</ul> -->
<!-- Author: Catherine Hu -->
</div>


Expand Down
116 changes: 38 additions & 78 deletions hknweb/candidate/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,25 @@
from django.views.generic.edit import FormView
from django.shortcuts import render, redirect, reverse
from django.core.mail import EmailMultiAlternatives
from django.core.exceptions import PermissionDenied
from django.template.loader import render_to_string
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from django.utils import timezone
from django.conf import settings
from django.contrib.staticfiles.finders import find
from django.db.models import Q
from dal import autocomplete
from random import randint

from hknweb.utils import login_and_permission, method_login_and_permission, get_rand_photo
from .models import OffChallenge, BitByteActivity, Announcement, CandidateForm
from ..events.models import Event, Rsvp
from .forms import ChallengeRequestForm, ChallengeConfirmationForm, BitByteRequestForm

# decorators

# used for things only officers and candidates can access
# TODO: use permissions instead of just the groups
def check_account_access(func):
def check_then_call(request, *args, **kwargs):
if not is_cand_or_officer(request.user):
return render(request, "errors/401.html", status=401)
return func(request, *args, **kwargs)
return check_then_call


# views

# Candidate portal home
@method_decorator(login_required(login_url='/accounts/login/'), name='dispatch')
@method_decorator(check_account_access, name='dispatch')
# @method_decorator(is_cand_or_officer)
@method_login_and_permission('candidate.view_announcement')
class IndexView(generic.TemplateView):
""" Candidate portal home. """
template_name = 'candidate/index.html'
context_object_name = 'my_favorite_publishers'

Expand Down Expand Up @@ -90,11 +74,9 @@ def get_context_data(self):
}
return context

# Form for submitting officer challenge requests
# And list of past requests for candidate
@method_decorator(login_required(login_url='/accounts/login/'), name='dispatch')
@method_decorator(check_account_access, name='dispatch')
@method_login_and_permission('candidate.add_offchallenge')
class CandRequestView(FormView, generic.ListView):
""" Form for submitting officer challenge requests and list of past requests for candidate. """
template_name = 'candidate/candreq.html'
form_class = ChallengeRequestForm
success_url = "/cand/candreq"
Expand Down Expand Up @@ -139,12 +121,11 @@ def get_queryset(self):
.order_by('-request_date')
return result

# List of past challenge requests for officer
# Non-officers can still visit this page by typing in the url,
# but it will not have any new entries
@method_decorator(login_required(login_url='/accounts/login/'), name='dispatch')
@method_decorator(check_account_access, name='dispatch')
@method_login_and_permission('candidate.view_offchallenge')
class OffRequestView(generic.ListView):
""" List of past challenge requests for officer.
Non-officers can still visit this page by typing in the url,
but it will not have any new entries. """
template_name = 'candidate/offreq.html'

context_object_name = 'challenge_list'
Expand All @@ -155,11 +136,10 @@ def get_queryset(self):
.order_by('-request_date')
return result

# List of past bit-byte activities for candidates
# Offices can still visit this page but it will not have any new entries
@method_decorator(login_required(login_url='/accounts/login/'), name='dispatch')
@method_decorator(check_account_access, name='dispatch')
@method_login_and_permission('candidate.add_bitbyteactivity')
class BitByteView(FormView, generic.ListView):
""" Form for submitting bit-byte activity requests and list of past requests for candidate.
Officers can still visit this page, but it will not have any new entries. """
template_name = 'candidate/bitbyte.html'
form_class = BitByteRequestForm
success_url = "/cand/bitbyte"
Expand Down Expand Up @@ -203,15 +183,13 @@ def get_queryset(self):
.order_by('-request_date')
return result

# Officer views and confirms a challenge request after clicking email link
# Only the officer who game the challenge can review it
@login_required(login_url='/accounts/login/')
@check_account_access
@login_and_permission('candidate.change_offchallenge')
def officer_confirm_view(request, pk):
# TODO: gracefully handle when a challenge does not exist
""" Officer views and confirms a challenge request after clicking email link.
Only the officer who gave the challenge can review it. """
challenge = OffChallenge.objects.get(id=pk)
if request.user.id != challenge.officer.id:
return render(request, "errors/401.html", status=401)
raise PermissionDenied # not the officer that gave the challenge

requester_name = challenge.requester.get_full_name()
form = ChallengeConfirmationForm(request.POST or None, instance=challenge)
Expand All @@ -235,11 +213,9 @@ def officer_confirm_view(request, pk):
return redirect('/cand/reviewconfirm/{}'.format(pk))
return render(request, "candidate/challenge_confirm.html", context=context)


# The page displayed after officer reviews challenge and clicks "submit"
@login_required(login_url='/accounts/login/')
@check_account_access
@login_and_permission('candidate.view_offchallenge')
def officer_review_confirmation(request, pk):
""" The page displayed after officer reviews challenge and clicks "submit." """
challenge = OffChallenge.objects.get(id=pk)
requester_name = challenge.requester.get_full_name()
context = {
Expand All @@ -248,11 +224,9 @@ def officer_review_confirmation(request, pk):
}
return render(request, "candidate/review_confirm.html", context=context)


# Detail view of an officer challenge
@login_required(login_url='/accounts/login/')
@check_account_access
@login_and_permission('candidate.view_offchallenge')
def challenge_detail_view(request, pk):
""" Detail view of an officer challenge. """
challenge = OffChallenge.objects.get(id=pk)
officer_name = challenge.officer.get_full_name()
requester_name = challenge.requester.get_full_name()
Expand All @@ -275,45 +249,31 @@ def challenge_detail_view(request, pk):
}
return render(request, "candidate/challenge_detail.html", context=context)


# HELPERS

# this is needed otherwise anyone can see the users in the database
@method_login_and_permission('auth.view_user')
class OfficerAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return User.objects.none()

qs = User.objects.filter(groups__name=settings.OFFICER_GROUP)
if self.q:
qs = qs.filter(Q(username__icontains=self.q) | Q(first_name__icontains=self.q) | Q(last_name__icontains=self.q))
qs = qs.filter(
Q(username__icontains=self.q) |
Q(first_name__icontains=self.q) |
Q(last_name__icontains=self.q))
return qs

# this is needed otherwise anyone can see the users in the database
@method_login_and_permission('auth.view_user')
class UserAutocomplete(autocomplete.Select2QuerySetView):
def get_queryset(self):
if not self.request.user.is_authenticated:
return User.objects.none()

qs = User.objects.all()
if self.q:
qs = qs.filter(
Q(username__icontains=self.q) | Q(first_name__icontains=self.q) | Q(last_name__icontains=self.q))
Q(username__icontains=self.q) |
Q(first_name__icontains=self.q) |
Q(last_name__icontains=self.q))
return qs

def is_cand_or_officer(user):
return user.groups.filter(name=settings.CAND_GROUP).exists() or \
user.groups.filter(name=settings.OFFICER_GROUP).exists()

# This function is not used; it can be used to view all photos available
def get_all_photos():
with open(find("candidate/animal_photo_urls.txt")) as f:
urls = f.readlines()
return [url.strip() + "?w=400" for url in urls]

# images from pexels.com
def get_rand_photo(width=400):
with open(find("candidate/animal_photo_urls.txt")) as f:
urls = f.readlines()
return urls[randint(0, len(urls) - 1)].strip() + "?w=" + str(width)
# HELPERS

def send_challenge_confirm_email(request, challenge, confirmed):
subject = '[HKN] Your officer challenge was reviewed'
Expand Down Expand Up @@ -359,8 +319,8 @@ def send_bitbyte_confirm_email(request, bitbyte, confirmed):
msg.send()


# what the event types are called on admin site
# code will not work if they're called something else!!
""" What the event types are called on admin site.
Code will not work if they're called something else!! """
map_event_vars = {
settings.MANDATORY_EVENT: 'Mandatory',
settings.FUN_EVENT: 'Fun',
Expand All @@ -370,9 +330,9 @@ def send_bitbyte_confirm_email(request, bitbyte, confirmed):
settings.HANGOUT_EVENT: 'Hangout',
}

# Takes in all confirmed rsvps and sorts them into types, current hard coded
# TODO: support more flexible typing and string-to-var parsing/conversion
def sort_rsvps_into_events(rsvps):
""" Takes in all confirmed rsvps and sorts them into types, currently hard coded. """
# Events in admin are currently in a readable format, must convert them to callable keys for Django template
sorted_events = dict.fromkeys(map_event_vars.keys())
for event_key, event_type in map_event_vars.items():
Expand All @@ -394,8 +354,8 @@ def sort_rsvps_into_events(rsvps):
settings.BITBYTE_ACTIVITY: 3,
}

# Checks which requirements have been fulfilled by a candidate
def check_requirements(sorted_rsvps, num_challenges, num_bitbytes):
""" Checks which requirements have been fulfilled by a candidate. """
req_statuses = dict.fromkeys(req_list.keys(), False)
for req_type, minimum in req_list.items():
if req_type == settings.BITBYTE_ACTIVITY:
Expand All @@ -409,6 +369,6 @@ def check_requirements(sorted_rsvps, num_challenges, num_bitbytes):
req_statuses[req_type] = True
return req_statuses

# returns whether officer interactivities are satisfied
def check_interactivity_requirements(hangouts, challenges):
""" Returns whether officer interactivities are satisfied. """
return hangouts >= 1 and challenges >= 1 and hangouts + challenges >= 3
3 changes: 2 additions & 1 deletion hknweb/events/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ def waitlist_set(self):
if not self.rsvp_limit:
return self.rsvp_set.none()
return self.rsvp_set.order_by("created_at")[self.rsvp_limit:]



class Rsvp(models.Model):
user = models.ForeignKey(User, models.CASCADE, verbose_name="rsvp'd by")
event = models.ForeignKey(Event, models.CASCADE)
Expand Down
2 changes: 1 addition & 1 deletion hknweb/events/templates/events/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ <h3 {% if messages.tags %} class="{{ message.tags }}" {% endif %} style="color:
</ul>

<!-- FullCalendar -->
<section class="pb3" id='calendar'></section>
<section id='calendar'></section>

{% if perms.events.add_event %}
<form action="new" method="GET" id="button">
Expand Down
Loading

0 comments on commit f2ff6d2

Please sign in to comment.