diff --git a/tavern/forms.py b/tavern/forms.py index 53934ac..dd7372a 100644 --- a/tavern/forms.py +++ b/tavern/forms.py @@ -1,6 +1,7 @@ from django import forms from django.contrib.auth.models import User from django.contrib.auth.forms import UserCreationForm +from django.forms.widgets import HiddenInput from .models import TavernGroup, Event, Membership from bootstrap3_datetime.widgets import DateTimePicker @@ -21,7 +22,7 @@ class Meta: exclude = ['creator', 'slug', 'show'] def __init__(self, *args, **kwargs): - self.user = kwargs.pop('user', None) + self.user = kwargs.pop('current_user', None) super(CreateEventForm, self).__init__(*args, **kwargs) self.fields['group'].queryset = self.user.tavern_groups.all() self.fields['starts_at'].widget = DateTimePicker(options={"format": "YYYY-MM-DD HH:mm", }) @@ -63,3 +64,38 @@ def save(self, commit=True): if commit: user.save() return user + + +class AddOrganizerForm(forms.Form): + usernames = forms.CharField() + + def clean(self): + cleaned_data = super(AddOrganizerForm, self).clean() + usernames = cleaned_data.get('usernames') + cleaned_data['users'] = [] + for username in usernames.split(','): + try: + user = User.objects.get(username=username.strip()) + cleaned_data['users'].append(user) + except User.DoesNotExist: + raise forms.ValidationError("%s is not a user" % username.strip()) + return cleaned_data + + +class RemoveOrganizerForm(AddOrganizerForm): + group = forms.CharField(max_length=5) + + def __init__(self, *args, **kwargs): + super(RemoveOrganizerForm, self).__init__(*args, **kwargs) + self.fields['group'].widget = HiddenInput() + + def clean(self): + cleaned_data = super(RemoveOrganizerForm, self).clean() + users = cleaned_data.get('users') + group = TavernGroup.objects.get(pk=cleaned_data['group']) + for user in users: + try: + group.organizers.get(username=user.username) + except User.DoesNotExist: + raise forms.ValidationError("%s is not an organizer of %s group" % (user.username, group.name)) + return cleaned_data diff --git a/tavern/multiform.py b/tavern/multiform.py new file mode 100644 index 0000000..5672467 --- /dev/null +++ b/tavern/multiform.py @@ -0,0 +1,135 @@ +# Class-based Views for handling multiple forms +# https://gist.github.com/jamesbrobb/748c47f46b9bd224b07f + +from django.views.generic.base import TemplateResponseMixin, ContextMixin +from django.views.generic.edit import ProcessFormView +from django.http import HttpResponseRedirect, HttpResponseForbidden + + +class MultiFormMixin(ContextMixin): + + form_classes = {} + prefixes = {} + success_urls = {} + grouped_forms = {} + + initial = {} + prefix = None + success_url = None + + def get_form_classes(self): + return self.form_classes + + def get_forms(self, form_classes, form_names=None, bind_all=False): + return dict([(key, self._create_form(key, klass, + (form_names and key in form_names) or bind_all)) for key, klass in form_classes.items()]) + + def get_form_kwargs(self, form_name, bind_form=False): + kwargs = {} + kwargs.update({'initial': self.get_initial(form_name)}) + kwargs.update({'prefix': self.get_prefix(form_name)}) + + if bind_form: + kwargs.update(self._bind_form_data()) + + return kwargs + + def forms_valid(self, forms, form_name): + form_valid_method = '%s_form_valid' % form_name + if hasattr(self, form_valid_method): + return getattr(self, form_valid_method)(forms[form_name]) + else: + return HttpResponseRedirect(self.get_success_url(form_name)) + + def forms_invalid(self, forms): + return self.render_to_response(self.get_context_data(forms=forms)) + + def get_initial(self, form_name): + initial_method = 'get_%s_initial' % form_name + if hasattr(self, initial_method): + return getattr(self, initial_method)() + else: + return self.initial.copy() + + def get_prefix(self, form_name): + return self.prefixes.get(form_name, self.prefix) + + def get_success_url(self, form_name=None): + return self.success_urls.get(form_name, self.success_url) + + def _create_form(self, form_name, klass, bind_form): + form_kwargs = self.get_form_kwargs(form_name, bind_form) + form_create_method = 'create_%s_form' % form_name + if hasattr(self, form_create_method): + form = getattr(self, form_create_method)(**form_kwargs) + else: + form = klass(**form_kwargs) + return form + + def _bind_form_data(self): + if self.request.method in ('POST', 'PUT'): + return{'data': self.request.POST, + 'files': self.request.FILES, + } + return {} + + +class ProcessMultipleFormsView(ProcessFormView): + + def get(self, request, *args, **kwargs): + form_classes = self.get_form_classes() + forms = self.get_forms(form_classes) + return self.render_to_response(self.get_context_data(forms=forms)) + + def post(self, request, *args, **kwargs): + form_classes = self.get_form_classes() + form_name = request.POST.get('action') + if self._individual_exists(form_name): + return self._process_individual_form(form_name, form_classes) + elif self._group_exists(form_name): + return self._process_grouped_forms(form_name, form_classes) + else: + return self._process_all_forms(form_classes) + + def _individual_exists(self, form_name): + return form_name in self.form_classes + + def _group_exists(self, group_name): + return group_name in self.grouped_forms + + def _process_individual_form(self, form_name, form_classes): + forms = self.get_forms(form_classes, (form_name,)) + form = forms.get(form_name) + if not form: + return HttpResponseForbidden() + elif form.is_valid(): + return self.forms_valid(forms, form_name) + else: + return self.forms_invalid(forms) + + def _process_grouped_forms(self, group_name, form_classes): + form_names = self.grouped_forms[group_name] + forms = self.get_forms(form_classes, form_names) + if all([forms.get(form_name).is_valid() for form_name in form_names.values()]): + return self.forms_valid(forms) + else: + return self.forms_invalid(forms) + + def _process_all_forms(self, form_classes): + forms = self.get_forms(form_classes, None, True) + if all([form.is_valid() for form in forms.values()]): + return self.forms_valid(forms) + else: + return self.forms_invalid(forms) + + +class BaseMultipleFormsView(MultiFormMixin, ProcessMultipleFormsView): + """ + A base view for displaying several forms. + """ + + +class MultiFormsView(TemplateResponseMixin, BaseMultipleFormsView): + """ + A view for displaying several forms, and rendering a template response. + """ diff --git a/tavern/signals.py b/tavern/signals.py index 4fe66be..b5d1947 100644 --- a/tavern/signals.py +++ b/tavern/signals.py @@ -6,11 +6,19 @@ from .models import TavernGroup, Event +def create_group_permission(sender, instance, **kwargs): + remove_perm('change_taverngroup', instance.creator, instance) + remove_perm('delete_taverngroup', instance.creator, instance) + assign_perm('change_taverngroup', instance.creator, instance) + assign_perm('delete_taverngroup', instance.creator, instance) + + def create_event_permission(sender, instance, created, **kwargs): remove_perm('change_event', instance.creator, instance) remove_perm('delete_event', instance.creator, instance) remove_perm('change_event', instance.group.creator, instance) remove_perm('delete_event', instance.group.creator, instance) + assign_perm('change_event', instance.creator, instance) assign_perm('delete_event', instance.creator, instance) assign_perm('change_event', instance.group.creator, instance) @@ -30,12 +38,10 @@ def create_group_permission_for_organizers(sender, instance, action, reverse, pk for pk in instance._old_m2m: user = User.objects.get(pk=pk) remove_perm('change_taverngroup', user, instance) - remove_perm('delete_taverngroup', user, instance) if action == 'post_add' and not reverse: for pk in pk_set: user = User.objects.get(pk=pk) assign_perm('change_taverngroup', user, instance) - assign_perm('delete_taverngroup', user, instance) def delete_group_permissions(sender, instance, **kwargs): @@ -43,7 +49,6 @@ def delete_group_permissions(sender, instance, **kwargs): remove_perm('delete_taverngroup', instance.creator, instance) for user in instance.organizers.all(): remove_perm('change_taverngroup', user, instance) - remove_perm('delete_taverngroup', user, instance) def delete_event_permissions(sender, instance, **kwargs): @@ -54,6 +59,7 @@ def delete_event_permissions(sender, instance, **kwargs): post_save.connect(create_event_permission, sender=Event) +post_save.connect(create_group_permission, sender=TavernGroup) pre_save.connect(group_pre_save, sender=TavernGroup) m2m_changed.connect(create_group_permission_for_organizers, sender=TavernGroup.organizers.through) pre_delete.connect(delete_group_permissions, sender=TavernGroup) diff --git a/tavern/templates/edit_organizers.html b/tavern/templates/edit_organizers.html new file mode 100644 index 0000000..56644fa --- /dev/null +++ b/tavern/templates/edit_organizers.html @@ -0,0 +1,17 @@ +{% extends "base.html" %} + +{% block content %} +

Add organizers

+
+ {% csrf_token %} + {{ forms.add.as_p }} + +
+
+

Remove organizers

+
+ {% csrf_token %} + {{ forms.remove.as_p }} + +
+{% endblock content %} diff --git a/tavern/templates/event_details.html b/tavern/templates/event_details.html index c291455..98dc9e5 100644 --- a/tavern/templates/event_details.html +++ b/tavern/templates/event_details.html @@ -72,7 +72,29 @@

Event Attendees

{% if editable %} {% get_obj_perms request.user for event as "event_perms" %} {% if "change_event" in event_perms %} -

Edit Event

+ Edit Event + {% endif %} + + {% if "delete_event" in event_perms %} + + {% endif %} {% endif %} {% endblock content %} diff --git a/tavern/templates/group_details.html b/tavern/templates/group_details.html index 02497d0..50b2929 100644 --- a/tavern/templates/group_details.html +++ b/tavern/templates/group_details.html @@ -20,7 +20,30 @@

Recently Joined: {{ user.name }}

{% get_obj_perms request.user for group as "group_perms" %} {% if "change_taverngroup" in group_perms %} -

Edit Group

+ Edit Group + {% endif %} + + {% if "delete_taverngroup" in group_perms %} + Add/Remove Organizers + + {% endif %} {% include 'past_events.html' %} diff --git a/tavern/urls.py b/tavern/urls.py index 1ebe853..517d019 100644 --- a/tavern/urls.py +++ b/tavern/urls.py @@ -6,18 +6,27 @@ url(r'^$', views.index, name='index'), url(r'^groups/(?P[\w-]+)/$', views.group_details, name='tavern_group_details'), - url(r'^events/(?P[\w-]+)/$', - views.event_details, name='tavern_event_details'), url(r'^create_group/', views.create_group, name='tavern_create_group'), url(r'^groups/(?P[\w-]+)/update', views.tavern_group_update, name='tavern_group_update'), + url(r'^groups/(?P[\w-]+)/delete', + views.group_delete, + name='delete_group'), + url(r'^groups/(?P[\w-]+)/edit_organizer', + views.edit_organizers, + name='edit_organizers'), + url(r'^events/(?P[\w-]+)/$', + views.event_details, name='tavern_event_details'), url(r'^create_event/', views.create_event, name='tavern_create_event'), url(r'^events/(?P[\w-]+)/update', views.tavern_event_update, name='tavern_event_update'), + url(r'^events/(?P[\w-]+)/delete', + views.event_delete, + name='delete_event'), url(r'^tavern_toggle_member', views.tavern_toggle_member, name='tavern_toggle_member'), url(r'^rsvp/(?P\d+)/(?P\w+)/', diff --git a/tavern/views.py b/tavern/views.py index f850206..d478182 100644 --- a/tavern/views.py +++ b/tavern/views.py @@ -3,16 +3,18 @@ from django.shortcuts import render from django.contrib.auth.decorators import login_required from django.core.urlresolvers import reverse -from django.http import HttpResponse +from django.http import HttpResponse, HttpResponseRedirect from django.contrib.auth.models import User from django.shortcuts import get_object_or_404, get_list_or_404 from django.views.generic import DetailView from django.views.generic import CreateView, UpdateView, DeleteView +from django.views.generic.detail import SingleObjectMixin from guardian.mixins import LoginRequiredMixin, PermissionRequiredMixin from .models import TavernGroup, Membership, Event, Attendee -from .forms import CreateGroupForm, CreateEventForm, UpdateEventForm +from .forms import CreateGroupForm, CreateEventForm, UpdateEventForm, AddOrganizerForm, RemoveOrganizerForm +from .multiform import MultiFormsView def today_date(): @@ -175,6 +177,50 @@ class GroupUpdate(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): return_403 = True +class GroupDelete(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): + model = TavernGroup + permission_required = 'tavern.delete_taverngroup' + render_403 = True + return_403 = True + + def get_success_url(self, **kwargs): + return reverse("index") + + +class EditOrganizers(LoginRequiredMixin, PermissionRequiredMixin, SingleObjectMixin, MultiFormsView): + template_name = "edit_organizers.html" + form_classes = {'add': AddOrganizerForm, + 'remove': RemoveOrganizerForm + } + success_url = '/' + model = TavernGroup + success_url = '/' + permission_required = 'tavern.delete_taverngroup' + render_403 = True + return_403 = True + + def get(self, request, *args, **kwargs): + self.object = self.get_object() + return super(EditOrganizers, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + self.object = self.get_object() + return super(EditOrganizers, self).post(request, *args, **kwargs) + + def get_remove_initial(self): + return {'group': self.object.pk} + + def add_form_valid(self, form): + for user in form.cleaned_data['users']: + self.object.organizers.add(user) + return HttpResponseRedirect(self.get_success_url()) + + def remove_form_valid(self, form): + for user in form.cleaned_data['users']: + self.object.organizers.remove(user) + return HttpResponseRedirect(self.get_success_url()) + + class EventCreate(LoginRequiredMixin, CreateView): """ Creates new Event """ form_class = CreateEventForm @@ -183,7 +229,7 @@ class EventCreate(LoginRequiredMixin, CreateView): def get_form_kwargs(self): kwargs = super(EventCreate, self).get_form_kwargs() - kwargs['user'] = self.request.user + kwargs['current_user'] = self.request.user return kwargs def form_valid(self, form): @@ -201,6 +247,16 @@ class EventUpdate(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): return_403 = True +class EventDelete(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): + model = Event + permission_required = 'tavern.delete_event' + render_403 = True + return_403 = True + + def get_success_url(self, **kwargs): + return reverse("tavern_group_details", kwargs={"slug": self.object.group.slug}) + + class RsvpDelete(LoginRequiredMixin, DeleteView): """ Remove a RSVP""" model = Attendee @@ -215,4 +271,7 @@ def get_success_url(self, **kwargs): create_event = EventCreate.as_view() event_details = EventDetail.as_view() group_details = GroupDetail.as_view() +group_delete = GroupDelete.as_view() +event_delete = EventDelete.as_view() delete_rsvp = RsvpDelete.as_view() +edit_organizers = EditOrganizers.as_view()