feat: contact form and more edits

This commit is contained in:
mohamad
2026-05-04 13:54:45 +03:30
parent 3c10f5c5ed
commit 744ef69942
49 changed files with 1073 additions and 179 deletions
+28 -28
View File
@@ -6,14 +6,14 @@ from apps.products.models import Article, ArticleSection, MainProduct, SubProduc
MAIN_PRODUCTS = [
{
"name": "ViSERA",
"slug": "visera",
"name": "Radiuma",
"slug": "radiuma",
"short_description": "Visualized & Standardized Environment for Radiomics Analysis",
"description": (
"ViSERA is a free, open-source software specialized for visualization, "
"Radiuma is a free, open-source software specialized for visualization, "
"processing, segmentation, registration, fusion and analysis of medical and "
"biomedical images, including radiomics and machine learning analysis. "
"ViSERA is a major, entirely-revamped upgrade to the original SERA "
"Radiuma is a major, entirely-revamped upgrade to the original SERA "
"(Matlab-based), now built on Python for broader accessibility and community "
"contribution. It enables standardized and reproducible radiomic feature "
"extraction in compliance with the Image Biomarker Standardization Initiative "
@@ -28,7 +28,7 @@ MAIN_PRODUCTS = [
"description": (
"Advanced image processing capabilities including standardized filtering "
"techniques compliant with IBSI 2.0, image registration, fusion, and "
"Standardized Uptake Value (SUV) conversion. ViSERA employs popular "
"Standardized Uptake Value (SUV) conversion. Radiuma employs popular "
"image processing algorithms to create end-to-end standardized workflows "
"for consistent, reproducible research outcomes."
),
@@ -37,7 +37,7 @@ MAIN_PRODUCTS = [
{
"title": "Image Filtering Techniques",
"description": (
"ViSERA implements a comprehensive set of image filtering techniques "
"Radiuma implements a comprehensive set of image filtering techniques "
"fully standardized against the Image Biomarker Standardization "
"Initiative (IBSI) phase 2. These filters enable reproducible "
"preprocessing across institutions and studies."
@@ -50,13 +50,13 @@ MAIN_PRODUCTS = [
"value": "Mean, Gaussian, Laplacian of Gaussian (LoG), Laws kernels, Gabor, Wavelets (PyWavelets), Log-Sigma",
"order": 2,
},
{"title": "Author", "value": "Tecvico Corp R&D Team", "order": 3},
{"title": "Author", "value": "Radiuma R&D Team", "order": 3},
],
},
{
"title": "Image Registration & Fusion",
"description": (
"ViSERA provides robust image registration and fusion methods, "
"Radiuma provides robust image registration and fusion methods, "
"enabling multi-modal image alignment for PET/CT, PET/MRI, and "
"other combined modality studies. Standardized Uptake Value (SUV) "
"conversion is also supported."
@@ -75,7 +75,7 @@ MAIN_PRODUCTS = [
"slug": "radiomics-features",
"short_description": "IBSI 1.0 compliant handcrafted radiomic feature extraction",
"description": (
"ViSERA provides comprehensive handcrafted radiomic feature extraction "
"Radiuma provides comprehensive handcrafted radiomic feature extraction "
"fully standardized by the Image Biomarker Standardization Initiative "
"(IBSI 1.0). Features are computed from segmented regions of interest "
"across multiple image modalities, enabling reproducible quantitative "
@@ -86,7 +86,7 @@ MAIN_PRODUCTS = [
{
"title": "IBSI Compliant Feature Extraction",
"description": (
"ViSERA computes a comprehensive set of radiomic features "
"Radiuma computes a comprehensive set of radiomic features "
"covering all IBSI 1.0 feature classes. Features are extracted "
"from segmented Regions of Interest (ROIs) and are fully "
"reproducible across different platforms and institutions."
@@ -110,7 +110,7 @@ MAIN_PRODUCTS = [
"slug": "medical-image-visualization",
"short_description": "Professional multi-modality medical image viewer",
"description": (
"ViSERA includes a professional medical image viewer that supports "
"Radiuma includes a professional medical image viewer that supports "
"multiple imaging modalities and file formats. The viewer provides "
"comfortable, intuitive controls for slice navigation, windowing, "
"zoom, and annotation, suitable for radiation oncologists, radiologists, "
@@ -140,7 +140,7 @@ MAIN_PRODUCTS = [
"slug": "format-conversion",
"short_description": "Professional converter for medical imaging file formats",
"description": (
"ViSERA provides a professional image format converter supporting all "
"Radiuma provides a professional image format converter supporting all "
"major medical imaging standards. Seamlessly convert between DICOM, "
"NIFTI, NRRD, MHA, and other formats without loss of spatial metadata "
"or patient information integrity."
@@ -169,7 +169,7 @@ MAIN_PRODUCTS = [
"slug": "workflow-management",
"short_description": "Reproducible research workflow creation and sharing",
"description": (
"ViSERA's workflow management system allows researchers to design, save, "
"Radiuma's workflow management system allows researchers to design, save, "
"share, and reuse analysis pipelines. Workflows connect individual "
"processing steps — from image loading and preprocessing to feature "
"extraction and machine learning — into reproducible, shareable sequences "
@@ -199,9 +199,9 @@ MAIN_PRODUCTS = [
FAQ_ENTRIES = [
{
"question": "What is the ViSERA license?",
"question": "What is the Radiuma license?",
"answer": (
"ViSERA is free and open-source for research purposes.\n\n"
"Radiuma is free and open-source for research purposes.\n\n"
"License: CC BY-NC-SA (Creative Commons Attribution-NonCommercial-ShareAlike). "
"This means you may use, share, and adapt the software for non-commercial "
"research purposes, provided you give appropriate credit and distribute "
@@ -210,21 +210,21 @@ FAQ_ENTRIES = [
"order": 1,
},
{
"question": "How do I cite ViSERA in my research?",
"question": "How do I cite Radiuma in my research?",
"answer": (
"Please cite the following reference if you publish results obtained with "
"the help of ViSERA:\n\n"
"the help of Radiuma:\n\n"
"M. R. Salmanpour, I. Shiri, M. Hosseinzadeh, H. Zaidi, S. Ashrafinia, "
"M. Oveisi, A. Rahmim. ViSERA: Visualized & Standardized Environment for "
"M. Oveisi, A. Rahmim. Radiuma: Visualized & Standardized Environment for "
"Radiomics Analysis — A Shareable, Executable, and Reproducible Workflow "
"Generator. Proc. IEEE Medical Imaging Conference, 2023."
),
"order": 2,
},
{
"question": "Which operating systems does ViSERA support?",
"question": "Which operating systems does Radiuma support?",
"answer": (
"ViSERA currently fully supports Windows 10 and above (64-bit). "
"Radiuma currently fully supports Windows 10 and above (64-bit). "
"New versions to support macOS and Linux systems are under active development "
"and coming soon. Follow our Discord or check the Downloads page for updates."
),
@@ -241,9 +241,9 @@ FAQ_ENTRIES = [
"order": 4,
},
{
"question": "Is ViSERA suitable for clinical use?",
"question": "Is Radiuma suitable for clinical use?",
"answer": (
"ViSERA is designed and intended exclusively for research purposes. "
"Radiuma is designed and intended exclusively for research purposes. "
"It is not certified for clinical diagnostic use. Always consult with "
"qualified medical professionals for clinical decisions."
),
@@ -252,7 +252,7 @@ FAQ_ENTRIES = [
{
"question": "Where can I get support or report issues?",
"answer": (
"Support is available via email at support@tecvico.com and through our "
"Support is available via email at support@visera.ca and through our "
"community Discord server. For bug reports and feature requests, please "
"use the Discord forum or contact us directly by email."
),
@@ -262,16 +262,16 @@ FAQ_ENTRIES = [
DOWNLOAD_ITEMS = [
{
"name": "ViSERA Desktop",
"name": "Radiuma Desktop",
"platform": "windows",
"version": "1.0.0",
"download_url": "https://github.com/tecvico/visera/releases/latest/download/ViSERA-Setup.exe",
"download_url": "https://github.com/radiuma/radiuma/releases/latest/download/Radiuma-Setup.exe",
"description": "Windows 10 and above (64-bit). Installer package.",
"is_active": True,
"order": 1,
},
{
"name": "ViSERA Desktop",
"name": "Radiuma Desktop",
"platform": "macos",
"version": "Coming Soon",
"download_url": "#",
@@ -280,7 +280,7 @@ DOWNLOAD_ITEMS = [
"order": 2,
},
{
"name": "ViSERA Desktop",
"name": "Radiuma Desktop",
"platform": "linux",
"version": "Coming Soon",
"download_url": "#",
@@ -292,7 +292,7 @@ DOWNLOAD_ITEMS = [
class Command(BaseCommand):
help = "Seed the database with initial Tecvico / ViSERA content from visera.ca"
help = "Seed the database with initial Radiuma / Radiuma content from visera.ca"
def add_arguments(self, parser):
parser.add_argument(
+14 -1
View File
@@ -1,6 +1,19 @@
from django.contrib import admin
from .models import DownloadItem, FAQEntry
from .models import ContactSubmission, DownloadItem, FAQEntry
@admin.register(ContactSubmission)
class ContactSubmissionAdmin(admin.ModelAdmin):
list_display = ("name", "title", "email", "submitted_at", "is_read")
list_filter = ("is_read", "submitted_at")
search_fields = ("name", "title", "description", "email")
list_editable = ("is_read",)
readonly_fields = ("name", "title", "description", "email", "submitted_at")
fieldsets = (
(None, {"fields": ("name", "title", "email", "description")}),
("Meta", {"fields": ("submitted_at", "is_read")}),
)
@admin.register(FAQEntry)
+24
View File
@@ -0,0 +1,24 @@
from django import forms
from .models import ContactSubmission
class ContactForm(forms.ModelForm):
class Meta:
model = ContactSubmission
fields = ["name", "title", "description", "email"]
widgets = {
"name": forms.TextInput(
attrs={"placeholder": "Your full name", "autocomplete": "name"}
),
"title": forms.TextInput(attrs={"placeholder": "Subject / topic"}),
"description": forms.Textarea(
attrs={"placeholder": "Write your message here…", "rows": 5}
),
"email": forms.EmailInput(
attrs={
"placeholder": "your@email.com (optional)",
"autocomplete": "email",
}
),
}
@@ -0,0 +1,30 @@
# Generated by Django 5.0.2 on 2026-05-04 09:13
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('pages', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ContactSubmission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('title', models.CharField(max_length=300)),
('description', models.TextField()),
('email', models.EmailField(blank=True, max_length=254)),
('submitted_at', models.DateTimeField(auto_now_add=True)),
('is_read', models.BooleanField(default=False)),
],
options={
'verbose_name': 'Contact Submission',
'verbose_name_plural': 'Contact Submissions',
'ordering': ['-submitted_at'],
},
),
]
+17
View File
@@ -1,6 +1,23 @@
from django.db import models
class ContactSubmission(models.Model):
name = models.CharField(max_length=200)
title = models.CharField(max_length=300)
description = models.TextField()
email = models.EmailField(blank=True)
submitted_at = models.DateTimeField(auto_now_add=True)
is_read = models.BooleanField(default=False)
class Meta:
ordering = ["-submitted_at"]
verbose_name = "Contact Submission"
verbose_name_plural = "Contact Submissions"
def __str__(self):
return f"{self.name}{self.title}"
class FAQEntry(models.Model):
question = models.CharField(max_length=500)
answer = models.TextField()
+2 -2
View File
@@ -27,14 +27,14 @@ class AboutViewTest(TestCase):
class DownloadsViewTest(TestCase):
def setUp(self):
DownloadItem.objects.create(
name="ViSERA Desktop",
name="Radiuma Desktop",
platform="windows",
version="1.0",
download_url="https://example.com/windows",
is_active=True,
)
DownloadItem.objects.create(
name="ViSERA Desktop",
name="Radiuma Desktop",
platform="macos",
version="Coming Soon",
download_url="#",
+46 -2
View File
@@ -1,6 +1,12 @@
import random
from django.http import JsonResponse
from django.shortcuts import render
from django.views import View
from django.views.generic import ListView, TemplateView
from .models import DownloadItem, FAQEntry
from .forms import ContactForm
from .models import ContactSubmission, DownloadItem, FAQEntry
class HomeView(TemplateView):
@@ -36,5 +42,43 @@ class FAQView(ListView):
queryset = FAQEntry.objects.filter(is_active=True)
class ContactView(TemplateView):
class ContactView(View):
template_name = "pages/contact.html"
def _new_captcha(self, request):
a, b = random.randint(1, 9), random.randint(1, 9)
request.session["captcha_answer"] = a + b
return f"{a} + {b}"
def get(self, request, *args, **kwargs):
return render(
request,
self.template_name,
{"form": ContactForm(), "captcha_question": self._new_captcha(request)},
)
def post(self, request, *args, **kwargs):
form = ContactForm(request.POST)
expected = request.session.get("captcha_answer")
captcha_question = self._new_captcha(request)
captcha_ok = False
try:
captcha_ok = int(request.POST.get("captcha_answer", "")) == expected
except (ValueError, TypeError):
pass
if form.is_valid() and captcha_ok:
form.save()
return JsonResponse({"success": True})
errors: dict = {}
if not captcha_ok:
errors["captcha"] = ["Incorrect answer — please try again."]
errors.update(
{field: [str(e) for e in errs] for field, errs in form.errors.items()}
)
return JsonResponse(
{"success": False, "errors": errors, "captcha_question": captcha_question},
status=400,
)
+11 -11
View File
@@ -7,8 +7,8 @@ from apps.products.models import Article, ArticleSection, MainProduct, SubProduc
class ProductViewsSetup(TestCase):
def setUp(self):
self.main_product = MainProduct.objects.create(
name="ViSERA",
slug="visera",
name="Radiuma",
slug="radiuma",
short_description="Radiomics software",
description="Full description.",
)
@@ -57,17 +57,17 @@ class ProductOverviewViewTest(ProductViewsSetup):
class MainProductDetailViewTest(ProductViewsSetup):
def test_detail_returns_200(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "visera"})
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"})
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
def test_detail_uses_correct_template(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "visera"})
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"})
response = self.client.get(url)
self.assertTemplateUsed(response, "products/main_detail.html")
def test_detail_contains_product_in_context(self):
url = reverse("products:main_product_detail", kwargs={"main_slug": "visera"})
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"})
response = self.client.get(url)
self.assertEqual(response.context["main_product"], self.main_product)
@@ -79,7 +79,7 @@ class MainProductDetailViewTest(ProductViewsSetup):
def test_inactive_product_returns_404(self):
self.main_product.is_active = False
self.main_product.save()
url = reverse("products:main_product_detail", kwargs={"main_slug": "visera"})
url = reverse("products:main_product_detail", kwargs={"main_slug": "radiuma"})
response = self.client.get(url)
self.assertEqual(response.status_code, 404)
@@ -88,7 +88,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_returns_200(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "visera", "sub_slug": "image-processing"},
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@@ -96,7 +96,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_uses_correct_template(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "visera", "sub_slug": "image-processing"},
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertTemplateUsed(response, "products/sub_detail.html")
@@ -104,7 +104,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_context_keys(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "visera", "sub_slug": "image-processing"},
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertIn("main_product", response.context)
@@ -115,7 +115,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_sub_detail_articles_in_context(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "visera", "sub_slug": "image-processing"},
kwargs={"main_slug": "radiuma", "sub_slug": "image-processing"},
)
response = self.client.get(url)
self.assertIn(self.article, list(response.context["articles"]))
@@ -123,7 +123,7 @@ class SubProductDetailViewTest(ProductViewsSetup):
def test_nonexistent_sub_slug_returns_404(self):
url = reverse(
"products:sub_product_detail",
kwargs={"main_slug": "visera", "sub_slug": "does-not-exist"},
kwargs={"main_slug": "radiuma", "sub_slug": "does-not-exist"},
)
response = self.client.get(url)
self.assertEqual(response.status_code, 404)