Time tracking you can host anywhere, built on Django. Full export support in multiple formats and easily extensible.
djangodockerhandcodedpythonself-hostedtime-trackingtimetracker
1from datetime import timedelta
2from decimal import Decimal
3from io import StringIO
4from csv import DictReader
5
6from django.test import TestCase
7from django.test import Client as HttpClient
8from django.contrib.auth.models import User
9from django.core.management import call_command
10
11from faker import Factory
12
13from conf.models import Site, SitePermission
14
15from .models import Client, Project, Entry, Task
16from .utils import parse_duration, duration_string, duration_decimal
17
18
19fake = Factory.create()
20
21
22class ClientTestCase(TestCase):
23 def setUp(self):
24 call_command("migrate", verbosity=0)
25
26 def test_client_created(self):
27 Client.objects.create(name="Timestrap")
28 client = Client.objects.get(name="Timestrap")
29 self.assertEqual(client.name, "Timestrap")
30
31 def test_client_created_unicode(self):
32 Client.objects.create(name="Юникод")
33 client = Client.objects.get(name="Юникод")
34 self.assertEqual(client.name, "Юникод")
35
36 def test_client_site_relationship(self):
37 Site.objects.create(domain="test.site", name="Test Site")
38 client = Client.objects.create(name="Client on time.strap")
39 self.assertEqual(client.sites.get().name, "Timestrap")
40 client = Client.objects.create(name="Client on test.site")
41 client.sites.set(Site.objects.filter(domain="test.site"))
42 client.save()
43 self.assertEqual(client.sites.get().name, "Test Site")
44
45
46class ProjectTestCase(TestCase):
47 def setUp(self):
48 call_command("migrate", verbosity=0)
49 self.user = User.objects.first()
50 self.client = Client.objects.create(name="Timestrap")
51
52 def test_project_created(self):
53 Project.objects.create(client=self.client, name="Testing")
54 project = Project.objects.get(name="Testing")
55 self.assertEqual(project.name, "Testing")
56 self.assertEqual(project.client, self.client)
57
58 def test_project_created_unicode(self):
59 Project.objects.create(client=self.client, name="Юникод")
60 project = Project.objects.get(name="Юникод")
61 self.assertEqual(project.name, "Юникод")
62 self.assertEqual(project.client, self.client)
63
64 def test_project_estimate(self):
65 project = Project.objects.create(
66 client=self.client, name="Testing", estimate=5000.00
67 )
68 task = Task.objects.create(name="Testing", hourly_rate=100.00)
69 Entry.objects.create(
70 project=project,
71 user=self.user,
72 task=task,
73 duration=timedelta(hours=10),
74 note="Creating tests for the core app",
75 )
76 self.assertEqual(project.get_percent_done(), 20)
77
78
79class EntryTestCase(TestCase):
80 def setUp(self):
81 call_command("migrate", verbosity=0)
82 self.user = User.objects.first()
83 client = Client.objects.create(name="Timestrap")
84 self.project = Project.objects.create(client=client, name="Testing")
85
86 Entry.objects.create(
87 project=self.project,
88 user=self.user,
89 duration=timedelta(hours=1),
90 note="Creating tests for the core app",
91 )
92 Entry.objects.create(
93 project=self.project,
94 user=self.user,
95 duration=timedelta(hours=2),
96 note="Continue creating tests for the core app",
97 )
98
99 def test_entry_created(self):
100 entry = Entry.objects.get(duration=timedelta(hours=1))
101 self.assertEqual(entry.note, "Creating tests for the core app")
102 entries = Entry.objects.filter(duration__lte=timedelta(hours=1, minutes=30))
103 self.assertEqual(len(entries), 1)
104
105 def test_entry_site_relationship(self):
106 site = Site.objects.create(domain="test.site", name="Test Site")
107 entry = Entry.objects.get(duration=timedelta(hours=1))
108 self.assertEqual(entry.site.name, "Timestrap")
109 entry.site = site
110 entry.save()
111 self.assertEqual(entry.site.name, "Test Site")
112
113 def test_parse_duration(self):
114 duration = parse_duration("3.25")
115 self.assertEqual(duration, timedelta(hours=3, minutes=15))
116 duration = parse_duration("2:34")
117 self.assertEqual(duration, timedelta(hours=2, minutes=34))
118 duration = parse_duration("0:05")
119 self.assertEqual(duration, timedelta(hours=0, minutes=5))
120 duration = parse_duration("5")
121 self.assertEqual(duration, timedelta(hours=5))
122 self.assertEqual(None, parse_duration("wut"))
123
124 def test_duration_string(self):
125 duration = duration_string(timedelta(hours=1, minutes=30))
126 self.assertEqual(duration, "1:30")
127 duration = duration_string(timedelta(hours=3, minutes=2))
128 self.assertEqual(duration, "3:02")
129 duration = duration_string(None)
130 self.assertAlmostEqual(duration, "0:00")
131
132 def test_duration_decimal(self):
133 # TODO: Determine is assertAlmostEqual is appropriate here.
134 duration = duration_decimal(timedelta(hours=2, minutes=3))
135 self.assertAlmostEqual(duration, Decimal(2.05))
136 duration = duration_decimal(timedelta(hours=5, minutes=15))
137 self.assertAlmostEqual(duration, Decimal(5.25))
138 duration = duration_decimal(None)
139 self.assertAlmostEqual(duration, Decimal(0))
140
141
142class ReportsTestCase(TestCase):
143 def setUp(self):
144 call_command("migrate", verbosity=0)
145 self.c = HttpClient()
146
147 fake_user = fake.simple_profile()
148 fake_password = fake.password()
149 user = User.objects.create_superuser(
150 fake_user["username"], fake_user["mail"], fake_password
151 )
152 site_permission = SitePermission.objects.create(user=user)
153 site_permission.sites.set(Site.objects.filter(id=1))
154 site_permission.save()
155
156 self.c.login(username=fake_user["username"], password=fake_password)
157
158 call_command("fake", verbosity=0, iterations=1)
159
160 def assert_entries_by_params(self, params):
161 url = "/reports/export/?"
162 for param, value in params.items():
163 url += "{0}={1}&".format(param, value)
164 report = self.c.get(url)
165 lines = DictReader(StringIO(report.content.decode("utf-8")))
166 for line in lines:
167 self.filter_exported_entry(line).delete()
168 self.assertFalse(Entry.objects.filter(**params).exists())
169
170 def filter_exported_entry(self, line):
171 # TODO: Improve this for better reporting when assertions fail.
172 entries = Entry.objects.filter(
173 date=line["date"],
174 duration=parse_duration(line["duration"]),
175 note=line["note"],
176 project__name=line["project__name"],
177 user__username=line["user__username"],
178 )
179 self.assertEqual(len(entries), 1)
180 return entries[0]
181
182 def test_export_response(self):
183 report = self.c.get("/reports/export/")
184 self.assertEqual(report.status_code, 200)
185 self.assertEqual(report.get("Content-Type"), "text/csv")
186 self.assertEqual(
187 report.get("Content-Disposition"), 'attachment; filename="report.csv"'
188 )
189
190 def test_export_all(self):
191 report = self.c.get("/reports/export/")
192 lines = DictReader(StringIO(report.content.decode("utf-8")))
193 for line in lines:
194 self.filter_exported_entry(line).delete()
195 # The above should have deleted _all_ entries
196 self.assertFalse(Entry.objects.all().exists())
197
198 def test_export_formats(self):
199 for f in ["csv", "xls", "tsv", "ods", "json", "yaml", "html"]:
200 report = self.c.get("/reports/export/?export_format={0}".format(f))
201 self.assertEqual(report.status_code, 200)
202 self.assertEqual(report.get("Content-Type"), "text/{0}".format(f))
203
204 def test_export_param_project(self):
205 self.assert_entries_by_params({"project": Project.objects.first().id})
206
207 def test_export_param_client(self):
208 self.assert_entries_by_params({"project__client": Client.objects.first().id})
209
210 def test_export_param_user(self):
211 self.assert_entries_by_params({"user": User.objects.first().id})
212
213 def test_export_param_date_range(self):
214 dates = sorted([Entry.objects.first().date, Entry.objects.last().date])
215 self.assert_entries_by_params(
216 {"date__gte": str(dates[0]), "date__lte": str(dates[1])}
217 )