[paperwork] Use semver to determine if if certs are latest or outdated

"Latest" = cert with the highest version number
"Current" = compatible version, so not latest, but still valid
"Outdated" = major version < major version of "latest"
This commit is contained in:
Adam Goldsmith 2022-11-07 13:49:39 -05:00
parent 9bf33f9b50
commit 3c73e9fa46
6 changed files with 104 additions and 10 deletions

View File

@ -18,14 +18,28 @@ class CertificationVersionInline(admin.TabularInline):
model = CertificationVersion
extra = 1
readonly_fields = (
"semantic_version",
"is_latest",
"is_current",
)
@admin.register(CertificationDefinition)
class CertificationDefinitionAdmin(admin.ModelAdmin):
search_fields = ["certification_name"]
list_display = ["certification_name", "department"]
list_display = [
"certification_name",
"department",
"latest_version__version",
]
list_filter = ["department"]
inlines = [CertificationVersionInline]
@admin.display(description="Latest Version")
def latest_version__version(self, obj):
return obj.latest_version().version
@admin.register(Certification)
class CertificationAdmin(admin.ModelAdmin):
@ -50,6 +64,10 @@ class CertificationAdmin(admin.ModelAdmin):
def certification_version_version(self, obj):
return obj.certification_version.version
@admin.display(description="Current", boolean=True)
def certification_version__is_current(self, obj):
return obj.certification_version.is_current()
@admin.display(
description="Department",
ordering="certification_version__definition__department",
@ -61,6 +79,7 @@ class CertificationAdmin(admin.ModelAdmin):
"certification_name",
"name",
"certification_version_version",
"certification_version__is_current",
"certification_department",
"date",
"shop_lead_notified",

View File

@ -1,7 +1,13 @@
from django.db import models
import re
import semver
from membershipworks.models import Member
VALID_SEMVER_PATTERN = re.compile(
r"(?P<semver>\d+\.\d+\.\d+) - (?P<approvaldate>\d{4}-\d{2}-\d{2})"
)
class CmsRedRiverVeteransScholarship(models.Model):
serial = models.AutoField(primary_key=True)
@ -60,6 +66,11 @@ class CertificationDefinition(models.Model):
db_table = "Certification Definitions"
ordering = ("certification_name", "department")
def latest_version(self) -> "CertificationVersion":
all_versions = CertificationVersion.objects.filter(definition=self)
return max(all_versions, key=lambda version: version.semantic_version())
class CertificationVersion(models.Model):
definition = models.ForeignKey(
@ -79,6 +90,35 @@ class CertificationVersion(models.Model):
)
]
def semantic_version(self) -> semver.VersionInfo:
if self.version is None:
return "0.0.0-none"
elif self.version == "MembershipWorks Label":
return semver.parse_version_info("0.0.1-mw-label")
elif match := VALID_SEMVER_PATTERN.match(self.version):
return semver.parse_version_info(
f'{match["semver"]}+{match["approvaldate"]}'
)
else:
return semver.parse_version_info(
"0.0.1-" + re.sub(r"[^.a-zA-Z0-9]", "-", self.version)
)
def is_latest(self) -> bool:
return self.definition.latest_version() == self
is_latest.boolean = True
def is_current(self) -> bool:
"""Returns true if this version compatible with the latest version"""
# TODO: should do a more correct comparison than just major version
return (
self.definition.latest_version().semantic_version().major
== self.semantic_version().major
)
is_current.boolean = True
class Certification(models.Model):
number = models.AutoField(db_column="Number", primary_key=True)

View File

@ -1,7 +1,15 @@
{% extends "base.dj.html" %}
{% block content %}
<p>You have been issued the following Claremont MakerSpace Member Certifications:</p>
<div class="d-flex flex-wrap justify-content-between">
<p>You have been issued the following Claremont MakerSpace Member Certifications:</p>
<div class="form-check form-switch">
<label class="form-check-label" for="flexSwitchCheckDefault">
<input class="form-check-input" type="checkbox" role="switch" {{ show_outdated|yesno:"checked," }} onchange="window.location.href='?show_outdated={{ show_outdated|yesno:"false,true" }}'">
Show Outdated
</label>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-striped table-hover" border="1">
<thead>
@ -15,13 +23,22 @@
</thead>
<tbody>
{% for certification in certifications %}
<tr>
<td>{{ certification.certification_version.definition.certification_name }}</td>
<td>{{ certification.certification_version.version }}</td>
<td>{{ certification.certification_version.definition.department }}</td>
<td>{{ certification.certified_by }}</td>
<td>{{ certification.date }}</td>
</tr>
{% with current=certification.certification_version.is_current %}
{% if current or show_outdated %}
<tr {% if not current %} class="table-warning"{% endif %}>
<td {% if not current %} class="text-decoration-line-through"{% endif %}>
{{ certification.certification_version.definition.certification_name }}
</td>
<td>
{{ certification.certification_version.version }}
{% if not current %}<span class="fw-bold">OUTDATED</span>{% endif %}
</td>
<td>{{ certification.certification_version.definition.department }}</td>
<td>{{ certification.certified_by }}</td>
<td>{{ certification.date }}</td>
</tr>
{% endif %}
{% endwith %}
{% endfor %}
</tbody>
</table>

View File

@ -17,6 +17,13 @@ class MemberCertificationListView(ListView):
template_name = "paperwork/member_certifications.dj.html"
context_object_name = "certifications"
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["show_outdated"] = (
self.request.GET.get("show_outdated", "false").lower() == "true"
)
return context
def get_queryset(self):
self.member = get_object_or_404(Member, uid=self.kwargs["uid"])
return Certification.objects.filter(member=self.member)

View File

@ -431,6 +431,12 @@ dependencies = [
"urllib3<1.27,>=1.21.1",
]
[[package]]
name = "semver"
version = "2.13.0"
requires_python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
summary = "Python helper for Semantic Versioning (http://semver.org/)"
[[package]]
name = "setuptools"
version = "60.10.0"
@ -542,7 +548,7 @@ summary = "Zopfli module for python"
[metadata]
lock_version = "4.0"
content_hash = "sha256:5767d12ac20bb72b5eeb24e23287083e1feba01fa982b88ffef1b3258a223223"
content_hash = "sha256:feaea70b44d3d723d770ae3ffa4d418a74ae9e5559c25f571f10a7b9b45def9f"
[metadata.files]
"asgiref 3.5.2" = [
@ -1106,6 +1112,10 @@ content_hash = "sha256:5767d12ac20bb72b5eeb24e23287083e1feba01fa982b88ffef1b3258
{url = "https://files.pythonhosted.org/packages/a5/61/a867851fd5ab77277495a8709ddda0861b28163c4613b011bc00228cc724/requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"},
{url = "https://files.pythonhosted.org/packages/ca/91/6d9b8ccacd0412c08820f72cebaa4f0c0441b5cda699c90f618b6f8a1b42/requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"},
]
"semver 2.13.0" = [
{url = "https://files.pythonhosted.org/packages/0b/70/b84f9944a03964a88031ef6ac219b6c91e8ba2f373362329d8770ef36f02/semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"},
{url = "https://files.pythonhosted.org/packages/31/a9/b61190916030ee9af83de342e101f192bbb436c59be20a4cb0cdb7256ece/semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"},
]
"setuptools 60.10.0" = [
{url = "https://files.pythonhosted.org/packages/7c/5b/3d92b9f0f7ca1645cba48c080b54fe7d8b1033a4e5720091d1631c4266db/setuptools-60.10.0-py3-none-any.whl", hash = "sha256:782ef48d58982ddb49920c11a0c5c9c0b02e7d7d1c2ad0aa44e1a1e133051c96"},
{url = "https://files.pythonhosted.org/packages/af/e8/894c71e914dfbe01276a42dfad40025cd96119f2eefc39c554b6e8b9df86/setuptools-60.10.0.tar.gz", hash = "sha256:6599055eeb23bfef457d5605d33a4d68804266e6cb430b0fb12417c5efeae36c"},

View File

@ -20,6 +20,7 @@ dependencies = [
"django-autocomplete-light~=3.9",
"weasyprint~=57.1",
"requests~=2.27",
"semver~=2.13",
]
requires-python = ">=3.9"