manager: agenda categories (#45448)

This commit is contained in:
Lauréline Guérin 2020-07-24 15:59:49 +02:00
parent d5a83ff7f5
commit 1cb955c8da
No known key found for this signature in database
GPG Key ID: 1FAB9B9B4F93D473
11 changed files with 336 additions and 5 deletions

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('agendas', '0053_event_date_range_constraint'),
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
(
'id',
models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
),
('slug', models.SlugField(max_length=160, unique=True, verbose_name='Identifier')),
('label', models.CharField(max_length=150, verbose_name='Label')),
],
options={'ordering': ['label'],},
),
migrations.AddField(
model_name='agenda',
name='category',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='agendas.Category'
),
),
]

View File

@ -149,6 +149,7 @@ class Agenda(models.Model):
on_delete=models.SET_NULL,
)
resources = models.ManyToManyField('Resource')
category = models.ForeignKey('Category', blank=True, null=True, on_delete=models.SET_NULL)
default_view = models.CharField(_('Default view'), max_length=20, choices=AGENDA_VIEWS, default='month')
class Meta:
@ -256,6 +257,7 @@ class Agenda(models.Model):
'label': self.label,
'slug': self.slug,
'kind': self.kind,
'category': self.category.slug if self.category else None,
'minimal_booking_delay': self.minimal_booking_delay,
'maximal_booking_delay': self.maximal_booking_delay,
'permissions': {
@ -295,6 +297,11 @@ class Agenda(models.Model):
if resource_slug not in resources_by_slug:
raise AgendaImportError(_('Missing "%s" resource') % resource_slug)
data = clean_import_data(cls, data)
if data.get('category'):
try:
data['category'] = Category.objects.get(slug=data['category'])
except Category.DoesNotExist:
raise AgendaImportError(_('Missing "%s" category') % data['category'])
agenda, created = cls.objects.get_or_create(slug=data['slug'], defaults=data)
if not created:
for k, v in data.items():
@ -1265,6 +1272,26 @@ class Resource(models.Model):
return slugify(self.label)
class Category(models.Model):
slug = models.SlugField(_('Identifier'), max_length=160, unique=True)
label = models.CharField(_('Label'), max_length=150)
def __str__(self):
return self.label
class Meta:
ordering = ['label']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug(self)
super().save(*args, **kwargs)
@property
def base_slug(self):
return slugify(self.label)
def ics_directory_path(instance, filename):
return 'ics/{0}/{1}'.format(str(uuid.uuid4()), filename)

View File

@ -38,6 +38,7 @@ from chrono.agendas.models import (
TimePeriodExceptionSource,
VirtualMember,
Resource,
Category,
WEEKDAYS_LIST,
)
@ -48,7 +49,7 @@ from .widgets import DateTimeWidget
class AgendaAddForm(forms.ModelForm):
class Meta:
model = Agenda
fields = ['label', 'kind', 'edit_role', 'view_role']
fields = ['label', 'kind', 'category', 'edit_role', 'view_role']
edit_role = forms.ModelChoiceField(
label=_('Edit Role'), required=False, queryset=Group.objects.all().order_by('name')
@ -64,6 +65,7 @@ class AgendaEditForm(AgendaAddForm):
fields = [
'label',
'slug',
'category',
'edit_role',
'view_role',
'minimal_booking_delay',
@ -92,6 +94,18 @@ class ResourceEditForm(forms.ModelForm):
fields = ['label', 'slug', 'description']
class CategoryAddForm(forms.ModelForm):
class Meta:
model = Category
fields = ['label']
class CategoryEditForm(forms.ModelForm):
class Meta:
model = Category
fields = ['label', 'slug']
class NewEventForm(forms.ModelForm):
class Meta:
model = Event

View File

@ -0,0 +1,22 @@
{% extends "chrono/manager_home.html" %}
{% load i18n %}
{% block appbar %}
{% if object.id %}
<h2>{% trans "Edit Category" %}</h2>
{% else %}
<h2>{% trans "New Category" %}</h2>
{% endif %}
{% endblock %}
{% block content %}
<form method="post" enctype="multipart/form-data">
{% csrf_token %}
{{ form.as_p }}
<div class="buttons">
<button class="submit-button">{% trans "Save" %}</button>
<a class="cancel" href="{% url 'chrono-manager-category-list' %}">{% trans 'Cancel' %}</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "chrono/manager_base.html" %}
{% load i18n %}
{% block breadcrumb %}
{{ block.super }}
<a href="{% url 'chrono-manager-category-list' %}">{% trans "Categories" %}</a>
{% endblock %}
{% block appbar %}
<h2>{% trans 'Categories' %}</h2>
<span class="actions">
<a rel="popup" href="{% url 'chrono-manager-category-add' %}">{% trans 'New' %}</a>
</span>
{% endblock %}
{% block content %}
{% if object_list %}
<div>
<ul class="objects-list single-links">
{% for object in object_list %}
<li>
<a href="{% url 'chrono-manager-category-edit' pk=object.pk %}">{{ object.label }} ({{ object.slug }})</a>
<a rel="popup" class="delete" href="{% url 'chrono-manager-category-delete' pk=object.id %}">{% trans "remove" %}</a>
</li>
{% endfor %}
</ul>
</div>
{% else %}
<div class="big-msg-info">
{% blocktrans %}
This site doesn't have any category yet. Click on the "New" button in the top
right of the page to add a first one.
{% endblocktrans %}
</div>
{% endif %}
{% endblock %}

View File

@ -5,6 +5,7 @@
<h2>{% trans 'Agendas' %}</h2>
{% if user.is_staff %}
<span class="actions">
<a href="{% url 'chrono-manager-category-list' %}">{% trans 'Categories' %}</a>
<a href="{% url 'chrono-manager-resource-list' %}">{% trans 'Resources' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agendas-import' %}">{% trans 'Import' %}</a>
<a rel="popup" href="{% url 'chrono-manager-agenda-add' %}">{% trans 'New' %}</a>
@ -15,13 +16,17 @@
{% block content %}
{% if object_list %}
<div>
{% regroup object_list by category as agenda_groups %}
{% for group in agenda_groups %}
<div class="section">
{% if group.grouper %}<h3>{{ group.grouper }}</h3>{% elif not forloop.first %}<h3>{% trans "Misc" %}</h3>{% endif %}
<ul class="objects-list single-links">
{% for object in object_list %}
<li><a href="{% url 'chrono-manager-agenda-view' pk=object.id %}">{{ object.label }}</a></li>
{% for object in group.list %}
<li><a href="{% url 'chrono-manager-agenda-view' pk=object.id %}"><span class="badge">{{ object.get_kind_display }}</span> {{ object.label }}</a></li>
{% endfor %}
</ul>
</div>
{% endfor %}
{% else %}
<div class="big-msg-info">
{% blocktrans %}

View File

@ -35,6 +35,10 @@ urlpatterns = [
),
url(r'^resource/(?P<pk>\d+)/edit/$', views.resource_edit, name='chrono-manager-resource-edit'),
url(r'^resource/(?P<pk>\d+)/delete/$', views.resource_delete, name='chrono-manager-resource-delete'),
url(r'^categories/$', views.category_list, name='chrono-manager-category-list'),
url(r'^category/add/$', views.category_add, name='chrono-manager-category-add'),
url(r'^category/(?P<pk>\d+)/edit/$', views.category_edit, name='chrono-manager-category-edit'),
url(r'^category/(?P<pk>\d+)/delete/$', views.category_delete, name='chrono-manager-category-delete'),
url(r'^agendas/add/$', views.agenda_add, name='chrono-manager-agenda-add'),
url(r'^agendas/import/$', views.agendas_import, name='chrono-manager-agendas-import'),
url(r'^agendas/(?P<pk>\d+)/$', views.agenda_view, name='chrono-manager-agenda-view'),

View File

@ -60,6 +60,7 @@ from chrono.agendas.models import (
TimePeriodExceptionSource,
VirtualMember,
Resource,
Category,
)
from .forms import (
@ -83,6 +84,8 @@ from .forms import (
ResourceEditForm,
AgendaResourceForm,
AgendaDuplicateForm,
CategoryAddForm,
CategoryEditForm,
)
from .utils import import_site
@ -98,7 +101,7 @@ class HomepageView(ListView):
if not self.request.user.is_staff:
group_ids = [x.id for x in self.request.user.groups.all()]
queryset = queryset.filter(Q(view_role_id__in=group_ids) | Q(edit_role_id__in=group_ids))
return queryset
return queryset.order_by('category__label', 'label')
homepage = HomepageView.as_view()
@ -460,6 +463,69 @@ class ResourceDeleteView(DeleteView):
resource_delete = ResourceDeleteView.as_view()
class CategoryListView(ListView):
template_name = 'chrono/manager_category_list.html'
model = Category
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
category_list = CategoryListView.as_view()
class CategoryAddView(CreateView):
template_name = 'chrono/manager_category_form.html'
model = Category
form_class = CategoryAddForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-category-list')
category_add = CategoryAddView.as_view()
class CategoryEditView(UpdateView):
template_name = 'chrono/manager_category_form.html'
model = Category
form_class = CategoryEditForm
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-category-list')
category_edit = CategoryEditView.as_view()
class CategoryDeleteView(DeleteView):
template_name = 'chrono/manager_confirm_delete.html'
model = Category
def dispatch(self, request, *args, **kwargs):
if not request.user.is_staff:
raise PermissionDenied()
return super().dispatch(request, *args, **kwargs)
def get_success_url(self):
return reverse('chrono-manager-category-list')
category_delete = CategoryDeleteView.as_view()
class AgendaAddView(CreateView):
template_name = 'chrono/manager_agenda_form.html'
model = Agenda

View File

@ -12,6 +12,7 @@ from django.utils.timezone import localtime, make_aware, now
from chrono.agendas.models import (
Agenda,
Booking,
Category,
Desk,
Event,
ICSError,
@ -149,6 +150,25 @@ def test_resource_duplicate_slugs():
assert resource.slug == 'foo-baz-2'
def test_category_slug():
category = Category.objects.create(label=u'Foo bar')
assert category.slug == 'foo-bar'
def test_category_existing_slug():
category = Category.objects.create(label=u'Foo bar', slug='bar')
assert category.slug == 'bar'
def test_category_duplicate_slugs():
category = Category.objects.create(label=u'Foo baz')
assert category.slug == 'foo-baz'
category = Category.objects.create(label=u'Foo baz')
assert category.slug == 'foo-baz-1'
category = Category.objects.create(label=u'Foo baz')
assert category.slug == 'foo-baz-2'
def test_event_slug():
other_agenda = Agenda.objects.create(label='Foo bar')
Event.objects.create(agenda=other_agenda, places=42, start_datetime=now(), slug='foo-bar')

View File

@ -19,6 +19,7 @@ from django.utils.timezone import make_aware, now
from chrono.agendas.models import (
Agenda,
Category,
Desk,
Event,
Resource,
@ -224,6 +225,30 @@ def test_import_export_resources(app):
assert list(agenda.resources.all()) == [resource]
def test_import_export_categorys(app):
category = Category.objects.create(label='foo')
agenda = Agenda.objects.create(label='Foo Bar', category=category)
output = get_output_of_command('export_site')
import_site(data={}, clean=True)
assert Agenda.objects.count() == 0
category.delete()
with pytest.raises(AgendaImportError) as excinfo:
import_site(json.loads(output), overwrite=True)
assert str(excinfo.value) == 'Missing "foo" category'
category = Category.objects.create(label='foobar')
with pytest.raises(AgendaImportError) as excinfo:
import_site(json.loads(output), overwrite=True)
assert str(excinfo.value) == 'Missing "foo" category'
category = Category.objects.create(label='foo')
import_site(json.loads(output), overwrite=True)
agenda = Agenda.objects.get(slug=agenda.slug)
assert agenda.category == category
def test_import_export_virtual_agenda(app):
virtual_agenda = Agenda.objects.create(label='Virtual Agenda', kind='virtual')
output = get_output_of_command('export_site')

View File

@ -22,6 +22,7 @@ from webtest import Upload
from chrono.agendas.models import (
Agenda,
Booking,
Category,
Desk,
Event,
MeetingType,
@ -689,6 +690,82 @@ def test_delete_resource(app, admin_user):
assert Resource.objects.exists() is False
def test_list_categories_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo Bar')
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
app = login(app, username='manager', password='manager')
app.get('/manage/categories/', status=403)
resp = app.get('/manage/', status=200)
assert 'Categories' not in resp.text
def test_add_category(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)
resp = resp.click('Categories')
resp = resp.click('New')
resp.form['label'] = 'Foo bar'
resp = resp.form.submit()
category = Category.objects.latest('pk')
assert resp.location.endswith('/manage/categories/')
assert category.label == 'Foo bar'
assert category.slug == 'foo-bar'
def test_add_category_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo Bar')
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
app = login(app, username='manager', password='manager')
app.get('/manage/category/add/', status=403)
def test_edit_category(app, admin_user):
category = Category.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/categories/', status=200)
resp = resp.click(href='/manage/category/%s/edit/' % category.pk)
resp.form['label'] = 'Foo bar baz'
resp.form['slug'] = 'baz'
resp = resp.form.submit()
assert resp.location.endswith('/manage/categories/')
category.refresh_from_db()
assert category.label == 'Foo bar baz'
assert category.slug == 'baz'
def test_edit_category_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo Bar')
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
category = Category.objects.create(label='Foo bar')
app = login(app, username='manager', password='manager')
app.get('/manage/category/%s/edit/' % category.pk, status=403)
def test_delete_category(app, admin_user):
category = Category.objects.create(label='Foo bar')
app = login(app)
resp = app.get('/manage/categories/', status=200)
resp = resp.click(href='/manage/category/%s/delete/' % category.pk)
resp = resp.form.submit()
assert resp.location.endswith('/manage/categories/')
assert Category.objects.exists() is False
def test_delete_category_as_manager(app, manager_user):
agenda = Agenda(label=u'Foo Bar')
agenda.view_role = manager_user.groups.all()[0]
agenda.save()
category = Category.objects.create(label='Foo bar')
app = login(app, username='manager', password='manager')
app.get('/manage/category/%s/delete/' % category.pk, status=403)
def test_add_agenda(app, admin_user):
app = login(app)
resp = app.get('/manage/', status=200)