manager: agenda categories (#45448)
This commit is contained in:
parent
d5a83ff7f5
commit
1cb955c8da
|
@ -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'
|
||||
),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue