show all item timestamps in timeline
This commit is contained in:
parent
9e0540d133
commit
1568252112
7 changed files with 313 additions and 22 deletions
|
@ -1,6 +1,9 @@
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.db.models.signals import pre_save
|
||||||
|
from django.dispatch import receiver
|
||||||
|
from django.utils import timezone
|
||||||
from django_softdelete.models import SoftDeleteModel, SoftDeleteManager
|
from django_softdelete.models import SoftDeleteModel, SoftDeleteManager
|
||||||
|
|
||||||
|
|
||||||
|
@ -64,6 +67,11 @@ class Item(SoftDeleteModel):
|
||||||
return '[' + str(self.id) + ']' + self.description
|
return '[' + str(self.id) + ']' + self.description
|
||||||
|
|
||||||
|
|
||||||
|
@receiver(pre_save, sender=Item)
|
||||||
|
def item_updated(sender, instance, **kwargs):
|
||||||
|
instance.updated_at = timezone.now()
|
||||||
|
|
||||||
|
|
||||||
class Container(SoftDeleteModel):
|
class Container(SoftDeleteModel):
|
||||||
id = models.AutoField(primary_key=True)
|
id = models.AutoField(primary_key=True)
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
|
|
@ -132,6 +132,22 @@ class ItemSerializer(BasicItemSerializer):
|
||||||
'cid': placement.container.id,
|
'cid': placement.container.id,
|
||||||
'box': placement.container.name
|
'box': placement.container.name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if obj.created_at:
|
||||||
|
timeline.append({
|
||||||
|
'type': 'created',
|
||||||
|
'timestamp': obj.created_at,
|
||||||
|
})
|
||||||
|
if obj.returned_at:
|
||||||
|
timeline.append({
|
||||||
|
'type': 'returned',
|
||||||
|
'timestamp': obj.returned_at,
|
||||||
|
})
|
||||||
|
if obj.deleted_at:
|
||||||
|
timeline.append({
|
||||||
|
'type': 'deleted',
|
||||||
|
'timestamp': obj.deleted_at,
|
||||||
|
})
|
||||||
return sorted(timeline, key=lambda x: x['timestamp'])
|
return sorted(timeline, key=lambda x: x['timestamp'])
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -63,28 +63,28 @@ class ItemTestCase(TestCase):
|
||||||
self.assertEqual(response.json()[0]['file'], None)
|
self.assertEqual(response.json()[0]['file'], None)
|
||||||
self.assertEqual(response.json()[0]['returned'], False)
|
self.assertEqual(response.json()[0]['returned'], False)
|
||||||
self.assertEqual(response.json()[0]['event'], self.event.slug)
|
self.assertEqual(response.json()[0]['event'], self.event.slug)
|
||||||
self.assertEqual(len(response.json()[0]['timeline']), 4)
|
self.assertEqual(len(response.json()[0]['timeline']), 5)
|
||||||
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'placement')
|
self.assertEqual(response.json()[0]['timeline'][0]['type'], 'created')
|
||||||
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'comment')
|
self.assertEqual(response.json()[0]['timeline'][1]['type'], 'placement')
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['type'], 'issue_relation')
|
self.assertEqual(response.json()[0]['timeline'][2]['type'], 'comment')
|
||||||
self.assertEqual(response.json()[0]['timeline'][3]['type'], 'placement')
|
self.assertEqual(response.json()[0]['timeline'][3]['type'], 'issue_relation')
|
||||||
self.assertEqual(response.json()[0]['timeline'][1]['id'], comment.id)
|
self.assertEqual(response.json()[0]['timeline'][4]['type'], 'placement')
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['id'], match.id)
|
self.assertEqual(response.json()[0]['timeline'][2]['id'], comment.id)
|
||||||
self.assertEqual(response.json()[0]['timeline'][3]['id'], placement.id)
|
self.assertEqual(response.json()[0]['timeline'][3]['id'], match.id)
|
||||||
self.assertEqual(response.json()[0]['timeline'][0]['box'], 'BOX1')
|
self.assertEqual(response.json()[0]['timeline'][4]['id'], placement.id)
|
||||||
self.assertEqual(response.json()[0]['timeline'][0]['cid'], self.box1.id)
|
self.assertEqual(response.json()[0]['timeline'][1]['box'], 'BOX1')
|
||||||
self.assertEqual(response.json()[0]['timeline'][1]['comment'], 'test')
|
self.assertEqual(response.json()[0]['timeline'][1]['cid'], self.box1.id)
|
||||||
self.assertEqual(response.json()[0]['timeline'][1]['timestamp'],
|
self.assertEqual(response.json()[0]['timeline'][0]['timestamp'], item.created_at.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||||
comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
self.assertEqual(response.json()[0]['timeline'][2]['comment'], 'test')
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['status'], 'possible')
|
self.assertEqual(response.json()[0]['timeline'][2]['timestamp'], comment.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['timestamp'],
|
self.assertEqual(response.json()[0]['timeline'][3]['status'], 'possible')
|
||||||
match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'], match.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['issue_thread']['name'], "test issue")
|
self.assertEqual(response.json()[0]['timeline'][3]['issue_thread']['name'], "test issue")
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['issue_thread']['event'], "EVENT")
|
self.assertEqual(response.json()[0]['timeline'][3]['issue_thread']['event'], "EVENT")
|
||||||
self.assertEqual(response.json()[0]['timeline'][2]['issue_thread']['state'], "pending_new")
|
self.assertEqual(response.json()[0]['timeline'][3]['issue_thread']['state'], "pending_new")
|
||||||
self.assertEqual(response.json()[0]['timeline'][3]['box'], 'BOX2')
|
self.assertEqual(response.json()[0]['timeline'][4]['box'], 'BOX2')
|
||||||
self.assertEqual(response.json()[0]['timeline'][3]['cid'], self.box2.id)
|
self.assertEqual(response.json()[0]['timeline'][4]['cid'], self.box2.id)
|
||||||
self.assertEqual(response.json()[0]['timeline'][3]['timestamp'],
|
self.assertEqual(response.json()[0]['timeline'][4]['timestamp'],
|
||||||
placement.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
placement.timestamp.strftime('%Y-%m-%dT%H:%M:%S.%fZ'))
|
||||||
self.assertEqual(len(response.json()[0]['related_issues']), 1)
|
self.assertEqual(len(response.json()[0]['related_issues']), 1)
|
||||||
self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue")
|
self.assertEqual(response.json()[0]['related_issues'][0]['name'], "test issue")
|
||||||
|
|
|
@ -24,6 +24,15 @@
|
||||||
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'placement'">
|
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'placement'">
|
||||||
<font-awesome-icon icon="archive"/>
|
<font-awesome-icon icon="archive"/>
|
||||||
</span>
|
</span>
|
||||||
|
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'created'">
|
||||||
|
<font-awesome-icon icon="archive"/>
|
||||||
|
</span>
|
||||||
|
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'returned'">
|
||||||
|
<font-awesome-icon icon="archive"/>
|
||||||
|
</span>
|
||||||
|
<span class="timeline-item-icon faded-icon" v-else-if="item.type === 'deleted'">
|
||||||
|
<font-awesome-icon icon="trash"/>
|
||||||
|
</span>
|
||||||
<span class="timeline-item-icon faded-icon" v-else>
|
<span class="timeline-item-icon faded-icon" v-else>
|
||||||
<font-awesome-icon icon="pen"/>
|
<font-awesome-icon icon="pen"/>
|
||||||
</span>
|
</span>
|
||||||
|
@ -35,6 +44,9 @@
|
||||||
<TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/>
|
<TimelineShippingVoucher v-else-if="item.type === 'shipping_voucher'" :item="item"/>
|
||||||
<TimelinePlacement v-else-if="item.type === 'placement'" :item="item"/>
|
<TimelinePlacement v-else-if="item.type === 'placement'" :item="item"/>
|
||||||
<TimelineRelatedTicket v-else-if="item.type === 'issue_relation'" :item="item"/>
|
<TimelineRelatedTicket v-else-if="item.type === 'issue_relation'" :item="item"/>
|
||||||
|
<TimelineCreated v-else-if="item.type === 'created'" :item="item"/>
|
||||||
|
<TimelineReturned v-else-if="item.type === 'returned'" :item="item"/>
|
||||||
|
<TimelineDeleted v-else-if="item.type === 'deleted'" :item="item"/>
|
||||||
<p v-else>{{ item }}</p>
|
<p v-else>{{ item }}</p>
|
||||||
</li>
|
</li>
|
||||||
<li class="timeline-item">
|
<li class="timeline-item">
|
||||||
|
@ -58,10 +70,16 @@ import TimelineShippingVoucher from "@/components/TimelineShippingVoucher.vue";
|
||||||
import AsyncButton from "@/components/inputs/AsyncButton.vue";
|
import AsyncButton from "@/components/inputs/AsyncButton.vue";
|
||||||
import TimelinePlacement from "@/components/TimelinePlacement.vue";
|
import TimelinePlacement from "@/components/TimelinePlacement.vue";
|
||||||
import TimelineRelatedTicket from "@/components/TimelineRelatedTicket.vue";
|
import TimelineRelatedTicket from "@/components/TimelineRelatedTicket.vue";
|
||||||
|
import TimelineCreated from "@/components/TimelineCreated.vue";
|
||||||
|
import TimelineReturned from "@/components/TimelineReturned.vue";
|
||||||
|
import TimelineDeleted from "@/components/TimelineDeleted.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Timeline',
|
name: 'Timeline',
|
||||||
components: {
|
components: {
|
||||||
|
TimelineDeleted,
|
||||||
|
TimelineReturned,
|
||||||
|
TimelineCreated,
|
||||||
TimelineRelatedTicket,
|
TimelineRelatedTicket,
|
||||||
TimelinePlacement,
|
TimelinePlacement,
|
||||||
TimelineShippingVoucher,
|
TimelineShippingVoucher,
|
||||||
|
|
83
web/src/components/TimelineCreated.vue
Normal file
83
web/src/components/TimelineCreated.vue
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div class="timeline-item-description"><span>created by
|
||||||
|
<i class="avatar | small">
|
||||||
|
<font-awesome-icon icon="user"/>
|
||||||
|
</i>
|
||||||
|
<a href="#">$USER</a> at <time :datetime="timestamp">{{ timestamp }}</time></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TimelineCreated',
|
||||||
|
props: {
|
||||||
|
'item': {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
'timestamp': function () {
|
||||||
|
return new Date(this.item.timestamp).toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.timeline-item-description {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 6px;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--gray);
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
/*color: var(--c-grey-500);*/
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
outline: 0; /* Don't actually do this */
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
83
web/src/components/TimelineDeleted.vue
Normal file
83
web/src/components/TimelineDeleted.vue
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div class="timeline-item-description"><span>marked deleted by
|
||||||
|
<i class="avatar | small">
|
||||||
|
<font-awesome-icon icon="user"/>
|
||||||
|
</i>
|
||||||
|
<a href="#">$USER</a> at <time :datetime="timestamp">{{ timestamp }}</time></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TimelineDeleted',
|
||||||
|
props: {
|
||||||
|
'item': {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
'timestamp': function () {
|
||||||
|
return new Date(this.item.timestamp).toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.timeline-item-description {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 6px;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--gray);
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
/*color: var(--c-grey-500);*/
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
outline: 0; /* Don't actually do this */
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
83
web/src/components/TimelineReturned.vue
Normal file
83
web/src/components/TimelineReturned.vue
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
<template>
|
||||||
|
<div class="timeline-item-description"><span>marked returned by
|
||||||
|
<i class="avatar | small">
|
||||||
|
<font-awesome-icon icon="user"/>
|
||||||
|
</i>
|
||||||
|
<a href="#">$USER</a> at <time :datetime="timestamp">{{ timestamp }}</time></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import {mapState} from "vuex";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'TimelineReturned',
|
||||||
|
props: {
|
||||||
|
'item': {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
'timestamp': function () {
|
||||||
|
return new Date(this.item.timestamp).toLocaleString();
|
||||||
|
},
|
||||||
|
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.timeline-item-description {
|
||||||
|
display: flex;
|
||||||
|
padding-top: 6px;
|
||||||
|
gap: 8px;
|
||||||
|
color: var(--gray);
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
/*color: var(--c-grey-500);*/
|
||||||
|
font-weight: 500;
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
outline: 0; /* Don't actually do this */
|
||||||
|
color: var(--info);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
&.small {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
Loading…
Add table
Reference in a new issue