Basic Examples
==============
This page provides complete, working examples of Martor integration. You can copy and adapt these examples for your own projects.
Simple Blog Application
-----------------------
Here's a complete blog application using Martor:
Models
~~~~~~
.. code-block:: python
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse
from django.utils.text import slugify
from martor.models import MartorField
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(unique=True)
description = models.TextField(blank=True)
class Meta:
verbose_name_plural = "Categories"
def __str__(self):
return self.name
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True, blank=True)
author = models.ForeignKey(User, on_delete=models.CASCADE)
category = models.ForeignKey(Category, on_delete=models.CASCADE)
# Martor field for rich content editing
content = MartorField(
verbose_name="Post Content",
help_text="Write your blog post using Markdown syntax"
)
excerpt = models.TextField(
max_length=500,
help_text="Brief description of the post"
)
# Publishing
published = models.BooleanField(default=False)
featured = models.BooleanField(default=False)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse('blog:post_detail', kwargs={'slug': self.slug})
Forms
~~~~~
.. code-block:: python
# blog/forms.py
from django import forms
from django.core.exceptions import ValidationError
from martor.fields import MartorFormField
from .models import Post, Category
class PostForm(forms.ModelForm):
content = MartorFormField(
help_text="Use Markdown syntax for formatting. You can upload images and use the toolbar for quick formatting."
)
class Meta:
model = Post
fields = ['title', 'category', 'excerpt', 'content', 'published', 'featured']
widgets = {
'title': forms.TextInput(attrs={'class': 'form-control'}),
'excerpt': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'category': forms.Select(attrs={'class': 'form-control'}),
}
def clean_content(self):
content = self.cleaned_data['content']
# Ensure minimum word count
word_count = len(content.split())
if word_count < 50:
raise ValidationError(f"Post content must be at least 50 words. Current: {word_count}")
return content
def clean_title(self):
title = self.cleaned_data['title']
# Check for unique title (excluding current instance)
qs = Post.objects.filter(title__iexact=title)
if self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError("A post with this title already exists.")
return title
Views
~~~~~
.. code-block:: python
# blog/views.py
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from django.core.paginator import Paginator
from django.db.models import Q
from .models import Post, Category
from .forms import PostForm
def post_list(request):
posts = Post.objects.filter(published=True).select_related('author', 'category')
# Search functionality
query = request.GET.get('q')
if query:
posts = posts.filter(
Q(title__icontains=query) |
Q(content__icontains=query) |
Q(excerpt__icontains=query)
)
# Category filter
category_slug = request.GET.get('category')
if category_slug:
posts = posts.filter(category__slug=category_slug)
# Pagination
paginator = Paginator(posts, 10)
page_number = request.GET.get('page')
page_obj = paginator.get_page(page_number)
categories = Category.objects.all()
context = {
'page_obj': page_obj,
'categories': categories,
'query': query,
'current_category': category_slug,
}
return render(request, 'blog/post_list.html', context)
def post_detail(request, slug):
post = get_object_or_404(Post, slug=slug, published=True)
# Get related posts
related_posts = Post.objects.filter(
category=post.category,
published=True
).exclude(id=post.id)[:3]
context = {
'post': post,
'related_posts': related_posts,
}
return render(request, 'blog/post_detail.html', context)
@login_required
def post_create(request):
if request.method == 'POST':
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
messages.success(request, 'Post created successfully!')
return redirect(post.get_absolute_url())
else:
form = PostForm()
context = {'form': form, 'title': 'Create New Post'}
return render(request, 'blog/post_form.html', context)
@login_required
def post_edit(request, slug):
post = get_object_or_404(Post, slug=slug, author=request.user)
if request.method == 'POST':
form = PostForm(request.POST, instance=post)
if form.is_valid():
form.save()
messages.success(request, 'Post updated successfully!')
return redirect(post.get_absolute_url())
else:
form = PostForm(instance=post)
context = {'form': form, 'post': post, 'title': 'Edit Post'}
return render(request, 'blog/post_form.html', context)
Templates
~~~~~~~~~
**Base Template:**
.. code-block:: html
{% block title %}Blog{% endblock %}
{% block css %}{% endblock %}
{% if messages %}
{% for message in messages %}
{{ message }}
{% endfor %}
{% endif %}
{% block content %}{% endblock %}
{% block js %}{% endblock %}
**Post Form Template:**
.. code-block:: html
{% extends "blog/base.html" %}
{% load static %}
{% block title %}{{ title }}{% endblock %}
{% block css %}
{% endblock %}
{% block content %}
{% endblock %}
{% block js %}
{% endblock %}
**Post Detail Template:**
.. code-block:: html
{% extends "blog/base.html" %}
{% load static %}
{% load martortags %}
{% block title %}{{ post.title }}{% endblock %}
{% block css %}
{% endblock %}
{% block content %}
{{ post.content|safe_markdown }}
{% endblock %}
{% block js %}
{% endblock %}
URLs
~~~~
.. code-block:: python
# blog/urls.py
from django.urls import path
from . import views
app_name = 'blog'
urlpatterns = [
path('', views.post_list, name='post_list'),
path('create/', views.post_create, name='post_create'),
path('/', views.post_detail, name='post_detail'),
path('/edit/', views.post_edit, name='post_edit'),
]
**Project URLs:**
.. code-block:: python
# myproject/urls.py
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
urlpatterns = [
path('admin/', admin.site.urls),
path('martor/', include('martor.urls')), # Required for Martor
path('blog/', include('blog.urls')),
path('', include('blog.urls')), # Default to blog
]
# Serve static files in development
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Admin Configuration
~~~~~~~~~~~~~~~~~~~
.. code-block:: python
# blog/admin.py
from django.contrib import admin
from django.db import models
from martor.widgets import AdminMartorWidget
from .models import Category, Post
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'slug']
prepopulated_fields = {'slug': ('name',)}
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ['title', 'author', 'category', 'published', 'featured', 'created_at']
list_filter = ['published', 'featured', 'category', 'created_at']
search_fields = ['title', 'content', 'excerpt']
prepopulated_fields = {'slug': ('title',)}
# Use Martor widget for content field
formfield_overrides = {
models.TextField: {'widget': AdminMartorWidget},
}
fieldsets = (
('Basic Information', {
'fields': ('title', 'slug', 'author', 'category')
}),
('Content', {
'fields': ('excerpt', 'content'),
'classes': ('wide',),
}),
('Publishing', {
'fields': ('published', 'featured'),
}),
)
def save_model(self, request, obj, form, change):
if not change: # Creating new post
obj.author = request.user
super().save_model(request, obj, form, change)
Settings Configuration
~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
# settings.py
import os
# ... other settings ...
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
# Third party apps
'martor',
# Local apps
'blog',
]
# Static files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')
# Media files
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# Martor settings
MARTOR_THEME = 'bootstrap'
MARTOR_ENABLE_CONFIGS = {
'emoji': 'true',
'imgur': 'true',
'mention': 'false',
'jquery': 'true',
'living': 'true',
'spellcheck': 'false',
'hljs': 'true',
}
# Required for AJAX functionality
CSRF_COOKIE_HTTPONLY = False
# Optional: imgur configuration for image uploads
MARTOR_IMGUR_CLIENT_ID = 'your-imgur-client-id'
MARTOR_IMGUR_API_KEY = 'your-imgur-api-key'
Running the Example
~~~~~~~~~~~~~~~~~~~
1. **Create and run migrations:**
.. code-block:: bash
python manage.py makemigrations blog
python manage.py migrate
2. **Create a superuser:**
.. code-block:: bash
python manage.py createsuperuser
3. **Collect static files:**
.. code-block:: bash
python manage.py collectstatic
4. **Run the development server:**
.. code-block:: bash
python manage.py runserver
5. **Access the application:**
- Visit http://127.0.0.1:8000/ for the blog
- Visit http://127.0.0.1:8000/admin/ for the admin interface
- Visit http://127.0.0.1:8000/blog/create/ to create posts (requires login)
Simple Contact Form Example
---------------------------
Here's a simpler example using Martor in a contact form:
.. code-block:: python
# contact/forms.py
from django import forms
from martor.fields import MartorFormField
class ContactForm(forms.Form):
name = forms.CharField(max_length=100)
email = forms.EmailField()
subject = forms.CharField(max_length=200)
message = MartorFormField(
label="Your Message",
help_text="You can use Markdown formatting in your message",
widget=forms.Textarea(attrs={
'rows': 10,
'placeholder': 'Write your message here...'
})
)
# contact/views.py
from django.shortcuts import render, redirect
from django.contrib import messages
from django.core.mail import send_mail
from django.conf import settings
from .forms import ContactForm
def contact_view(request):
if request.method == 'POST':
form = ContactForm(request.POST)
if form.is_valid():
# Send email or save to database
# ... email logic ...
messages.success(request, 'Thank you for your message!')
return redirect('contact')
else:
form = ContactForm()
return render(request, 'contact/contact.html', {'form': form})
User Documentation Example
--------------------------
For documentation or knowledge base applications:
.. code-block:: python
# docs/models.py
from django.db import models
from martor.models import MartorField
class DocumentSection(models.Model):
name = models.CharField(max_length=100)
order = models.PositiveIntegerField(default=0)
class Meta:
ordering = ['order']
def __str__(self):
return self.name
class Document(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(unique=True)
section = models.ForeignKey(DocumentSection, on_delete=models.CASCADE)
content = MartorField()
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
version = models.CharField(max_length=10, default='1.0')
# Access control
is_public = models.BooleanField(default=True)
class Meta:
ordering = ['section__order', 'title']
def __str__(self):
return self.title
This provides a solid foundation for building documentation websites with Martor.
Next Steps
----------
* :doc:`../usage/models` - Learn more about using Martor with models
* :doc:`../usage/forms` - Advanced form techniques
* :doc:`custom-uploader` - Set up custom image uploading
* :doc:`../customization` - Customize Martor's appearance and behavior