SciPost Code Repository

Skip to content
Snippets Groups Projects
Commit 2f9f3f9f authored by Jorran de Wit's avatar Jorran de Wit
Browse files

Production renovation

parent aa7ccc08
No related branches found
No related tags found
No related merge requests found
Showing
with 264 additions and 75 deletions
...@@ -4,7 +4,7 @@ from .models import ProductionStream, ProductionEvent, ProductionUser ...@@ -4,7 +4,7 @@ from .models import ProductionStream, ProductionEvent, ProductionUser
def event_count(obj): def event_count(obj):
return obj.productionevent_set.count() return obj.events.count()
class ProductionUserInline(admin.StackedInline): class ProductionUserInline(admin.StackedInline):
......
...@@ -8,7 +8,6 @@ class ProductionConfig(AppConfig): ...@@ -8,7 +8,6 @@ class ProductionConfig(AppConfig):
def ready(self): def ready(self):
super().ready() super().ready()
from .models import ProductionStream, ProductionEvent from .models import ProductionEvent
from .signals import notify_new_stream, notify_new_event from .signals import notify_new_event
post_save.connect(notify_new_stream, sender=ProductionStream)
post_save.connect(notify_new_event, sender=ProductionEvent) post_save.connect(notify_new_event, sender=ProductionEvent)
...@@ -34,3 +34,11 @@ class AssignOfficerForm(forms.ModelForm): ...@@ -34,3 +34,11 @@ class AssignOfficerForm(forms.ModelForm):
officer = self.cleaned_data['officer'] officer = self.cleaned_data['officer']
self.instance.officers.add(officer) self.instance.officers.add(officer)
return self.instance return self.instance
class UserToOfficerForm(forms.ModelForm):
class Meta:
model = ProductionUser
fields = (
'user',
)
...@@ -15,5 +15,5 @@ class ProductionStreamQuerySet(models.QuerySet): ...@@ -15,5 +15,5 @@ class ProductionStreamQuerySet(models.QuerySet):
class ProductionEventManager(models.Manager): class ProductionEventManager(models.Manager):
def get_my_events(self, current_contributor): def get_my_events(self, production_user):
return self.filter(noted_by=current_contributor) return self.filter(noted_by=production_user)
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-14 20:20
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('production', '0012_productionstream_officers'),
]
operations = [
migrations.RenameField(
model_name='productionevent',
old_name='noted_by',
new_name='noted_by_contributor',
),
migrations.AlterField(
model_name='productionstream',
name='officers',
field=models.ManyToManyField(blank=True, related_name='streams', to='production.ProductionUser'),
),
migrations.AlterField(
model_name='productionuser',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.PROTECT, related_name='production_user', to=settings.AUTH_USER_MODEL),
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-14 20:20
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('production', '0013_auto_20170914_2220'),
]
operations = [
migrations.AddField(
model_name='productionevent',
name='noted_by',
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, to='production.ProductionUser'),
preserve_default=False,
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-14 20:37
from __future__ import unicode_literals
from django.contrib.auth.models import Group, User
from django.db import migrations
def contributor_to_officer(apps, schema_editor):
# Create ProductionUser for all current Officers
ProductionUser = apps.get_model('production', 'ProductionUser')
officers = Group.objects.get(name='Production Officers')
for user in officers.user_set.all():
ProductionUser.objects.get_or_create(user__id=user.id)
print('\n - Production Officers transfered to ProductionUser')
# Transfer all Events
ProductionEvent = apps.get_model('production', 'ProductionEvent')
for event in ProductionEvent.objects.all():
user = User.objects.get(contributor__id=event.noted_by_contributor.id)
event.noted_by.id = user.production_user.id
event.save()
print(' - ProductionEvents updated')
return
def officer_to_contributor(apps, schema_editor):
# Transfer all Events
ProductionEvent = apps.get_model('production', 'ProductionEvent')
for event in ProductionEvent.objects.all():
user = User.objects.get(production_user__id=event.noted_by.id)
event.noted_by_contributor.id = user.contributor.id
event.save()
print('\n - ProductionEvents updated')
return
class Migration(migrations.Migration):
dependencies = [
('production', '0014_productionevent_noted_by'),
]
operations = [
migrations.RunPython(contributor_to_officer, officer_to_contributor)
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-09-14 20:46
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('production', '0015_auto_20170914_2237'),
]
operations = [
migrations.RemoveField(
model_name='productionevent',
name='noted_by_contributor',
),
]
...@@ -42,16 +42,16 @@ class ProductionStream(models.Model): ...@@ -42,16 +42,16 @@ class ProductionStream(models.Model):
return reverse('production:completed') + '#stream_' + str(self.id) return reverse('production:completed') + '#stream_' + str(self.id)
def total_duration(self): def total_duration(self):
totdur = self.productionevent_set.aggregate(models.Sum('duration')) totdur = self.events.aggregate(models.Sum('duration'))
return totdur['duration__sum'] return totdur['duration__sum']
class ProductionEvent(models.Model): class ProductionEvent(models.Model):
stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE) stream = models.ForeignKey(ProductionStream, on_delete=models.CASCADE, related_name='events')
event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS) event = models.CharField(max_length=64, choices=PRODUCTION_EVENTS)
comments = models.TextField(blank=True, null=True) comments = models.TextField(blank=True, null=True)
noted_on = models.DateTimeField(default=timezone.now) noted_on = models.DateTimeField(default=timezone.now)
noted_by = models.ForeignKey('scipost.Contributor', on_delete=models.CASCADE) noted_by = models.ForeignKey('production.ProductionUser', on_delete=models.CASCADE)
duration = models.DurationField(blank=True, null=True) duration = models.DurationField(blank=True, null=True)
objects = ProductionEventManager() objects = ProductionEventManager()
......
from django.contrib.auth.decorators import user_passes_test
def is_production_user():
"""Requires user to be a ProductionUser."""
def test(u):
if u.is_authenticated():
if hasattr(u, 'production_user') and u.production_user:
return True
return False
return user_passes_test(test)
from django.contrib.auth.models import Group, User
from notifications.signals import notify from notifications.signals import notify
def notify_new_stream(sender, instance, created, **kwargs): def notify_new_stream(sender, instance, recipient, **kwargs):
""" """
Notify the production team about a new Production Stream created. Notify the production team about a new Production Stream created.
""" """
if created: notify.send(sender=sender, recipient=recipient, actor=sender,
production_officers = User.objects.filter(groups__name='Production Officers') verb=' assigned you to a Production Stream.', target=instance)
editorial_college = Group.objects.get(name='Editorial College')
for user in production_officers:
notify.send(sender=sender, recipient=user, actor=editorial_college, verb=' accepted a submission. A new productionstream has been started.', target=instance)
def notify_new_event(sender, instance, created, **kwargs): def notify_new_event(sender, instance, created, **kwargs):
...@@ -19,15 +14,15 @@ def notify_new_event(sender, instance, created, **kwargs): ...@@ -19,15 +14,15 @@ def notify_new_event(sender, instance, created, **kwargs):
Notify the production team about a new Production Event created. Notify the production team about a new Production Event created.
""" """
if created: if created:
production_officers = User.objects.filter(groups__name='Production Officers') for officer in instance.stream.officers.all():
for user in production_officers: notify.send(sender=sender, recipient=officer.user, actor=instance.noted_by.user,
notify.send(sender=sender, recipient=user, actor=instance.noted_by.user, verb=' created a new Production Event ', target=instance) verb=' created a new Production Event.', target=instance)
def notify_stream_completed(sender, instance, **kwargs): def notify_stream_completed(sender, instance, **kwargs):
""" """
Notify the production team about a Production Stream being completed. Notify the production team about a Production Stream being completed.
""" """
production_officers = User.objects.filter(groups__name='Production Officers') for officer in instance.officers.all():
for user in production_officers: notify.send(sender=sender, recipient=officer.user, actor=sender,
notify.send(sender=sender, recipient=user, actor=sender, verb=' marked Production Stream as completed.', target=instance) verb=' marked Production Stream as completed.', target=instance)
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
<div class="w-100" id="stream_{{stream.id}}">{% include 'submissions/_submission_card_content_sparse.html' with submission=stream.submission %}</div> <div class="w-100" id="stream_{{stream.id}}">{% include 'submissions/_submission_card_content_sparse.html' with submission=stream.submission %}</div>
<div class="card-body"> <div class="card-body">
<h3>Events</h3> <h3>Events</h3>
{% include 'production/partials/production_events.html' with events=stream.productionevent_set.all %} {% include 'production/partials/production_events.html' with events=stream.events.all %}
</div> </div>
</li> </li>
{% empty %} {% empty %}
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
{% for event in events %} {% for event in events %}
<li id="event_{{ event.id }}"> <li id="event_{{ event.id }}">
<p class="mb-0 font-weight-bold">{{ event.get_event_display }} <p class="mb-0 font-weight-bold">{{ event.get_event_display }}
{% if event.noted_by == request.user.contributor %} {% if event.noted_by == request.user.production_user %}
&middot; <a href="{% url 'production:update_event' event.id %}">Edit</a> &middot; <a href="{% url 'production:update_event' event.id %}">Edit</a>
&middot; <a class="text-danger" href="{% url 'production:delete_event' event.id %}">Delete</a> &middot; <a class="text-danger" href="{% url 'production:delete_event' event.id %}">Delete</a>
{% endif %} {% endif %}
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
{% endfor %} {% endfor %}
</ul> </ul>
<h3>Events</h3> <h3>Events</h3>
{% include 'production/partials/production_events.html' with events=stream.productionevent_set.all %} {% include 'production/partials/production_events.html' with events=stream.events.all %}
<br/> <br/>
......
...@@ -34,6 +34,11 @@ ...@@ -34,6 +34,11 @@
<a href="#teamtimesheets" class="nav-link" data-toggle="tab">Team Timesheets</a> <a href="#teamtimesheets" class="nav-link" data-toggle="tab">Team Timesheets</a>
</li> </li>
{% endif %} {% endif %}
{% if perms.scipost.can_promote_user_to_production_officer %}
<li class="nav-item btn btn-secondary">
<a href="#officers" class="nav-link" data-toggle="tab">Production Officers</a>
</li>
{% endif %}
</ul> </ul>
</div> </div>
</div> </div>
...@@ -66,8 +71,8 @@ ...@@ -66,8 +71,8 @@
<td>{{ stream.submission.author_list }}</td> <td>{{ stream.submission.author_list }}</td>
<td>{{ stream.submission.title }}</td> <td>{{ stream.submission.title }}</td>
<td>{{ stream.submission.acceptance_date|date:"Y-m-d" }}</td> <td>{{ stream.submission.acceptance_date|date:"Y-m-d" }}</td>
<td>{{ stream.productionevent_set.last.get_event_display }}</td> <td>{{ stream.events.last.get_event_display }}</td>
<td>{{ stream.productionevent_set.last.noted_on }}</td> <td>{{ stream.events.last.noted_on }}</td>
</tr> </tr>
<tr id="collapse{{ stream.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ stream.id }}" style="background-color: #fff;"> <tr id="collapse{{ stream.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ stream.id }}" style="background-color: #fff;">
<td colspan="5"> <td colspan="5">
...@@ -119,39 +124,60 @@ ...@@ -119,39 +124,60 @@
</div> </div>
{% if perms.scipost.can_view_timesheets %} {% if perms.scipost.can_view_timesheets %}
<div class="tab-pane" id="teamtimesheets" role="tabpanel"> <div class="tab-pane" id="teamtimesheets" role="tabpanel">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h2 class="highlight">Team Timesheets</h2> <h2 class="highlight">Team Timesheets</h2>
</div> </div>
</div> </div>
<table class="table table-hover mb-5"> <table class="table table-hover mb-5">
<thead class="thead-default"> <thead class="thead-default">
<tr> <tr>
<th>Name</th> <th>Name</th>
</tr> </tr>
</thead> </thead>
<tbody id="accordion" role="tablist" aria-multiselectable="true">
{% for member in production_team.all %}
<tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ member.id }}" aria-expanded="true" aria-controls="collapse{{ member.id }}" style="cursor: pointer;">
<td>{{ member }}</td>
</tr>
<tr id="collapse{{ member.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ member.id }}" style="background-color: #fff;">
<td>
{% include 'production/partials/production_timesheet_card.html' with events=member.productionevent_set.all %}
</td>
</tr>
{% empty %}
<tr>
<td>No Team Member found.</td>
</tr>
{% endfor %}
</tbody>
</table>
<tbody id="accordion" role="tablist" aria-multiselectable="true"> </div>
{% for member in production_team.all %} {% endif %}
<tr data-toggle="collapse" data-parent="#accordion" href="#collapse{{ member.id }}" aria-expanded="true" aria-controls="collapse{{ member.id }}" style="cursor: pointer;">
<td>{{ member }}</td>
</tr>
<tr id="collapse{{ member.id }}" class="collapse" role="tabpanel" aria-labelledby="heading{{ member.id }}" style="background-color: #fff;">
<td>
{% include 'production/partials/production_timesheet_card.html' with events=member.productionevent_set.all %}
</td>
</tr>
{% empty %}
<tr>
<td>No Team Member found.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div> {% if perms.scipost.can_promote_user_to_production_officer %}
<div class="tab-pane" id="officers" role="tabpanel">
<h2 class="highlight">Production Officers</h2>
<h3>Current Production Officer</h3>
<ul>
{% for officer in production_officers %}
<li>{{ officer }}</li>
{% endfor %}
</ul>
{% if new_officer_form %}
<h3>Promote user to Production Officer</h3>
<form action="{% url 'production:user_to_officer' %}" method="post">
{% csrf_token %}
{{ new_officer_form|bootstrap }}
<input type="submit" class="btn btn-primary" value="Promote to Production Officer">
</form>
{% endif %}
</div>
{% endif %} {% endif %}
</div> </div>
......
...@@ -5,6 +5,7 @@ from production import views as production_views ...@@ -5,6 +5,7 @@ from production import views as production_views
urlpatterns = [ urlpatterns = [
url(r'^$', production_views.production, name='production'), url(r'^$', production_views.production, name='production'),
url(r'^completed$', production_views.completed, name='completed'), url(r'^completed$', production_views.completed, name='completed'),
url(r'^officers/new$', production_views.user_to_officer, name='user_to_officer'),
url(r'^streams/(?P<stream_id>[0-9]+)/events/add$', url(r'^streams/(?P<stream_id>[0-9]+)/events/add$',
production_views.add_event, name='add_event'), production_views.add_event, name='add_event'),
url(r'^streams/(?P<stream_id>[0-9]+)/officer/add$', url(r'^streams/(?P<stream_id>[0-9]+)/officer/add$',
......
import datetime import datetime
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.models import Group
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.utils import timezone from django.utils import timezone
...@@ -11,30 +12,32 @@ from guardian.decorators import permission_required ...@@ -11,30 +12,32 @@ from guardian.decorators import permission_required
from .constants import PRODUCTION_STREAM_COMPLETED from .constants import PRODUCTION_STREAM_COMPLETED
from .models import ProductionUser, ProductionStream, ProductionEvent from .models import ProductionUser, ProductionStream, ProductionEvent
from .forms import ProductionEventForm, AssignOfficerForm from .forms import ProductionEventForm, AssignOfficerForm, UserToOfficerForm
from .signals import notify_stream_completed from .permissions import is_production_user
from .signals import notify_stream_completed, notify_new_stream
from scipost.models import Contributor
###################### ######################
# Production process # # Production process #
###################### ######################
@is_production_user()
@permission_required('scipost.can_view_production', return_403=True) @permission_required('scipost.can_view_production', return_403=True)
def production(request): def production(request):
""" """
Overview page for the production process. Overview page for the production process.
All papers with accepted but not yet published status are included here. All papers with accepted but not yet published status are included here.
""" """
if request.user.has_perm('scipost.can_assign_production_officer'): streams = ProductionStream.objects.ongoing()
streams = ProductionStream.objects.ongoing().order_by('opened') if not request.user.has_perm('scipost.can_assign_production_officer'):
else: # Restrict stream queryset if user is not supervisor
streams = ProductionStream.objects.ongoing().filter_for_user(request.user.production_user).order_by('opened') streams = streams.filter_for_user(request.user.production_user)
streams = streams.order_by('opened')
prodevent_form = ProductionEventForm() prodevent_form = ProductionEventForm()
assignment_form = AssignOfficerForm() assignment_form = AssignOfficerForm()
ownevents = ProductionEvent.objects.filter( ownevents = ProductionEvent.objects.filter(
noted_by=request.user.contributor, noted_by=request.user.production_user,
duration__gte=datetime.timedelta(minutes=1)).order_by('-noted_on') duration__gte=datetime.timedelta(minutes=1)).order_by('-noted_on')
context = { context = {
'streams': streams, 'streams': streams,
...@@ -43,11 +46,15 @@ def production(request): ...@@ -43,11 +46,15 @@ def production(request):
'ownevents': ownevents, 'ownevents': ownevents,
} }
if request.user.has_perm('scipost.can_view_timesheets'): if request.user.has_perm('scipost.can_view_timesheets'):
context['production_team'] = Contributor.objects.filter( context['production_team'] = ProductionUser.objects.all()
user__groups__name='Production Officers').order_by('user__last_name')
if request.user.has_perm('scipost.can_promote_user_to_production_officer'):
context['production_officers'] = ProductionUser.objects.all()
context['new_officer_form'] = UserToOfficerForm()
return render(request, 'production/production.html', context) return render(request, 'production/production.html', context)
@is_production_user()
@permission_required('scipost.can_view_production', return_403=True) @permission_required('scipost.can_view_production', return_403=True)
def completed(request): def completed(request):
""" """
...@@ -58,6 +65,21 @@ def completed(request): ...@@ -58,6 +65,21 @@ def completed(request):
return render(request, 'production/completed.html', context) return render(request, 'production/completed.html', context)
@is_production_user()
@permission_required('scipost.can_promote_user_to_production_officer')
def user_to_officer(request):
form = UserToOfficerForm(request.POST or None)
if form.is_valid():
officer = form.save()
# Add permission group
group = Group.objects.get(name='Production Officers')
officer.user.groups.add(group)
messages.success(request, '{user} promoted to Production Officer'.format(user=officer))
return redirect(reverse('production:production'))
@is_production_user()
@permission_required('scipost.can_view_production', return_403=True) @permission_required('scipost.can_view_production', return_403=True)
def add_event(request, stream_id): def add_event(request, stream_id):
stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
...@@ -65,27 +87,30 @@ def add_event(request, stream_id): ...@@ -65,27 +87,30 @@ def add_event(request, stream_id):
if prodevent_form.is_valid(): if prodevent_form.is_valid():
prodevent = prodevent_form.save(commit=False) prodevent = prodevent_form.save(commit=False)
prodevent.stream = stream prodevent.stream = stream
prodevent.noted_by = request.user.contributor prodevent.noted_by = request.user.production_user
prodevent.save() prodevent.save()
else: else:
messages.warning(request, 'The form was invalidly filled.') messages.warning(request, 'The form was invalidly filled.')
return redirect(reverse('production:production')) return redirect(reverse('production:production'))
@is_production_user()
@permission_required('scipost.can_assign_production_officer', return_403=True) @permission_required('scipost.can_assign_production_officer', return_403=True)
def add_officer(request, stream_id): def add_officer(request, stream_id):
stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
form = AssignOfficerForm(request.POST or None, instance=stream) form = AssignOfficerForm(request.POST or None, instance=stream)
if form.is_valid(): if form.is_valid():
form.save() form.save()
messages.success(request, 'Officer {officer} has been assigned.'.format( officer = form.cleaned_data.get('officer')
officer=form.cleaned_data.get('officer'))) messages.success(request, 'Officer {officer} has been assigned.'.format(officer=officer))
notify_new_stream(request.user, stream, officer.user)
else: else:
for key, error in form.errors.items(): for key, error in form.errors.items():
messages.warning(request, error[0]) messages.warning(request, error[0])
return redirect(reverse('production:production')) return redirect(reverse('production:production'))
@is_production_user()
@permission_required('scipost.can_assign_production_officer', return_403=True) @permission_required('scipost.can_assign_production_officer', return_403=True)
def remove_officer(request, stream_id, officer_id): def remove_officer(request, stream_id, officer_id):
stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
...@@ -99,6 +124,7 @@ def remove_officer(request, stream_id, officer_id): ...@@ -99,6 +124,7 @@ def remove_officer(request, stream_id, officer_id):
return redirect(reverse('production:production')) return redirect(reverse('production:production'))
@method_decorator(is_production_user(), name='dispatch')
@method_decorator(permission_required('scipost.can_view_production', raise_exception=True), @method_decorator(permission_required('scipost.can_view_production', raise_exception=True),
name='dispatch') name='dispatch')
class UpdateEventView(UpdateView): class UpdateEventView(UpdateView):
...@@ -108,13 +134,14 @@ class UpdateEventView(UpdateView): ...@@ -108,13 +134,14 @@ class UpdateEventView(UpdateView):
slug_url_kwarg = 'event_id' slug_url_kwarg = 'event_id'
def get_queryset(self): def get_queryset(self):
return self.model.objects.get_my_events(self.request.user.contributor) return self.model.objects.get_my_events(self.request.user.production_user)
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, 'Event updated succesfully') messages.success(self.request, 'Event updated succesfully')
return super().form_valid(form) return super().form_valid(form)
@method_decorator(is_production_user(), name='dispatch')
@method_decorator(permission_required('scipost.can_view_production', raise_exception=True), @method_decorator(permission_required('scipost.can_view_production', raise_exception=True),
name='dispatch') name='dispatch')
class DeleteEventView(DeleteView): class DeleteEventView(DeleteView):
...@@ -123,7 +150,7 @@ class DeleteEventView(DeleteView): ...@@ -123,7 +150,7 @@ class DeleteEventView(DeleteView):
slug_url_kwarg = 'event_id' slug_url_kwarg = 'event_id'
def get_queryset(self): def get_queryset(self):
return self.model.objects.get_my_events(self.request.user.contributor) return self.model.objects.get_my_events(self.request.user.production_user)
def form_valid(self, form): def form_valid(self, form):
messages.success(self.request, 'Event deleted succesfully') messages.success(self.request, 'Event deleted succesfully')
...@@ -133,6 +160,7 @@ class DeleteEventView(DeleteView): ...@@ -133,6 +160,7 @@ class DeleteEventView(DeleteView):
return self.object.get_absolute_url() return self.object.get_absolute_url()
@is_production_user()
@permission_required('scipost.can_publish_accepted_submission', return_403=True) @permission_required('scipost.can_publish_accepted_submission', return_403=True)
def mark_as_completed(request, stream_id): def mark_as_completed(request, stream_id):
stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id) stream = get_object_or_404(ProductionStream.objects.ongoing(), pk=stream_id)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment