diff --git a/finances/admin.py b/finances/admin.py index 8c38f3f3dad51e4585f3984282c2a4bec5349c1e..d8d4d52c35c2131bf14a5313cff84015f49881a7 100644 --- a/finances/admin.py +++ b/finances/admin.py @@ -1,3 +1,6 @@ from django.contrib import admin -# Register your models here. +from .models import WorkLog + + +admin.site.register(WorkLog) diff --git a/finances/forms.py b/finances/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..dd99b82bb5aa501a3b23097fbcace9a0a56e4350 --- /dev/null +++ b/finances/forms.py @@ -0,0 +1,16 @@ +from django import forms + +from .models import WorkLog + + +class WorkLogForm(forms.ModelForm): + class Meta: + model = WorkLog + fields = ( + 'comments', + 'duration', + ) + widgets = { + 'comments': forms.Textarea(attrs={'rows': 4}), + 'duration': forms.TextInput(attrs={'placeholder': 'HH:MM:SS'}) + } diff --git a/finances/migrations/0001_initial.py b/finances/migrations/0001_initial.py new file mode 100644 index 0000000000000000000000000000000000000000..a0b67974f46214763138959424ed10bb21f5ef96 --- /dev/null +++ b/finances/migrations/0001_initial.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-07 11:49 +from __future__ import unicode_literals + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='WorkLog', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('comments', models.TextField(blank=True)), + ('duration', models.DurationField(blank=True, null=True)), + ('work_date', models.DateField(default=django.utils.timezone.now)), + ('created', models.DateTimeField(auto_now_add=True)), + ('target_object_id', models.PositiveIntegerField(blank=True, null=True)), + ('target_content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='work_log_target', to='contenttypes.ContentType')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='worklogs', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'default_related_name': 'worklogs', + }, + ), + ] diff --git a/finances/migrations/0002_auto_20171007_1349.py b/finances/migrations/0002_auto_20171007_1349.py new file mode 100644 index 0000000000000000000000000000000000000000..abab65056fe0b8b637687d7b06d3444086eed0a9 --- /dev/null +++ b/finances/migrations/0002_auto_20171007_1349.py @@ -0,0 +1,46 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-07 11:50 +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 = [ + ('finances', '0001_initial'), + ('production', '0028_auto_20171007_1311'), + ] + + operations = [ + migrations.RunPython(move_hour_registrations, move_hour_registrations_inverse) + ] diff --git a/finances/migrations/0003_auto_20171007_1425.py b/finances/migrations/0003_auto_20171007_1425.py new file mode 100644 index 0000000000000000000000000000000000000000..6d667cb908443488220eeff36b3cecac147926e5 --- /dev/null +++ b/finances/migrations/0003_auto_20171007_1425.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-07 12:25 +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 = [ + ('contenttypes', '0002_remove_content_type_name'), + ('finances', '0002_auto_20171007_1349'), + ] + + operations = [ + migrations.RenameField( + model_name='worklog', + old_name='target_object_id', + new_name='object_id', + ), + migrations.RenameField( + model_name='worklog', + old_name='target_content_type', + new_name='content_type', + ), + migrations.AlterField( + model_name='worklog', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='work_logs', to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/finances/migrations/0004_auto_20171007_1426.py b/finances/migrations/0004_auto_20171007_1426.py new file mode 100644 index 0000000000000000000000000000000000000000..5d039886b918bc5e673be76bb82caaf1e4bb3e0a --- /dev/null +++ b/finances/migrations/0004_auto_20171007_1426.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-07 12:26 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('finances', '0003_auto_20171007_1425'), + ] + + operations = [ + migrations.AlterField( + model_name='worklog', + name='content_type', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='work_logs', to='contenttypes.ContentType'), + ), + ] diff --git a/finances/models.py b/finances/models.py index 71a836239075aa6e6e4ecb700e9c42c95c022d91..d4c8622fd8b3ac23b346343343ee41f2b3244b0e 100644 --- a/finances/models.py +++ b/finances/models.py @@ -1,3 +1,30 @@ +from django.conf import settings +from django.contrib.contenttypes.models import ContentType +from django.contrib.contenttypes.fields import GenericForeignKey from django.db import models +from django.utils import timezone -# Create your models here. +from .utils import id_to_slug + + +class WorkLog(models.Model): + 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) + + content_type = models.ForeignKey(ContentType, blank=True, null=True) + object_id = models.PositiveIntegerField(blank=True, null=True) + content = GenericForeignKey() + + class Meta: + default_related_name = 'work_logs' + + def __str__(self): + return 'Log of {0} {1} on {2}'.format( + self.user.first_name, self.user.last_name, self.work_date) + + @property + def slug(self): + return id_to_slug(self.id) diff --git a/finances/templates/finances/worklog_confirm_delete.html b/finances/templates/finances/worklog_confirm_delete.html new file mode 100644 index 0000000000000000000000000000000000000000..cc03bcc7db216d340cdee18bff64e842a149e612 --- /dev/null +++ b/finances/templates/finances/worklog_confirm_delete.html @@ -0,0 +1,49 @@ +{% extends 'production/base.html' %} + +{% load scipost_extras %} + +{% block breadcrumb_items %} + {{block.super}} + <span class="breadcrumb-item">Delete log</span> +{% endblock %} + +{% load bootstrap %} + +{% block content %} + +<div class="row"> + <div class="col-12"> + <h1 class="highlight">Delete log</h1> + {% include 'submissions/_submission_card_content_sparse.html' with submission=object.content.submission %} + </div> +</div> +<div class="row"> + <div class="col-12"> + <form method="post"> + {% csrf_token %} + <h3 class="mb-2">Are you sure you want to delete this work log?</h3> + <table class="table"> + <tr> + <th>Logged by</th> + <td>{{ object.user }}</td> + </tr> + <tr> + <th>Comment</th> + <td>{{ object.comments|default:'-' }}</td> + </tr> + <tr> + <th>Logged time</th> + <td>{{ object.duration|duration }}</td> + </tr> + <tr> + <th>Log date</th> + <td>{{ object.work_date }}</td> + </tr> + </table> + <input type="submit" class="btn btn-danger" value="Yes, delete log" /> + </form> + </ul> + </div> +</div> + +{% endblock content %} diff --git a/finances/templates/partials/finances/logs.html b/finances/templates/partials/finances/logs.html new file mode 100644 index 0000000000000000000000000000000000000000..7b9db48220dfd2ed74781910031b994031df1ce8 --- /dev/null +++ b/finances/templates/partials/finances/logs.html @@ -0,0 +1,34 @@ + {% load scipost_extras %} + + <ul class="list-unstyled"> + {% for log in logs %} + <li id="log_{{ log.slug }}" class="pb-2"> + <div class="d-flex justify-content-between"> + <div> + <strong>{{ log.user.first_name }} {{ log.user.last_name }}</strong> + <br> + {{ log.comments }} + </div> + <div class="text-muted text-right d-flex justify-content-end"> + <div> + {{ log.work_date }} + <br> + <strong>Duration: {{ log.duration|duration }}</strong> + </div> + <div class="pl-2"> + <a class="text-danger" href="{% url 'finances:log_delete' log.slug %}"><i class="fa fa-trash" aria-hidden="true"></i></a> + </div> + </div> + </div> + </li> + {% empty %} + <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/urls.py b/finances/urls.py index 21569e295d98c0c64a768ff55dc9c71bcbf07e7e..80346704ba12068b5150c4005395c29c3111effd 100644 --- a/finances/urls.py +++ b/finances/urls.py @@ -5,4 +5,5 @@ from . import views urlpatterns = [ url(r'^$', views.timesheets, name='finance'), url(r'^timesheets$', views.timesheets, name='timesheets'), + url(r'^logs/(?P<slug>\d+)/delete$', views.LogDeleteView.as_view(), name='log_delete'), ] diff --git a/finances/utils.py b/finances/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..b7edcfab36651b6f6948eb299393963c27473d2e --- /dev/null +++ b/finances/utils.py @@ -0,0 +1,7 @@ + +def id_to_slug(id): + return max(0, int(id) + 821) + + +def slug_to_id(slug): + return max(0, int(slug) - 821) diff --git a/finances/views.py b/finances/views.py index b967443f18581bfd581f6710541e6f73e46fbf4f..f235ef16abcdc2b786b14fb70eb69e69a4566182 100644 --- a/finances/views.py +++ b/finances/views.py @@ -1,8 +1,14 @@ +from django.contrib import messages from django.contrib.auth.decorators import permission_required +from django.http import Http404 from django.shortcuts import render +from django.views.generic.edit import DeleteView from production.forms import ProductionUserMonthlyActiveFilter +from .models import WorkLog +from .utils import slug_to_id + @permission_required('scipost.can_view_timesheets', raise_exception=True) def timesheets(request): @@ -18,3 +24,17 @@ def timesheets(request): context['totals'] = form.get_totals() return render(request, 'finances/timesheets.html', context) + + +class LogDeleteView(DeleteView): + model = WorkLog + + def get_object(self): + try: + return WorkLog.objects.get(user=self.request.user, id=slug_to_id(self.kwargs['slug'])) + except WorkLog.DoesNotExist: + raise Http404 + + def get_success_url(self): + messages.success(self.request, 'Log deleted.') + return self.object.content.get_absolute_url() diff --git a/production/forms.py b/production/forms.py index 0ec7169e7c27e77e3fc40f76d5fc106d4eb34a7a..9fc2edc32957ed5d292f545c06e840ab6f2b87b4 100644 --- a/production/forms.py +++ b/production/forms.py @@ -1,7 +1,6 @@ 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 @@ -17,11 +16,9 @@ class ProductionEventForm(forms.ModelForm): model = ProductionEvent fields = ( 'comments', - 'duration' ) widgets = { 'comments': forms.Textarea(attrs={'rows': 4}), - 'duration': forms.TextInput(attrs={'placeholder': 'HH:MM:SS'}) } diff --git a/production/migrations/0028_auto_20171007_1311.py b/production/migrations/0028_auto_20171007_1311.py new file mode 100644 index 0000000000000000000000000000000000000000..547f5cb709e966342cc30782477dbd275f98471c --- /dev/null +++ b/production/migrations/0028_auto_20171007_1311.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-10-07 11:11 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('production', '0027_merge_20171004_1947'), + ] + + operations = [ + migrations.AlterModelOptions( + name='proof', + options={'ordering': ['stream', 'version']}, + ), + migrations.AlterField( + model_name='proof', + name='status', + field=models.CharField(choices=[('uploaded', 'Proofs uploaded'), ('sent', 'Proofs sent to authors'), ('accepted_sup', 'Proofs accepted by supervisor'), ('declined_sup', 'Proofs declined by supervisor'), ('accepted', 'Proofs accepted by authors'), ('declined', 'Proofs declined by authors'), ('renewed', 'Proofs renewed')], default='uploaded', max_length=16), + ), + ] diff --git a/production/models.py b/production/models.py index c04f85841a1f6e31859b269b1cd6903a3bee3aa1..fa6da78dbfb8d539f2e34ecceaf20ef93e21cc93 100644 --- a/production/models.py +++ b/production/models.py @@ -1,5 +1,6 @@ from django.db import models from django.conf import settings +from django.contrib.contenttypes.fields import GenericRelation from django.core.urlresolvers import reverse from django.contrib.auth.models import User from django.utils import timezone @@ -11,6 +12,7 @@ from .constants import PRODUCTION_STREAM_STATUS, PRODUCTION_STREAM_INITIATED, PR from .managers import ProductionStreamQuerySet, ProductionEventManager, ProofsQuerySet from .utils import proof_id_to_slug +from finances.models import WorkLog from scipost.storage import SecureFileStorage @@ -41,6 +43,8 @@ class ProductionStream(models.Model): supervisor = models.ForeignKey('production.ProductionUser', blank=True, null=True, related_name='supervised_streams') + work_logs = GenericRelation(WorkLog, related_query_name='streams') + objects = ProductionStreamQuerySet.as_manager() class Meta: diff --git a/production/templates/production/partials/production_stream_card.html b/production/templates/production/partials/production_stream_card.html index 3367c515d6e65280e97e0d95506184476f417b52..0a3537e741a6dc75d329acea3bb45210e7d27bae 100644 --- a/production/templates/production/partials/production_stream_card.html +++ b/production/templates/production/partials/production_stream_card.html @@ -9,7 +9,7 @@ {% include 'production/partials/production_events.html' with events=stream.events.all %} {% if "can_work_for_stream" in sub_perms and prodevent_form %} - <h3>Add message and/or hours to the Stream</h3> + <h3>Add message to the Stream</h3> <form action="{% url 'production:add_event' stream_id=stream.id %}" method="post" class="mb-2"> {% csrf_token %} {{ prodevent_form|bootstrap }} @@ -17,6 +17,22 @@ </form> {% endif %} + <h3>Work Log</h3> + {% if "can_work_for_stream" in sub_perms and work_log_form %} + <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"> + {% csrf_token %} + {{ work_log_form|bootstrap }} + <input type="submit" class="btn btn-secondary" name="submit" value="Log"> + </form> + </li> + </ul> + {% endif %} + + {% include 'partials/finances/logs.html' with logs=stream.work_logs.all %} + {% if "can_perform_supervisory_actions" 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 2745f2d6505b8c73f6def8e51c5cf18ebfd7d30a..13f535c159b4fb283195584d49fb4dea2dcc7754 100644 --- a/production/templates/production/partials/production_stream_card_completed.html +++ b/production/templates/production/partials/production_stream_card_completed.html @@ -31,6 +31,9 @@ {% block actions %} <h3>Events</h3> {% include 'production/partials/production_events.html' with events=stream.events.all non_editable=1 %} + + <h3>Work Log</h3> + {% include 'partials/finances/logs.html' with logs=stream.work_logs.all %} {% endblock %} {% if "can_work_for_stream" in sub_perms %} diff --git a/production/views.py b/production/views.py index 11911b9b402f17ea8398f3ac6e56599ca565bf3e..20110d94d4477936b0ee99b629ed8ff74eb8e4d8 100644 --- a/production/views.py +++ b/production/views.py @@ -15,6 +15,8 @@ from django.views.generic.edit import UpdateView, DeleteView from guardian.core import ObjectPermissionChecker from guardian.shortcuts import assign_perm, remove_perm +from finances.forms import WorkLogForm + from . import constants from .models import ProductionUser, ProductionStream, ProductionEvent, Proof from .forms import ProductionEventForm, AssignOfficerForm, UserToOfficerForm,\ @@ -98,6 +100,7 @@ def stream(request, stream_id): assign_officer_form = AssignOfficerForm() assign_supervisor_form = AssignSupervisorForm() upload_proofs_form = ProofUploadForm() + work_log_form = WorkLogForm() status_form = StreamStatusForm(instance=stream, production_user=request.user.production_user) context = { @@ -107,6 +110,7 @@ def stream(request, stream_id): 'assign_supervisor_form': assign_supervisor_form, 'status_form': status_form, 'upload_proofs_form': upload_proofs_form, + 'work_log_form': work_log_form, } if request.GET.get('json'): @@ -161,8 +165,6 @@ def add_event(request, stream_id): if prodevent_form.is_valid(): prodevent = prodevent_form.save(commit=False) prodevent.stream = stream - if prodevent.duration: - prodevent.event = constants.EVENT_HOUR_REGISTRATION prodevent.noted_by = request.user.production_user prodevent.save() messages.success(request, 'Comment added to Stream.')