diff --git a/finances/forms.py b/finances/forms.py index dd99b82bb5aa501a3b23097fbcace9a0a56e4350..36f2907125b04a5328016a652614a75f37e919b9 100644 --- a/finances/forms.py +++ b/finances/forms.py @@ -1,16 +1,69 @@ +import datetime + from django import forms +from django.contrib.auth import get_user_model +from django.utils.dates import MONTHS +from django.db.models import Sum from .models import WorkLog +today = datetime.datetime.today() + class WorkLogForm(forms.ModelForm): + def __init__(self, *args, **kwargs): + self.types = kwargs.pop('log_types', False) + super().__init__(*args, **kwargs) + if self.types: + self.fields['log_type'] = forms.ChoiceField(choices=self.types) + class Meta: model = WorkLog fields = ( 'comments', + 'log_type', 'duration', ) widgets = { 'comments': forms.Textarea(attrs={'rows': 4}), 'duration': forms.TextInput(attrs={'placeholder': 'HH:MM:SS'}) } + + +class LogsMonthlyActiveFilter(forms.Form): + month = forms.ChoiceField(choices=[(k, v) for k, v in MONTHS.items()]) + year = forms.ChoiceField(choices=[(y, y) for y in reversed(range(today.year-6, today.year+1))]) + + def __init__(self, *args, **kwargs): + if not kwargs.get('data', False) and not args[0]: + args = list(args) + args[0] = { + 'month': today.month, + 'year': today.year + } + args = tuple(args) + kwargs['initial'] = { + 'month': today.month, + 'year': today.year + } + super().__init__(*args, **kwargs) + + def get_totals(self): + # Make accessible without need to explicitly check validity of form. + self.is_valid() + + users = get_user_model().objects.filter( + work_logs__work_date__month=self.cleaned_data['month'], + work_logs__work_date__year=self.cleaned_data['year']).distinct() + output = [] + for user in users: + logs = user.work_logs.filter( + work_date__month=self.cleaned_data['month'], + work_date__year=self.cleaned_data['year']) + output.append({ + 'logs': logs, + 'duration': logs.aggregate(total=Sum('duration')), + 'user': user + }) + + return output diff --git a/finances/migrations/0002_auto_20171007_1349.py b/finances/migrations/0002_auto_20171007_1349.py index abab65056fe0b8b637687d7b06d3444086eed0a9..01d4dcaf94b2c414dfe25f2b5ddc40088e2aecbf 100644 --- a/finances/migrations/0002_auto_20171007_1349.py +++ b/finances/migrations/0002_auto_20171007_1349.py @@ -5,35 +5,6 @@ from __future__ import unicode_literals from django.db import migrations -def move_hour_registrations(apps, schema_editor): - """ - Move all ProductionEvent hours to Finances model. - """ - ProductionEvent = apps.get_model('production', 'ProductionEvent') - WorkLog = apps.get_model('finances', 'WorkLog') - ContentType = apps.get_model('contenttypes', 'ContentType') - stream_content_type = ContentType.objects.get(app_label='production', model='productionstream') - - for event in ProductionEvent.objects.filter(duration__isnull=False): - log = WorkLog( - user=event.noted_by.user, - comments=event.comments, - duration=event.duration, - work_date=event.noted_on, - target_content_type=stream_content_type, - target_object_id=event.stream.id - ) - log.save() - return - - -def move_hour_registrations_inverse(apps, schema_editor): - """ - Move all ProductionEvent hours to Finances model inversed (not implemented). - """ - return - - class Migration(migrations.Migration): dependencies = [ @@ -41,6 +12,4 @@ class Migration(migrations.Migration): ('production', '0028_auto_20171007_1311'), ] - operations = [ - migrations.RunPython(move_hour_registrations, move_hour_registrations_inverse) - ] + operations = [] diff --git a/finances/migrations/0005_auto_20171010_0921.py b/finances/migrations/0005_auto_20171010_0921.py new file mode 100644 index 0000000000000000000000000000000000000000..c3ffa146363d51e1864ceca81cb91530799c0d09 --- /dev/null +++ b/finances/migrations/0005_auto_20171010_0921.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-10 07:21 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('finances', '0004_auto_20171007_1426'), + ] + + operations = [ + migrations.AlterModelOptions( + name='worklog', + options={'ordering': ['-work_date', 'created']}, + ), + migrations.AddField( + model_name='worklog', + name='log_type', + field=models.CharField(blank=True, max_length=128), + ), + ] diff --git a/finances/migrations/0006_auto_20171010_1003.py b/finances/migrations/0006_auto_20171010_1003.py new file mode 100644 index 0000000000000000000000000000000000000000..2789ccec488ef8f80370306548c28cdda16eaf2c --- /dev/null +++ b/finances/migrations/0006_auto_20171010_1003.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-10 08:03 +from __future__ import unicode_literals + +from django.db import migrations + + +def move_hour_registrations(apps, schema_editor): + """ + Move all ProductionEvent hours to Finances model. + """ + ProductionEvent = apps.get_model('production', 'ProductionEvent') + WorkLog = apps.get_model('finances', 'WorkLog') + ContentType = apps.get_model('contenttypes', 'ContentType') + stream_content_type = ContentType.objects.get(app_label='production', model='productionstream') + + for event in ProductionEvent.objects.filter(duration__isnull=False): + if event.event in ['assigned_to_supervisor', + 'message_edadmin_to_supervisor', + 'message_supervisor_to_edadmin', + 'officer_tasked_with_proof_production', + 'message_supervisor_to_officer', + 'proofs_checked_by_supervisor', + 'proofs_returned_by_authors', + 'proofs_returned_by_authors', + 'authors_have_accepted_proofs', + 'paper_published']: + _type = 'Production: Supervisory tasks' + else: + _type = 'Production: Production Officer tasks' + + log = WorkLog( + user=event.noted_by.user, + log_type=_type, + comments=event.comments, + duration=event.duration, + work_date=event.noted_on, + content_type=stream_content_type, + object_id=event.stream.id + ) + log.save() + return + + +def move_hour_registrations_inverse(apps, schema_editor): + """ + Move all ProductionEvent hours to Finances model inversed (not implemented). + """ + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('finances', '0005_auto_20171010_0921'), + ] + + operations = [ + migrations.RunPython(move_hour_registrations, move_hour_registrations_inverse) + ] diff --git a/finances/models.py b/finances/models.py index d4c8622fd8b3ac23b346343343ee41f2b3244b0e..3982eb14a3e5b0536d504ba4ef13ffa4103d95a6 100644 --- a/finances/models.py +++ b/finances/models.py @@ -10,6 +10,7 @@ from .utils import id_to_slug class WorkLog(models.Model): user = models.ForeignKey(settings.AUTH_USER_MODEL) comments = models.TextField(blank=True) + log_type = models.CharField(max_length=128, blank=True) duration = models.DurationField(blank=True, null=True) work_date = models.DateField(default=timezone.now) created = models.DateTimeField(auto_now_add=True) @@ -20,6 +21,7 @@ class WorkLog(models.Model): class Meta: default_related_name = 'work_logs' + ordering = ['-work_date', 'created'] def __str__(self): return 'Log of {0} {1} on {2}'.format( diff --git a/finances/templates/finances/timesheets.html b/finances/templates/finances/timesheets.html index 63d43519a766bf7535bedf40f08346554eb7c837..f32ae41dfa46ac45e19d96ffca47e1e1e01e030f 100644 --- a/finances/templates/finances/timesheets.html +++ b/finances/templates/finances/timesheets.html @@ -7,6 +7,7 @@ {% block pagetitle %}: Team Timesheets{% endblock pagetitle %} {% load bootstrap %} +{% load scipost_extras %} {% block content %} @@ -25,26 +26,25 @@ <div class="col-12"> <h2 class="mb-2 mt-4">Team Timesheets</h2> {% for total in totals %} - <h3 class="mb-1">{{ total.user }}</h3> + <h3 class="mb-1">{{ total.user.first_name }} {{ total.user.last_name }}</h3> <table class="table"> <thead class="thead-default"> <tr> <th>Date</th> - <th>By</th> - <th>Stream</th> - <th>Event</th> + <th>Related to object</th> + <th>Log type</th> + <th>Comments</th> <th>Duration</th> </tr> </thead> - <tbody> - {% for event in total.events %} + {% for log in total.logs %} <tr> - <td>{{ event.noted_on }}</td> - <td>{{ event.noted_by }}</td> - <td>{{ event.stream }}</td> - <td>{{ event.get_event_display }}</td> - <td>{{ event.duration }}</td> + <td>{{ log.work_date }}</td> + <td>{{ log.content }}</td> + <td>{{ log.log_type }}</td> + <td>{{ log.comments }}</td> + <td>{{ log.duration|duration }}</td> </tr> {% endfor %} <tr> diff --git a/finances/templates/partials/finances/logs.html b/finances/templates/partials/finances/logs.html index 7b9db48220dfd2ed74781910031b994031df1ce8..956a07bf576138d2f52963462326ff17212f3c56 100644 --- a/finances/templates/partials/finances/logs.html +++ b/finances/templates/partials/finances/logs.html @@ -7,6 +7,8 @@ <div> <strong>{{ log.user.first_name }} {{ log.user.last_name }}</strong> <br> + <span class="text-muted">{{ log.log_type }}</span> + <br> {{ log.comments }} </div> <div class="text-muted text-right d-flex justify-content-end"> @@ -25,10 +27,3 @@ <li>No logs were found.</li> {% endfor %} </ul> -{% comment %} -user = models.ForeignKey(settings.AUTH_USER_MODEL) -comments = models.TextField(blank=True) -duration = models.DurationField(blank=True, null=True) -work_date = models.DateField(default=timezone.now) -created = models.DateTimeField(auto_now_add=True) -{% endcomment %} diff --git a/finances/views.py b/finances/views.py index f235ef16abcdc2b786b14fb70eb69e69a4566182..33200e7c2eb3a10a9c2b6f905960d0cb497583f9 100644 --- a/finances/views.py +++ b/finances/views.py @@ -4,8 +4,7 @@ from django.http import Http404 from django.shortcuts import render from django.views.generic.edit import DeleteView -from production.forms import ProductionUserMonthlyActiveFilter - +from .forms import LogsMonthlyActiveFilter from .models import WorkLog from .utils import slug_to_id @@ -15,7 +14,7 @@ def timesheets(request): """ See an overview per month of all timesheets. """ - form = ProductionUserMonthlyActiveFilter(request.GET or None) + form = LogsMonthlyActiveFilter(request.GET or None) context = { 'form': form, } diff --git a/mails/forms.py b/mails/forms.py index 2c34d6453d48abdd7803fb433788d84e8040437c..9e8b9fbeb840877dbb04aaf6a869e715396e8cf7 100644 --- a/mails/forms.py +++ b/mails/forms.py @@ -1,10 +1,14 @@ import json +import inspect from django import forms from django.core.mail import EmailMultiAlternatives +from django.contrib.auth import get_user_model from django.conf import settings from django.template import loader +from scipost.models import Contributor + class EmailTemplateForm(forms.Form): subject = forms.CharField(max_length=250, label="Subject*") @@ -27,6 +31,8 @@ class EmailTemplateForm(forms.Form): recipient = self.object for attr in self.mail_data.get('to_address').split('.'): recipient = getattr(recipient, attr) + if inspect.ismethod(recipient): + recipient = recipient() self.recipient = recipient # Set the data as initials @@ -39,15 +45,42 @@ class EmailTemplateForm(forms.Form): html_template = loader.get_template('email/general.html') html_message = html_template.render({'text': message}) - # Get recipients list. Always send through BCC to prevent privacy issues! - bcc_to = self.object - for attr in self.mail_data.get('bcc_to').split('.'): - bcc_to = getattr(bcc_to, attr) - bcc_list = [ - bcc_to, - ] - if self.cleaned_data.get('additional_bcc'): - bcc_list.append(self.cleaned_data.get('additional_bcc')) + # Get recipients list. Try to send through BCC to prevent privacy issues! + bcc_list = [] + if self.mail_data.get('bcc_to'): + bcc_to = self.object + for attr in self.mail_data.get('bcc_to').split('.'): + bcc_to = getattr(bcc_to, attr) + + if not isinstance(bcc_to, list): + bcc_list = [bcc_to] + else: + bcc_list = bcc_to + + if self.cleaned_data.get('extra_recipient'): + bcc_list.append(self.cleaned_data.get('extra_recipient')) + + # Check the send list + if isinstance(self.recipient, list): + recipients = self.recipient + elif not isinstance(self.recipient, str): + try: + recipients = list(self.recipient) + except TypeError: + recipients = [self.recipient] + else: + recipients = [self.recipient] + recipients = list(recipients) + + # Check if email needs to be taken from instance + _recipients = [] + for recipient in recipients: + if isinstance(recipient, Contributor): + _recipients.append(recipient.user.email) + elif isinstance(recipient, get_user_model()): + _recipients.append(recipient.email) + elif isinstance(recipient, str): + _recipients.append(recipient) # Send the mail email = EmailMultiAlternatives( @@ -55,7 +88,7 @@ class EmailTemplateForm(forms.Form): message, '%s <%s>' % (self.mail_data.get('from_address_name', 'SciPost'), self.mail_data.get('from_address', 'no-reply@scipost.org')), # From - [self.recipient], # To + _recipients, # To bcc=bcc_list, reply_to=[self.mail_data.get('from_address', 'no-reply@scipost.org')]) email.attach_alternative(html_message, 'text/html') diff --git a/mails/templates/mail_templates/production_send_proofs.json b/mails/templates/mail_templates/production_send_proofs.json new file mode 100644 index 0000000000000000000000000000000000000000..5e38b3a6e8bb1f858e66805c7cfd09da77e93913 --- /dev/null +++ b/mails/templates/mail_templates/production_send_proofs.json @@ -0,0 +1,7 @@ +{ + "subject": "SciPost: Your proofs have been produced", + "to_address": "stream.submission.authors.all", + "from_address_name": "SciPost Production", + "from_address": "proofs@scipost.org", + "context_object": "proofs" +} diff --git a/mails/templates/mail_templates/production_send_proofs.txt b/mails/templates/mail_templates/production_send_proofs.txt new file mode 100644 index 0000000000000000000000000000000000000000..d6cbda22c323b76397e3a18668aa1211185668cd --- /dev/null +++ b/mails/templates/mail_templates/production_send_proofs.txt @@ -0,0 +1,11 @@ +Dear {% for author in proofs.stream.submission.authors.all %}{{ author.get_title_display }} {{ author.user.last_name }}{% if not forloop.last %}, {% elif proofs.stream.submission.authors.count > 1 %} and {% endif %}{% endfor %}, + +The SciPost production team has finished the proofs of your manuscript (version {{ proofs.version }}). You can find the proofs on your submission's page (see https://scipost.org{{ proofs.stream.submission.get_absolute_url }}). + +Please review the proofs and let us know whether you accept the proofs for publication using the form on the submission page. + + +Sincerely, + +{{ proofs.stream.supervisor.user.first_name }} {{ proofs.stream.supervisor.user.last_name }} +SciPost Production diff --git a/production/constants.py b/production/constants.py index e37691c7a445b05bc6081a2c7ca3adacc5e3eee9..8c7f309dc63976aa7c03dca48b7b41fc61380a3f 100644 --- a/production/constants.py +++ b/production/constants.py @@ -47,3 +47,11 @@ PROOFS_STATUSES = ( (PROOFS_DECLINED, 'Proofs declined by authors'), (PROOFS_RENEWED, 'Proofs renewed'), ) + +PRODUCTION_OFFICERS_WORK_LOG_TYPES = ( + ('Production: Production Officer tasks', 'Production Officer tasks'), +) +PRODUCTION_ALL_WORK_LOG_TYPES = ( + ('Production: Supervisory tasks', 'Supervisory tasks'), + ('Production: Production Officer tasks', 'Production Officer tasks'), +) diff --git a/production/forms.py b/production/forms.py index bec9a69c289a2564b7ddebc66efc1fb7eb894b2d..1768d6d12d07e8e59cf64f72db7e501dea1d64ae 100644 --- a/production/forms.py +++ b/production/forms.py @@ -1,8 +1,6 @@ import datetime from django import forms -from django.utils.dates import MONTHS -from django.db.models import Sum from . import constants from .models import ProductionUser, ProductionStream, ProductionEvent, Proofs @@ -136,46 +134,6 @@ class UserToOfficerForm(forms.ModelForm): production_user__isnull=True).order_by('last_name') -class ProductionUserMonthlyActiveFilter(forms.Form): - month = forms.ChoiceField(choices=[(k, v) for k, v in MONTHS.items()]) - year = forms.ChoiceField(choices=[(y, y) for y in reversed(range(today.year-6, today.year+1))]) - - def __init__(self, *args, **kwargs): - if not kwargs.get('data', False) and not args[0]: - args = list(args) - args[0] = { - 'month': today.month, - 'year': today.year - } - args = tuple(args) - kwargs['initial'] = { - 'month': today.month, - 'year': today.year - } - super().__init__(*args, **kwargs) - - def get_totals(self): - # Make accessible without need to explicitly check validity of form. - self.is_valid() - - users = ProductionUser.objects.filter(events__duration__isnull=False, - events__noted_on__month=self.cleaned_data['month'], - events__noted_on__year=self.cleaned_data['year'] - ).distinct() - output = [] - for user in users: - events = user.events.filter(duration__isnull=False, - noted_on__month=self.cleaned_data['month'], - noted_on__year=self.cleaned_data['year']) - output.append({ - 'events': events, - 'duration': events.aggregate(total=Sum('duration')), - 'user': user - }) - - return output - - class ProofsUploadForm(forms.ModelForm): class Meta: model = Proofs @@ -215,7 +173,8 @@ class ProofsDecisionForm(forms.ModelForm): prodevent = ProductionEvent( stream=proofs.stream, event='status', - comments='Received feedback: {comments}'.format(comments=comments), + comments='Received feedback from the authors: {comments}'.format( + comments=comments), noted_by=proofs.stream.supervisor ) prodevent.save() diff --git a/production/migrations/0023_auto_20170930_1759.py b/production/migrations/0023_auto_20170930_1759.py index 2143c4cdffab851f0d7049f9a09e96c4a2f9fc3e..0f510418996feeeaa5376a1e1af99baba670296c 100644 --- a/production/migrations/0023_auto_20170930_1759.py +++ b/production/migrations/0023_auto_20170930_1759.py @@ -5,35 +5,6 @@ from __future__ import unicode_literals from django.db import migrations, models -def update_status(apps, schema_editor): - """ - Update current Production Event type. - """ - ProductionEvent = apps.get_model('production', 'ProductionEvent') - for event in ProductionEvent.objects.all(): - if event.duration: - event.event = 'registration' - elif event.event == ['assigned_to_supervisor', 'officer_tasked_with_proof_production']: - event.event = 'assignment' - elif event.event in ['message_edadmin_to_supervisor', 'message_supervisor_to_edadmin', 'message_supervisor_to_officer', 'message_officer_to_supervisor']: - event.event = 'message' - else: - event.event = 'status' - event.save() - return - - -def update_status_inverse(apps, schema_editor): - """ - Inverse update current Production Event type. As this mapping is impossible to make, - it'll just all be a unique status: `Event` - """ - ProductionEvent = apps.get_model('production', 'ProductionEvent') - for event in ProductionEvent.objects.all(): - event.event = 'Event' - return - - class Migration(migrations.Migration): dependencies = [ @@ -46,5 +17,4 @@ class Migration(migrations.Migration): name='event', field=models.CharField(choices=[('assignment', 'Assignment'), ('status', 'Status change'), ('message', 'Message'), ('registration', 'Registration hours')], max_length=64), ), - migrations.RunPython(update_status, update_status_inverse) ] diff --git a/production/migrations/0031_auto_20171010_0921.py b/production/migrations/0031_auto_20171010_0921.py new file mode 100644 index 0000000000000000000000000000000000000000..dcf9fe6f6dad9b30fe69346ce5747543fc0a247d --- /dev/null +++ b/production/migrations/0031_auto_20171010_0921.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-10 07:21 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0030_auto_20171009_2111'), + ] + + operations = [ + migrations.AlterModelOptions( + name='proofs', + options={'ordering': ['stream', 'version'], 'verbose_name_plural': 'Proofs'}, + ), + ] diff --git a/production/migrations/0032_auto_20171010_1008.py b/production/migrations/0032_auto_20171010_1008.py new file mode 100644 index 0000000000000000000000000000000000000000..e92a693b6487c1f75028d2d3c3ace21afef92dd8 --- /dev/null +++ b/production/migrations/0032_auto_20171010_1008.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-10 08:08 +from __future__ import unicode_literals + +from django.db import migrations + + +def update_status(apps, schema_editor): + """ + Update current Production Event type. + """ + ProductionEvent = apps.get_model('production', 'ProductionEvent') + for event in ProductionEvent.objects.all(): + if event.duration: + event.event = 'registration' + elif event.event == ['assigned_to_supervisor', 'officer_tasked_with_proof_production']: + event.event = 'assignment' + elif event.event in ['message_edadmin_to_supervisor', 'message_supervisor_to_edadmin', 'message_supervisor_to_officer', 'message_officer_to_supervisor']: + event.event = 'message' + else: + event.event = 'status' + event.save() + return + + +def update_status_inverse(apps, schema_editor): + """ + Inverse update current Production Event type. As this mapping is impossible to make, + it'll just all be a unique status: `Event` + """ + ProductionEvent = apps.get_model('production', 'ProductionEvent') + for event in ProductionEvent.objects.all(): + event.event = 'Event' + return + + +class Migration(migrations.Migration): + + dependencies = [ + ('finances', '0006_auto_20171010_1003'), + ('production', '0031_auto_20171010_0921'), + ] + + operations = [ + # Do not run this migration, + # the field is U/S anyway and in case it goes wrong we loose data here. + + # migrations.RunPython(update_status, update_status_inverse) + ] diff --git a/production/models.py b/production/models.py index 3c55f1a91ebd3c88b0a6c54abf78a4905ad59b41..549027075dea4866f62f1cb3cc2d5620cfd60868 100644 --- a/production/models.py +++ b/production/models.py @@ -63,7 +63,7 @@ class ProductionStream(models.Model): @cached_property def total_duration(self): - totdur = self.events.aggregate(models.Sum('duration')) + totdur = self.work_logs.aggregate(models.Sum('duration')) return totdur['duration__sum'] @cached_property diff --git a/production/templates/production/partials/production_events.html b/production/templates/production/partials/production_events.html index 13b1b0251dd3cbac9fe0a1c524fa349db5ff56ae..653f9c3df4bbf1999172d6fd62d320206a720b41 100644 --- a/production/templates/production/partials/production_events.html +++ b/production/templates/production/partials/production_events.html @@ -43,8 +43,3 @@ <li>No events were found.</li> {% endfor %} </ul> - -{% if stream.total_duration %} - <hr> - <p class="text-right">Total duration for this stream: <strong>{{ stream.total_duration|duration }}</strong></p> -{% endif %} diff --git a/production/templates/production/partials/production_stream_card.html b/production/templates/production/partials/production_stream_card.html index 7a5470487dd6c901fe07e19d453126247d315d32..85473b2a34db7edfd56af09298e575b9c3a9df72 100644 --- a/production/templates/production/partials/production_stream_card.html +++ b/production/templates/production/partials/production_stream_card.html @@ -1,6 +1,7 @@ {% extends 'production/partials/production_stream_card_completed.html' %} {% load bootstrap %} +{% load scipost_extras %} {% block actions %} {% include 'production/partials/stream_status_changes.html' with form=status_form stream=stream %} @@ -22,7 +23,7 @@ <ul> <li> <a href="javascript:;" data-toggle="toggle" data-target="#log_form">Add hours to the Stream</a> - <form id="log_form" style="display: none;" action="{% url 'production:add_event' stream_id=stream.id %}" method="post" class="mb-2"> + <form id="log_form" style="display: none;" action="{% url 'production:add_work_log' stream_id=stream.id %}" method="post" class="mb-2"> {% csrf_token %} {{ work_log_form|bootstrap }} <input type="submit" class="btn btn-secondary" name="submit" value="Log"> @@ -33,6 +34,11 @@ {% include 'partials/finances/logs.html' with logs=stream.work_logs.all %} + {% if stream.total_duration %} + <hr> + <p class="text-right">Total duration for this stream: <strong>{{ stream.total_duration|duration }}</strong></p> + {% endif %} + {% if "can_perform_supervisory_actions" in sub_perms or "can_work_for_stream" in sub_perms %} <h3>Actions</h3> <ul> diff --git a/production/templates/production/partials/production_stream_card_completed.html b/production/templates/production/partials/production_stream_card_completed.html index 99165bd4bdd764038d8433eab2c05aeef933d14c..19eb76bb651c36fc534b62fcd830b29da70ae657 100644 --- a/production/templates/production/partials/production_stream_card_completed.html +++ b/production/templates/production/partials/production_stream_card_completed.html @@ -1,5 +1,6 @@ {% load bootstrap %} {% load guardian_tags %} +{% load scipost_extras %} {% get_obj_perms request.user for stream as "sub_perms" %} @@ -41,6 +42,11 @@ <h3>Work Log</h3> {% include 'partials/finances/logs.html' with logs=stream.work_logs.all %} + + {% if stream.total_duration %} + <hr class="sm"> + <p class="pl-4 ml-3">Total duration for this stream: <strong>{{ stream.total_duration|duration }}</strong></p> + {% endif %} {% endblock %} {% if "can_work_for_stream" in sub_perms %} diff --git a/production/templates/production/production.html b/production/templates/production/production.html index 80dc89057a589d769bb44cff099e2b418aa82621..844922fd09730361c68469c5a446ac6493eb5201 100644 --- a/production/templates/production/production.html +++ b/production/templates/production/production.html @@ -7,6 +7,7 @@ {% block pagetitle %}: Production page{% endblock pagetitle %} {% load bootstrap %} +{% load scipost_extras %} {% block content %} @@ -113,23 +114,24 @@ <thead class="thead-default"> <tr> <th>Date</th> + <th>Comment</th> <th>Stream</th> - <th>Event</th> + <th>Log type</th> <th>Duration</th> </tr> </thead> - <tbody role="tablist"> - {% for event in ownevents %} - <tr> - <td>{{ event.noted_on }}</td> - <td>{{ event.stream }}</td> - <td>{{ event.get_event_display }}</td> - <td>{{ event.duration }}</td> - </tr> + {% for log in request.user.work_logs.all %} + <tr> + <td>{{ log.work_date }}</td> + <td>{{ log.comments }}</td> + <td>{{ log.content }}</td> + <td>{{ log.log_type }}</td> + <td>{{ log.duration|duration }}</td> + </tr> {% empty %} <tr> - <td colspan="4">No events found.</td> + <td colspan="4">No logs found.</td> </tr> {% endfor %} </tbody> diff --git a/production/urls.py b/production/urls.py index a4c6703083ab5420c1461e5ee3d2c334539e2b3a..c44ef6554601b294b125872607bb94f85bd7dcc5 100644 --- a/production/urls.py +++ b/production/urls.py @@ -23,6 +23,8 @@ urlpatterns = [ production_views.toggle_accessibility, name='toggle_accessibility'), url(r'^streams/(?P<stream_id>[0-9]+)/events/add$', production_views.add_event, name='add_event'), + url(r'^streams/(?P<stream_id>[0-9]+)/logs/add$', + production_views.add_work_log, name='add_work_log'), url(r'^streams/(?P<stream_id>[0-9]+)/officer/add$', production_views.add_officer, name='add_officer'), url(r'^streams/(?P<stream_id>[0-9]+)/officer/(?P<officer_id>[0-9]+)/remove$', diff --git a/production/views.py b/production/views.py index 83f047c4dc23926191f4d33d9ac78ed7ac3a75d3..49bfa8926e18bd7ba4c338287cb27d08c1c6110d 100644 --- a/production/views.py +++ b/production/views.py @@ -16,6 +16,7 @@ from guardian.core import ObjectPermissionChecker from guardian.shortcuts import assign_perm, remove_perm from finances.forms import WorkLogForm +from mails.views import MailEditingSubView from . import constants from .models import ProductionUser, ProductionStream, ProductionEvent, Proofs @@ -44,13 +45,8 @@ def production(request, stream_id=None): streams = streams.filter_for_user(request.user.production_user) streams = streams.order_by('opened') - ownevents = ProductionEvent.objects.filter( - noted_by=request.user.production_user, - duration__gte=datetime.timedelta(minutes=1)).order_by('-noted_on') - context = { 'streams': streams, - 'ownevents': ownevents, } if stream_id: @@ -61,7 +57,12 @@ def production(request, stream_id=None): context['assign_invitiations_officer_form'] = AssignInvitationsOfficerForm() context['assign_supervisor_form'] = AssignSupervisorForm() context['prodevent_form'] = ProductionEventForm() - context['work_log_form'] = WorkLogForm() + + if request.user.has_perm('scipost.can_view_all_production_streams'): + types = constants.PRODUCTION_ALL_WORK_LOG_TYPES + else: + types = constants.PRODUCTION_OFFICERS_WORK_LOG_TYPES + context['work_log_form'] = WorkLogForm(log_types=types) context['upload_proofs_form'] = ProofsUploadForm() except ProductionStream.DoesNotExist: pass @@ -106,7 +107,12 @@ def stream(request, stream_id): assign_invitiations_officer_form = AssignInvitationsOfficerForm() assign_supervisor_form = AssignSupervisorForm() upload_proofs_form = ProofsUploadForm() - work_log_form = WorkLogForm() + + if request.user.has_perm('scipost.can_view_all_production_streams'): + types = constants.PRODUCTION_ALL_WORK_LOG_TYPES + else: + types = constants.PRODUCTION_OFFICERS_WORK_LOG_TYPES + work_log_form = WorkLogForm(log_types=types) status_form = StreamStatusForm(instance=stream, production_user=request.user.production_user) context = { @@ -180,6 +186,31 @@ def add_event(request, stream_id): return redirect(reverse('production:production', args=(stream.id,))) +@is_production_user() +@permission_required('scipost.can_view_production', raise_exception=True) +def add_work_log(request, stream_id): + stream = get_object_or_404(ProductionStream, pk=stream_id) + checker = ObjectPermissionChecker(request.user) + if not checker.has_perm('can_work_for_stream', stream): + return redirect(stream.get_absolute_url()) + + if request.user.has_perm('scipost.can_view_all_production_streams'): + types = constants.PRODUCTION_ALL_WORK_LOG_TYPES + else: + types = constants.PRODUCTION_OFFICERS_WORK_LOG_TYPES + work_log_form = WorkLogForm(request.POST or None, log_types=types) + + if work_log_form.is_valid(): + log = work_log_form.save(commit=False) + log.content = stream + log.user = request.user + log.save() + messages.success(request, 'Work Log added to Stream.') + else: + messages.warning(request, 'The form was invalidly filled.') + return redirect(stream.get_absolute_url()) + + @is_production_user() @permission_required('scipost.can_assign_production_officer', raise_exception=True) @transaction.atomic @@ -581,20 +612,35 @@ def send_proofs(request, stream_id, version): return redirect(reverse('production:production')) try: - proof = stream.proofs.can_be_send().get(version=version) + proofs = stream.proofs.can_be_send().get(version=version) except Proofs.DoesNotExist: raise Http404 - proof.status = constants.PROOFS_SENT - proof.accessible_for_authors = True - proof.save() + proofs.status = constants.PROOFS_SENT + proofs.accessible_for_authors = True if stream.status not in [constants.PROOFS_PUBLISHED, constants.PROOFS_CITED, constants.PRODUCTION_STREAM_COMPLETED]: stream.status = constants.PROOFS_SENT stream.save() - # TODO: SEND EMAIL TO NOTIFY OR KEEP THIS A HUMAN ACTION? + mail_request = MailEditingSubView(request, mail_code='production_send_proofs', + proofs=proofs) + if mail_request.is_valid(): + proofs.save() + stream.save() + messages.success(request, 'Proofs have been sent.') + mail_request.send() + prodevent = ProductionEvent( + stream=stream, + event='status', + comments='Proofs version {version} sent to authors.'.format(version=proofs.version), + noted_by=request.user.production_user + ) + prodevent.save() + return redirect(stream.get_absolute_url()) + else: + return mail_request.return_render() messages.success(request, 'Proofs have been sent.') return redirect(stream.get_absolute_url()) diff --git a/scipost/views.py b/scipost/views.py index b6c67e3297e4c11354988483ab9225e43acf08be..c142244dc7789f3c738d31341bfc9b0aaa3148ee 100644 --- a/scipost/views.py +++ b/scipost/views.py @@ -39,7 +39,7 @@ from commentaries.models import Commentary from comments.models import Comment from journals.models import Publication, Journal from news.models import NewsItem -from submissions.models import Submission, EditorialAssignment, RefereeInvitation,\ +from submissions.models import Submission, RefereeInvitation,\ Report, EICRecommendation from partners.models import MembershipAgreement from theses.models import ThesisLink