diff --git a/src/backoffice/templates/backoffice/assembly_detail.html b/src/backoffice/templates/backoffice/assembly_detail.html index 9ae3747639f8a0d4abefce12e075f59da4552d0b..da2710a8433420a664a2a631741fc1e9030e97e3 100644 --- a/src/backoffice/templates/backoffice/assembly_detail.html +++ b/src/backoffice/templates/backoffice/assembly_detail.html @@ -91,6 +91,49 @@ </div> </div> </div> +{% if assembly.requests %} +<div class="row mt-3"> + <div class="col-md-12"> + <div class="card"> + <div class="card-header">{% trans 'assembly_requests' %}</div> + <div class="card-body"> + <p class="card-text"><small class="text-muted">{% trans 'assembly_requests_help' %}</small></p> + <table class="table"> + <tbody> + {% for child in assembly.requests.all %} + {% if child.source_assembly != assembly %} + <tr> + <td> + <a href="{% url 'backoffice:assembly' pk=child.source_assembly.id %}">{{ child.source_assembly.name }}</a> + {% if child.source_assembly.state_assembly == "hidden" %}<i class="bi bi-eye-slash" title='hidden'></i> {% endif %} + </td> + <td>{{ child.state }}</td> + <td> + {% if child.state != child.RequestsState.ACCEPTED %} + <form action="{% url 'backoffice:assembly-editchildren' pk=child.destination_assembly.id %}" method="POST" style="display: inline;">{% csrf_token %} + <input type="hidden" name="approve" value="{{ child.source_assembly.pk }}"> + <input type="hidden" name="next" value="{% url 'backoffice:assembly' pk=assembly.pk %}"> + <button type="submit" class="btn btn-sm btn-primary">{% trans 'approve' %}</button> + </form> + {% endif %} + {% if child.state != child.RequestsState.REJECTED %} + <form action="{% url 'backoffice:assembly-editchildren' pk=child.destination_assembly.id %}" method="POST" style="display: inline;">{% csrf_token %} + <input type="hidden" name="reject" value="{{ child.source_assembly.pk }}"> + <input type="hidden" name="next" value="{% url 'backoffice:assembly' pk=assembly.pk %}"> + <button type="submit" class="btn btn-sm btn-danger">{% trans 'reject' %}</button> + </form> + {% endif %} + </td> + </tr> + {% endif %} + {% endfor %} + </tbody> + </table> + </div> + </div> + </div> +</div> +{% endif %} <div class="row mt-3"> {% if assembly.hierarchy == 'regular' %} @@ -126,11 +169,20 @@ <tr> <th>Name</th> <th>Beschreibung</th> + <th>Status</th> </tr> </thead> <tbody> {% for child in assembly.children.all %} - <tr><td><a href="{% url 'backoffice:assembly' pk=child.pk %}">{{ child.name }}</a></td><td>{{ child.description|truncatechars:200 }}</td></tr> + <tr> + <td><a href="{% url 'backoffice:assembly' pk=child.pk %}">{{ child.name }}</a></td> + <td>{{ child.description|truncatechars:200 }}</td> + <td> + <span class="font-weight-bold{% if child.state_parent == 'accepted' %} text-success{% elif child.state_parent == 'rejected' %} text-danger{% else %} text-default{% endif %}"> + {{ child.state_parent }} + </span> + </td> + </tr> {% endfor %} </tbody> </table> diff --git a/src/backoffice/templates/backoffice/assembly_edit.html b/src/backoffice/templates/backoffice/assembly_edit.html index 5b4b655362053b94dea87fc5c3ee80c9d191b360..f29d72928ee5d6d0da2f14d504797d2fb86c6cfe 100644 --- a/src/backoffice/templates/backoffice/assembly_edit.html +++ b/src/backoffice/templates/backoffice/assembly_edit.html @@ -61,18 +61,41 @@ {% trans 'assemblyedit_parent__intro' %} </p> + {% with assembly_requests=assembly.requests.all %} + {% if assembly_requests %} + <div class="input-group"> + <ul> + {% for request in assembly_requests %} + <li> + {% if request.source_assembly == assembly %} + {{ request.destination_assembly }} + {% else %} + {{ request.source_assembly }} + {% endif %} + <span class="badge {% if request.state == request.RequestsState.REJECTED %}bg-danger{% elif request.state == request.RequestsState.ACCEPTED %}bg-success{% else %}bg-primary{% endif %} rounded-pill py-1 px-2 text-decoration-none">{{ request.state }}</span> + </li> + {% endfor %} + </li> + </div> + {% endif %} + <div class="input-group"> <label class="form-label" for="assembly_parent">{% trans 'assembly_parent' %}</label> <select id="assembly_parent" name="parent_id"> <option value=""{% if assembly.parent is None %} selected="selected"{% endif %}>-- {% trans 'none' %} --</option> + {% if assembly.parent.pk %} + <option value="{{ assembly.parent.pk }}" selected="selected">{{ assembly.parent }}</option> + {% else %} {% if assembly.parent is not None and assembly.parent.hierarchy == assembly.Hierarchy.CLUSTER_RESTRICTED %} <option value="{{ assembly.parent.pk }}" selected="selected">{{ assembly.parent.name }}</option> {% endif %} {% for cluster in clusters %} <option value="{{ cluster.pk }}"{% if assembly.parent.pk == cluster.pk %} selected="selected"{% endif %}>{{ cluster.name }}</option> {% endfor %} + {% endif %} </select> </div> + {% endwith %} </div> </div> </div></div> diff --git a/src/backoffice/templates/backoffice/assembly_editchildren.html b/src/backoffice/templates/backoffice/assembly_editchildren.html index e325c1703ec39944f7ce7da8f61ad2ac9fe305e0..b7624381868c02c2d3a24aa352a2ba75da9d683a 100644 --- a/src/backoffice/templates/backoffice/assembly_editchildren.html +++ b/src/backoffice/templates/backoffice/assembly_editchildren.html @@ -42,19 +42,43 @@ {% trans 'assemblyedit_children' %} </div> <div class="card-body"> - {% with children=assembly.children.all %} + {% with children=assembly.requests.all %} {% if children|length > 0 %} - <ul> + <table class="table"> + <thead> + <tr> + <th>{% trans 'Assembly__name' %}</th> + <th>{% trans 'Assembly__requests_state' %}</th> + <th> </th> + </thead> + <tbody> {% for child in children %} - <li> - {% if child.state_assembly == "hidden" %}<i class="bi bi-eye-slash" title='hidden'></i> {% endif %}<a href="{% url 'backoffice:assembly' pk=child.id %}">{{ child.name }}</a> - <form action="{% url 'backoffice:assembly-editchildren' pk=assembly.id %}" method="POST" style="display: inline;">{% csrf_token %} - <input type="hidden" name="delete" value="{{ child.pk }}"> - <button type="submit" class="btn btn-sm btn-secondary">{% trans 'delete' %}</button> - </form> - </li> + <tr> + <td> + <a href="{% url 'backoffice:assembly' pk=child.destination_assembly.id %}">{{ child.destination_assembly.name }}</a> + {% if child.destination_assembly.state_assembly == "hidden" %}<i class="bi bi-eye-slash" title='hidden'></i> {% endif %} + </td> + <td>{{ child.state }}</td> + <td> + {% if child.source_assembly != assembly %} + {% if child.state != child.RequestsState.APPROVED %} + <form action="{% url 'backoffice:assembly-editchildren' pk=assembly.id %}" method="POST" style="display: inline;">{% csrf_token %} + <input type="hidden" name="approve" value="{{ child.source_assembly.pk }}"> + <button type="submit" class="btn btn-sm btn-primary">{% trans 'approve' %}</button> + </form> + {% endif %} + {% if child.state != child.RequestsState.REJECTED %} + <form action="{% url 'backoffice:assembly-editchildren' pk=assembly.id %}" method="POST" style="display: inline;">{% csrf_token %} + <input type="hidden" name="reject" value="{{ child.source_assembly.pk }}"> + <button type="submit" class="btn btn-sm btn-danger">{% trans 'reject' %}</button> + </form> + {% endif %} + {% endif %} + </td> + </tr> {% endfor %} - </ul> + </tbody> + </table> {% else %} {% trans 'no_entries' %} {% endif %} diff --git a/src/backoffice/views/assemblies.py b/src/backoffice/views/assemblies.py index 5d0e94b96cd7318fdcef0ac29f327012228ae2cd..52e1bb61922bb11b51dad3411669d62a81eaddf3 100644 --- a/src/backoffice/views/assemblies.py +++ b/src/backoffice/views/assemblies.py @@ -1,5 +1,8 @@ +from http.client import ACCEPTED +from importlib.util import source_hash import logging from datetime import date +from sys import deactivate_stack_trampoline from rest_framework.authtoken.models import Token @@ -7,9 +10,10 @@ from django.conf import settings from django.contrib import messages from django.core.exceptions import PermissionDenied from django.http import Http404, HttpResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse +from django.shortcuts import get_object_or_404, get_list_or_404, redirect, render +from django.urls import conf, reverse from django.utils import timezone +from django.utils.http import url_has_allowed_host_and_scheme from django.utils.html import format_html from django.utils.safestring import mark_safe from django.utils.text import format_lazy @@ -18,9 +22,10 @@ from django.utils.translation import gettext_lazy as _ from django.views.generic import TemplateView, View from django.views.generic.edit import CreateView, FormView, UpdateView +from core.admin import AssemblyMemberInline, AssemblyRequestsAdmin from core.integrations import BigBlueButton, Hangar, IntegrationError, WorkAdventure -from core.models.assemblies import Assembly, AssemblyLink, AssemblyMember -from core.models.conference import ConferenceExportCache +from core.models.assemblies import Assembly, AssemblyLink, AssemblyMember, AssemblyRequests +from core.models.conference import Conference, ConferenceExportCache from core.models.events import Event from core.models.rooms import Room, RoomLink from core.models.sso import Application @@ -281,17 +286,50 @@ class EditAssemblyView(AssemblyMixin, UpdateView): if parent is not None and parent.hierarchy == Assembly.Hierarchy.CLUSTER_RESTRICTED: raise PermissionDenied if parent is not None: + assembly_requests = AssemblyRequests.objects.filter(source_assembly=assembly, destination_assembly=parent) + if not assembly_requests: + logger.info( + 'Assigning assembly "%(assembly_slug)s" (%(assembly_pk)s) to "%(parent_slug)s" (%(parent_pk)s) upon request by <%(user)s>.', + { + 'assembly_slug': assembly.slug, + 'assembly_pk': assembly.pk, + 'parent_slug': parent.slug, + 'parent_pk': parent.pk, + 'user': self.request.user.username, + }, + ) + request = AssemblyRequests.objects.create( + source_assembly=assembly, + destination_assembly=parent, + request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK, + state=AssemblyRequests.RequestsState.REQUESTED, + ) + request.save() + else: + request = AssemblyRequests.objects.filter( + source_assembly=assembly, + request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK, + state=AssemblyRequests.RequestsState.ACCEPTED, + ).delete() + assembly.parent = None + assembly.save() logger.info( - 'Assigning assembly "%(assembly_slug)s" (%(assembly_pk)s) to "%(parent_slug)s" (%(parent_pk)s) upon request by <%(user)s>.', + 'Unassigned assembly "%(slug)s" (%(pk)s) upon request by <%(user)s>.', { - 'assembly_slug': assembly.slug, - 'assembly_pk': assembly.pk, - 'parent_slug': parent.slug, - 'parent_pk': parent.pk, + 'slug': assembly.slug, + 'pk': assembly.pk, 'user': self.request.user.username, }, ) - else: + elif parent_id is None: + if AssemblyRequests.objects.filter( + source_assembly=assembly, request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK, state=AssemblyRequests.RequestsState.REQUESTED + ): + request = AssemblyRequests.objects.filter( + source_assembly=assembly, + request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK, + state=AssemblyRequests.RequestsState.REQUESTED, + ).delete() logger.info( 'Unassigned assembly "%(slug)s" (%(pk)s) upon request by <%(user)s>.', { @@ -300,9 +338,6 @@ class EditAssemblyView(AssemblyMixin, UpdateView): 'user': self.request.user.username, }, ) - changes['parent'] = (assembly.parent.slug if assembly.parent_id is not None else None, parent.slug if parent is not None else None) - changes['parent_id'] = (assembly.parent_id, parent_id) - assembly.parent = parent elif 'parent_id' in changes: # no change happened, delete that part of the log entry (artifact of combobox and Django's Form) @@ -382,8 +417,8 @@ class AssemblyEditChildrenView(AssemblyMixin, View): def get_object(self, *args, **kwargs): assembly = self.assembly - # say 'Not Found' if assembly is not a cluster or clusters aren't supported at all - if not assembly.is_cluster or not self.conference.support_clusters: + # say 'Not Found' clusters aren't supported at all + if not self.conference.support_clusters: raise Http404 # bail out if the current user is not associated as a contact @@ -398,26 +433,69 @@ class AssemblyEditChildrenView(AssemblyMixin, View): changed = False add_id = request.POST.get('add', None) + approve_id = request.POST.get('approve', None) + reject_id = request.POST.get('reject', None) remove_id = request.POST.get('delete', None) + redirect_to = request.POST.get('next') or 'backoffice:assembly-editchildren' + url_is_safe = url_has_allowed_host_and_scheme( + url=redirect_to, + allowed_hosts=[self.request.get_host()], + require_https=self.request.is_secure(), + ) + if not url_is_safe: + redirect_to = 'backoffice:assembly-editchildren' + + if approve_id is not None: + assembly_request = AssemblyRequests.objects.filter(source_assembly=approve_id, destination_assembly=assembly) + assembly_request.update( + state=AssemblyRequests.RequestsState.ACCEPTED, + ) + if assembly.is_cluster: + child_id = approve_id + parent = assembly + else: + child_id = assembly.pk + parent = get_object_or_404(Assembly, conference=self.conference, pk=approve_id) + child = get_object_or_404(Assembly, conference=self.conference, pk=child_id) + child.parent = parent + child.save() + + if reject_id is not None: + assembly_request = AssemblyRequests.objects.filter(source_assembly=reject_id, destination_assembly=assembly) + assembly_request.update( + state=AssemblyRequests.RequestsState.REJECTED, + ) + if assembly.is_cluster: + child_id = reject_id + else: + child_id = assembly.pk + child = get_object_or_404(Assembly, conference=self.conference, pk=child_id) + child.parent = None + child.save() + if add_id is not None: child = get_object_or_404(Assembly, conference=self.conference, pk=add_id, hierarchy=Assembly.Hierarchy.REGULAR) if child.parent != assembly: - if child.parent is None: - child.parent = assembly - child.save() - changed = True - messages.success(request, gettext('assemblyedit_addedchild').format(child_name=child.name)) - logger.info( - 'Assembly "%(assembly_name)s" (%(assembly_pk)s): added child "%(child)s" (%(child_pk)s), requested by {%(user)s', - {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username}, - ) - else: - messages.error(request, gettext('assemblyedit_not_adding_foreign_child').format(child_name=child.name)) - logger.info( - 'Assembly "%(assembly_name)s" (%(assembly_pk)s): could not steal child "%(child)s" (%(child_pk)s), requested by {%(user)s', - {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username}, - ) + assembly_request = AssemblyRequests( + source_assembly=assembly, + destination_assembly=child, + request=AssemblyRequests.RequestsRequest.ASSEMBLY_LINK, + state=AssemblyRequests.RequestsState.REQUESTED, + ) + assembly_request.save() + + messages.success(request, gettext('assemblyedit_addedchild').format(child_name=child.name)) + logger.info( + 'Assembly "%(assembly_name)s" (%(assembly_pk)s): added child "%(child)s" (%(child_pk)s), requested by {%(user)s', + {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username}, + ) + else: + messages.error(request, gettext('assemblyedit_not_adding_foreign_child').format(child_name=child.name)) + logger.info( + 'Assembly "%(assembly_name)s" (%(assembly_pk)s): could not steal child "%(child)s" (%(child_pk)s), requested by {%(user)s', + {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'child': child, 'child_pk': child.pk, 'user': request.user.username}, + ) if remove_id is not None: child = get_object_or_404(Assembly, conference=self.conference, pk=remove_id, hierarchy=Assembly.Hierarchy.REGULAR) @@ -444,7 +522,7 @@ class AssemblyEditChildrenView(AssemblyMixin, View): ConferenceExportCache.signal_assembly_modification(conference=self.conference, assembly=None) # we're done saving, redirect to children list view again - return redirect('backoffice:assembly-editchildren', pk=assembly.pk) + return redirect(redirect_to, pk=assembly.pk) def get(self, *args, **kwargs): candidates_qs = ( @@ -477,6 +555,7 @@ class AssemblyEditLinksView(AssemblyMixin, View): assembly = self.get_object() add_id = request.POST.get('add', None) + approve_id = request.POST.get('approve', None) remove_id = request.POST.get('delete', None) if add_id is not None: @@ -500,6 +579,37 @@ class AssemblyEditLinksView(AssemblyMixin, View): {'assembly_name': assembly.name, 'assembly_pk': assembly.pk, 'linkee': linkee, 'linkee_pk': linkee.pk, 'user': request.user.username}, ) + if approve_id is not None: + logger.info('hallo') + try: + linkee = Assembly.objects.accessible_by_user(user=request.user, conference=self.conference).get(pk=remove_id) + except Assembly.DoesNotExist: + messages.warning('404 -Assembly %s', remove_id) + linkee = None + + if linkee is not None: + count = AssemblyLink.objects.filter(a=assembly, b=linkee, is_public=True, type=AssemblyLink.Type.RELATED).update( + parent_state=Assembly.StateParent.ACCEPTED + ) + logger.info( + 'Assembly "%s" (%s): approved link to "%s" (%s) upon request by <%s>', + assembly.slug, + assembly.pk, + linkee.slug, + linkee.pk, + request.user.username, + ) + if count > 0: + messages.success(request, gettext('assemblyedit_approvedlink').format(linked_name=linkee.name)) + logger.info( + 'Assembly "%s" (%s): approved link to "%s" (%s) upon request by <%s>', + assembly.slug, + assembly.pk, + linkee.slug, + linkee.pk, + request.user.username, + ) + if remove_id is not None: try: linkee = Assembly.objects.accessible_by_user(user=request.user, conference=self.conference).get(pk=remove_id) diff --git a/src/core/admin.py b/src/core/admin.py index 3b6d86532ecc2cfa0854e1020eaa41b6dc2e1d1a..8ea5982a633ccdfdbc0d141fdf365ee990a34817 100644 --- a/src/core/admin.py +++ b/src/core/admin.py @@ -56,6 +56,7 @@ from .models import ( VoucherEntry, WorkadventureSession, WorkadventureTexture, + AssemblyRequests, ) logger = logging.getLogger(__name__) @@ -458,6 +459,10 @@ class AssemblyLogEntryAdmin(admin.ModelAdmin): ] +class AssemblyRequestsAdmin(admin.ModelAdmin): + model = AssemblyRequests + + class MapFloorAdmin(admin.ModelAdmin): list_display = ['index', 'name', 'conference'] list_display_links = ['name'] @@ -1157,6 +1162,7 @@ admin.site.register(ConferenceTag, ConferenceTagAdmin) admin.site.register(ConferenceTrack, ConferenceTrackAdmin) admin.site.register(Assembly, AssemblyAdmin) admin.site.register(AssemblyLogEntry, AssemblyLogEntryAdmin) +admin.site.register(AssemblyRequests, AssemblyRequestsAdmin) admin.site.register(BadgeCategory, BadgeCategoryAdmin) admin.site.register(Badge, BadgeAdmin) admin.site.register(BadgeToken, BadgeTokenAdmin) diff --git a/src/core/migrations/0150_assemblyrequests.py b/src/core/migrations/0150_assemblyrequests.py new file mode 100644 index 0000000000000000000000000000000000000000..dd63209d38d8691586b48741de8b314d463e1176 --- /dev/null +++ b/src/core/migrations/0150_assemblyrequests.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2024-10-19 21:10 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0149_alter_event_additional_data'), + ] + + operations = [ + migrations.CreateModel( + name='AssemblyRequests', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('request', models.CharField(choices=[('assembly link', 'Assembly__requests_request-link')], help_text='Assembly__requests_request__help', max_length=20, verbose_name='Assembly__requests_request')), + ('state', models.CharField(choices=[('requested', 'Assembly__requests_state-requested'), ('accepted', 'Assembly__requests_state-accepted'), ('rejected', 'Assembly__requests_state-rejected')], default='requested', help_text='Assembly__requests_state__help', max_length=20, verbose_name='Assembly__requests_state')), + ('destination_assembly', models.ForeignKey(blank=True, help_text='Assembly__requests__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.assembly', verbose_name='Assembly__destination')), + ('source_assembly', models.ForeignKey(blank=True, help_text='Assembly__requests__help', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='core.assembly', verbose_name='Assembly__source')), + ], + ), + ] diff --git a/src/core/models/__init__.py b/src/core/models/__init__.py index 8ab46fecbf381c6c75334de584555bda9c585f30..e686acaa07c95502cc7423c2aa99866d66f164bb 100644 --- a/src/core/models/__init__.py +++ b/src/core/models/__init__.py @@ -1,4 +1,4 @@ -from .assemblies import Assembly, AssemblyLink, AssemblyLogEntry, AssemblyMember +from .assemblies import Assembly, AssemblyLink, AssemblyLogEntry, AssemblyMember, AssemblyRequests from .badges import Badge, BadgeCategory, BadgeToken, BadgeTokenTimeConstraint, UserBadge from .board import BulletinBoardEntry from .conference import Conference, ConferenceExportCache, ConferenceMember, ConferenceNavigationItem, ConferenceTrack @@ -29,6 +29,7 @@ __all__ = [ 'AssemblyLink', 'AssemblyLogEntry', 'AssemblyMember', + 'AssemblyRequests', 'BackendMixin', 'Badge', 'BadgeCategory', diff --git a/src/core/models/assemblies.py b/src/core/models/assemblies.py index 7e8139efa490eb36edf4637b6892b26b421b71d9..fb6f582a4aaa3e6c703e97f0320745a5cd287538 100644 --- a/src/core/models/assemblies.py +++ b/src/core/models/assemblies.py @@ -409,6 +409,10 @@ class Assembly(TaggedItemMixin, models.Model): def public_children(self): return Assembly.objects.filter(parent=self).filter(state_assembly__in=Assembly.PUBLIC_STATES).order_by('name') + @property + def requests(self): + return AssemblyRequests.objects.filter(Q(source_assembly=self) | Q(destination_assembly=self)) + @property def linked_assemblies(self): qs = self.assembly_links.filter(is_public=True, type__in=AssemblyLink.PUBLIC_TYPES) @@ -756,6 +760,45 @@ class AssemblyMember(models.Model): return f'{self.member} ({self.get_roles_display()})' +class AssemblyRequests(models.Model): + class RequestsRequest(models.TextChoices): + ASSEMBLY_LINK = 'assembly link', _('Assembly__requests_request-link') + + class RequestsState(models.TextChoices): + REQUESTED = 'requested', _('Assembly__requests_state-requested') + ACCEPTED = 'accepted', _('Assembly__requests_state-accepted') + REJECTED = 'rejected', _('Assembly__requests_state-rejected') + + source_assembly = models.ForeignKey( + Assembly, blank=True, null=True, related_name='+', on_delete=models.PROTECT, verbose_name=_('Assembly__source'), help_text=_('Assembly__requests__help') + ) + destination_assembly = models.ForeignKey( + Assembly, + blank=True, + null=True, + related_name='+', + on_delete=models.PROTECT, + verbose_name=_('Assembly__destination'), + help_text=_('Assembly__requests__help'), + ) + request = models.CharField( + max_length=20, + choices=RequestsRequest.choices, + verbose_name=_('Assembly__requests_request'), + help_text=_('Assembly__requests_request__help'), + ) + state = models.CharField( + max_length=20, + default=RequestsState.REQUESTED, + choices=RequestsState.choices, + verbose_name=_('Assembly__requests_state'), + help_text=_('Assembly__requests_state__help'), + ) + + def __str__(self): + return f'{self.source_assembly} -- {self.state} --> {self.destination_assembly}' + + class AssemblyLogEntry(models.Model): class Meta: verbose_name = _('AssemblyLogEntry')