feat: contact form and more edits
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user