Skip to content

Commit 835f688

Browse files
Merge pull request #36 from SADiLaR/feature/add-simple-history
added django-simple-history to project
2 parents e3b78f0 + 91e014b commit 835f688

14 files changed

+289
-28
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ About the project:
2525
4. Run `make stop` to stop the docker container
2626

2727

28+
### Plugins installed
29+
#### Django Simple History
2830

29-
30-
31-
31+
https://django-simple-history.readthedocs.io/en/latest/

app/app/settings.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
"django.contrib.staticfiles",
4141
"users",
4242
"general",
43+
"simple_history",
4344
]
4445
if DEBUG:
4546
INSTALLED_APPS += [
@@ -57,6 +58,7 @@
5758
"django.contrib.auth.middleware.AuthenticationMiddleware",
5859
"django.contrib.messages.middleware.MessageMiddleware",
5960
"django.middleware.clickjacking.XFrameOptionsMiddleware",
61+
"simple_history.middleware.HistoryRequestMiddleware",
6062
]
6163

6264
ROOT_URLCONF = "app.urls"

app/general/admin.py

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22

33
from django.contrib import admin
44
from django.forms import HiddenInput, ModelForm, fields_for_model
5+
from simple_history.admin import SimpleHistoryAdmin
56

67
from .models import DocumentFile, Institution, Language, Project, Subject
78

89

9-
class ProjectAdminInline(admin.TabularInline):
10-
model = Project
11-
extra = 0
12-
13-
1410
class DocumentFileForm(ModelForm):
1511
class Meta:
1612
model = DocumentFile
@@ -47,22 +43,45 @@ def clean(self):
4743
return cleaned_data
4844

4945

50-
class DocumentFileAdmin(admin.ModelAdmin):
46+
class DocumentFileAdmin(SimpleHistoryAdmin):
5147
list_display = ["title", "license", "document_type", "available"]
52-
ordering = [
53-
"license",
54-
]
48+
ordering = ["license"]
5549
search_fields = ["title", "license", "document_type"]
56-
5750
form = DocumentFileForm
51+
history_list_display = ["title", "license", "document_type", "available"]
52+
53+
54+
class SubjectAdmin(SimpleHistoryAdmin):
55+
search_fields = ["name"]
56+
list_display = ["name"]
57+
history_list_display = ["name"]
58+
59+
60+
class LanguageAdmin(SimpleHistoryAdmin):
61+
history_list_display = ["name", "iso_code"]
62+
list_display = ["name", "iso_code"]
63+
64+
65+
class ProjectAdminInline(admin.TabularInline):
66+
model = Project
67+
extra = 0
68+
69+
70+
class ProjectAdmin(SimpleHistoryAdmin):
71+
search_fields = ["name"]
72+
list_display = ["name"]
73+
history_list_display = ["name"]
5874

5975

60-
class ProjectAdmin(admin.ModelAdmin):
76+
class InstitutionAdmin(SimpleHistoryAdmin):
77+
search_fields = ["name"]
78+
list_display = ["name"]
6179
inlines = [ProjectAdminInline]
80+
history_list_display = ["name", "abbreviation"]
6281

6382

64-
admin.site.register(Project)
65-
admin.site.register(Institution, ProjectAdmin)
66-
admin.site.register(Language)
67-
admin.site.register(Subject)
83+
admin.site.register(Project, ProjectAdmin)
84+
admin.site.register(Institution, InstitutionAdmin)
85+
admin.site.register(Language, LanguageAdmin)
86+
admin.site.register(Subject, SubjectAdmin)
6887
admin.site.register(DocumentFile, DocumentFileAdmin)
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import django.core.validators
2+
import django.db.models.deletion
3+
import simple_history.models
4+
from django.conf import settings
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('general', '0003_rename_institution_documentfile_institution_and_more'),
12+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name='HistoricalDocumentFile',
18+
fields=[
19+
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
20+
('title', models.CharField(max_length=200)),
21+
('url', models.URLField(blank=True, verbose_name='URL')),
22+
('uploaded_file', models.TextField(blank=True, help_text='PDF files up to 10MB are allowed.', max_length=100, validators=[django.core.validators.FileExtensionValidator(['pdf'])])),
23+
('available', models.BooleanField(default=True)),
24+
('license', models.CharField(choices=[('(c)', 'All rights reserved'), ('CC0', 'No rights reserved'), ('CC BY', 'Creative Commons Attribution'), ('CC BY-SA', 'Creative Commons Attribution-ShareAlike'), ('CC BY-NC', 'Creative Commons Attribution-NonCommercial'), ('CC BY-NC-SA', 'Creative Commons Attribution-NonCommercial-ShareAlike')], default='(c)', help_text='\n <a\n href="https://creativecommons.org/share-your-work/cclicenses/"\n rel="noreferrer"\n target="_blank"\n >\n More information about Creative Commons licenses.\n </a>\'\n ', max_length=200)),
25+
('mime_type', models.CharField(blank=True, help_text='This input will auto-populate.', max_length=200)),
26+
('document_type', models.CharField(choices=[('Glossary', 'Glossary'), ('Policy', 'Policy')], max_length=200)),
27+
('history_id', models.AutoField(primary_key=True, serialize=False)),
28+
('history_date', models.DateTimeField(db_index=True)),
29+
('history_change_reason', models.CharField(max_length=100, null=True)),
30+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
31+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
32+
('institution', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='general.institution')),
33+
],
34+
options={
35+
'verbose_name': 'historical document file',
36+
'verbose_name_plural': 'historical document files',
37+
'ordering': ('-history_date', '-history_id'),
38+
'get_latest_by': ('history_date', 'history_id'),
39+
},
40+
bases=(simple_history.models.HistoricalChanges, models.Model),
41+
),
42+
migrations.CreateModel(
43+
name='HistoricalInstitution',
44+
fields=[
45+
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
46+
('name', models.CharField(db_index=True, max_length=200)),
47+
('abbreviation', models.CharField(max_length=200)),
48+
('url', models.URLField(blank=True, verbose_name='URL')),
49+
('email', models.EmailField(blank=True, max_length=200)),
50+
('logo', models.TextField(blank=True, max_length=100)),
51+
('history_id', models.AutoField(primary_key=True, serialize=False)),
52+
('history_date', models.DateTimeField(db_index=True)),
53+
('history_change_reason', models.CharField(max_length=100, null=True)),
54+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
55+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
56+
],
57+
options={
58+
'verbose_name': 'historical institution',
59+
'verbose_name_plural': 'historical institutions',
60+
'ordering': ('-history_date', '-history_id'),
61+
'get_latest_by': ('history_date', 'history_id'),
62+
},
63+
bases=(simple_history.models.HistoricalChanges, models.Model),
64+
),
65+
migrations.CreateModel(
66+
name='HistoricalLanguage',
67+
fields=[
68+
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
69+
('name', models.CharField(db_index=True, max_length=150)),
70+
('iso_code', models.CharField(db_index=True, help_text='The 2 or 3 letter code from ISO 639.', max_length=50, verbose_name='ISO code')),
71+
('history_id', models.AutoField(primary_key=True, serialize=False)),
72+
('history_date', models.DateTimeField(db_index=True)),
73+
('history_change_reason', models.CharField(max_length=100, null=True)),
74+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
75+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
76+
],
77+
options={
78+
'verbose_name': 'historical language',
79+
'verbose_name_plural': 'historical languages',
80+
'ordering': ('-history_date', '-history_id'),
81+
'get_latest_by': ('history_date', 'history_id'),
82+
},
83+
bases=(simple_history.models.HistoricalChanges, models.Model),
84+
),
85+
migrations.CreateModel(
86+
name='HistoricalProject',
87+
fields=[
88+
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
89+
('name', models.CharField(max_length=200)),
90+
('url', models.URLField(blank=True, verbose_name='URL')),
91+
('logo', models.TextField(blank=True, max_length=100)),
92+
('start_date', models.DateField(blank=True, null=True)),
93+
('end_date', models.DateField(blank=True, null=True)),
94+
('history_id', models.AutoField(primary_key=True, serialize=False)),
95+
('history_date', models.DateTimeField(db_index=True)),
96+
('history_change_reason', models.CharField(max_length=100, null=True)),
97+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
98+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
99+
('institution', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='general.institution', verbose_name='institution')),
100+
],
101+
options={
102+
'verbose_name': 'historical project',
103+
'verbose_name_plural': 'historical projects',
104+
'ordering': ('-history_date', '-history_id'),
105+
'get_latest_by': ('history_date', 'history_id'),
106+
},
107+
bases=(simple_history.models.HistoricalChanges, models.Model),
108+
),
109+
migrations.CreateModel(
110+
name='HistoricalSubject',
111+
fields=[
112+
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
113+
('name', models.CharField(db_index=True, max_length=150)),
114+
('history_id', models.AutoField(primary_key=True, serialize=False)),
115+
('history_date', models.DateTimeField(db_index=True)),
116+
('history_change_reason', models.CharField(max_length=100, null=True)),
117+
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
118+
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
119+
],
120+
options={
121+
'verbose_name': 'historical subject',
122+
'verbose_name_plural': 'historical subjects',
123+
'ordering': ('-history_date', '-history_id'),
124+
'get_latest_by': ('history_date', 'history_id'),
125+
},
126+
bases=(simple_history.models.HistoricalChanges, models.Model),
127+
),
128+
]

app/general/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from django.core.validators import FileExtensionValidator
22
from django.db import models
3+
from simple_history.models import HistoricalRecords
34

45

56
class Project(models.Model):
@@ -14,6 +15,9 @@ class Project(models.Model):
1415
subjects = models.ManyToManyField("Subject", blank=True)
1516
languages = models.ManyToManyField("Language", blank=True)
1617

18+
# added simple historical records to the model
19+
history = HistoricalRecords()
20+
1721
def __str__(self):
1822
return self.name
1923

@@ -25,6 +29,9 @@ class Institution(models.Model):
2529
email = models.EmailField(max_length=200, blank=True)
2630
logo = models.FileField(upload_to="logos/", blank=True)
2731

32+
# added simple historical records to the model
33+
history = HistoricalRecords()
34+
2835
def __str__(self):
2936
return f"{self.name} ({self.abbreviation})"
3037

@@ -38,13 +45,19 @@ class Language(models.Model):
3845
verbose_name="ISO code",
3946
)
4047

48+
# added simple historical records to the model
49+
history = HistoricalRecords()
50+
4151
def __str__(self):
4252
return self.name
4353

4454

4555
class Subject(models.Model):
4656
name = models.CharField(max_length=150, unique=True)
4757

58+
# added simple historical records to the model
59+
history = HistoricalRecords()
60+
4861
def __str__(self):
4962
return self.name
5063

@@ -98,5 +111,8 @@ class DocumentFile(models.Model):
98111
subjects = models.ManyToManyField("Subject", blank=True)
99112
languages = models.ManyToManyField("Language", blank=True)
100113

114+
# added simple historical records to the model
115+
history = HistoricalRecords()
116+
101117
def __str__(self):
102118
return self.title

app/general/tests/test_document_file.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,18 @@ def test_document_str_representation(self): # Test __str__ method
4444
def test_document_available_by_default(self): # Test default value
4545
self.assertTrue(self.document.available)
4646

47+
def test_history_records_creation(self):
48+
self.assertEqual(self.document.history.count(), 1)
49+
self.assertEqual(self.document.history.first().title, "Some document")
50+
self.assertEqual(self.document.history.first().url, "https://example.com")
51+
self.assertEqual(self.document.history.first().uploaded_file, "documents/example.pdf")
52+
self.assertEqual(self.document.history.first().license, "MIT")
53+
self.assertEqual(self.document.history.first().mime_type, "pdf")
54+
self.assertEqual(self.document.history.first().document_type, "Glossary")
55+
self.assertEqual(self.document.institution, self.institution)
56+
self.assertIn(self.subject, self.document.subjects.all())
57+
self.assertIn(self.language, self.document.languages.all())
58+
4759
def tearDown(self):
4860
if self.document.uploaded_file:
4961
self.document.uploaded_file.delete()

app/general/tests/tests_institution.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from django.test import TestCase
44

5-
from general.models import Institution, Project
5+
from general.models import Institution
66

77

88
class TestInstitution(TestCase):
@@ -34,6 +34,14 @@ def test_institution_email(self):
3434
def test_institution_logo(self):
3535
self.assertEqual(self.institution.logo, "testuni.png")
3636

37+
def test_history_records_creation(self):
38+
self.assertEqual(self.institution.history.count(), 1)
39+
self.assertEqual(self.institution.history.first().name, "Test University")
40+
self.assertEqual(self.institution.history.first().abbreviation, "tu")
41+
self.assertEqual(self.institution.history.first().url, "http://www.testuni.com")
42+
self.assertEqual(self.institution.history.first().email, "[email protected]")
43+
self.assertEqual(self.institution.history.first().logo, "testuni.png")
44+
3745

3846
if __name__ == "__main__":
3947
unittest.main()

app/general/tests/tests_language.py

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@
22

33
from django.test import TestCase
44

5-
from general.models import Subject
5+
from general.models import Language
66

77

8-
class TestSubject(TestCase):
8+
class TestLanguage(TestCase):
99
def setUp(self):
10-
self.subject = Subject.objects.create(name="Maths")
11-
self.subject2 = Subject.objects.create(name="Science")
10+
self.language = Language.objects.create(name="English", iso_code="EN")
11+
self.language2 = Language.objects.create(name="Afrikaans", iso_code="AF")
1212

1313
def test_subject_creation(self):
14-
self.assertEqual(self.subject.name, "Maths")
15-
self.assertEqual(self.subject.__str__(), "Maths")
14+
self.assertEqual(self.language.name, "English")
15+
self.assertEqual(self.language.iso_code, "EN")
16+
17+
self.assertEqual(self.language2.name, "Afrikaans")
18+
self.assertEqual(self.language2.iso_code, "AF")
1619

1720
def test_subject_name_uniqueness(self):
18-
duplicate_subject = Subject(name="Maths")
19-
self.assertRaises(Exception, duplicate_subject.save)
21+
with self.assertRaises(Exception):
22+
Language.objects.create(name="English")
23+
24+
#
25+
def test_history_records_creation(self):
26+
self.assertEqual(self.language.history.count(), 1)
27+
self.assertEqual(self.language.history.first().name, "English")
28+
self.assertEqual(self.language.history.first().iso_code, "EN")
2029

2130

2231
if __name__ == "__main__":

app/general/tests/tests_projects.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ def test_project_language(self):
6161
def test_str(self):
6262
self.assertEqual(str(self.project), "Test Project")
6363

64+
def test_history_records_creation(self):
65+
self.assertEqual(self.project.history.count(), 1)
66+
self.assertEqual(self.project.history.first().name, "Test Project")
67+
self.assertEqual(self.project.history.first().url, "http://test.com")
68+
self.assertEqual(self.project.history.first().logo, "http://test.com/logo.png")
69+
self.assertEqual(self.project.history.first().start_date.strftime("%Y-%m-%d"), "2023-01-01")
70+
self.assertEqual(self.project.history.first().end_date.strftime("%Y-%m-%d"), "2023-12-31")
71+
self.assertEqual(self.project.history.first().institution, self.institution)
72+
6473

6574
if __name__ == "__main__":
6675
unittest.main()

app/general/tests/tests_subject.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ def test_subject_name_uniqueness(self):
1818
with self.assertRaises(Exception):
1919
Subject.objects.create(name="Mathematics")
2020

21+
def test_history_records_creation(self):
22+
self.assertEqual(self.subject1.history.count(), 1)
23+
self.assertEqual(self.subject1.history.first().name, "Mathematics")
24+
2125

2226
if __name__ == "__main__":
2327
unittest.main()

0 commit comments

Comments
 (0)