Commit a2ba2c89 by benjaoming

Attachment plugin almost finished. Can delete and restore files and replace.…

Attachment plugin almost finished. Can delete and restore files and replace. Contains a smart obscurification feature that hides files. This way, files can have reading restrictions imposed.
parent d96da784
django-wiki
===========
*Last update: 2012-08-01*
*Last update: 2012-08-08*
Demo here:
Demo here, sign up for an account to see the notification system.
[wiki.overtag.dk](http://wiki.overtag.dk) - login to admin interface: admin/admin
[wiki.overtag.dk](http://wiki.overtag.dk)
NB!! *THIS IS A WORK IN PROGRESS*
---------------------------------
......
......@@ -2,10 +2,10 @@ Not implemented - will be ASAP
==============================
* Permission system in settings tab **Done**
* Notification system **Almost done**
* Simple user account handling: login/register etc.
* Notification system **Almost done** (email notifications)
* Simple user account handling: login/register etc. **Done**
* Implement notifications, revision log messages and user messages thoroughly
* Attachment plugin **In the making**
* Attachment plugin **Almost done** (needs to be able to add attachments from other articles.. a simple search function)
* Image plugin
* Example plugin
* Bot editing detection. Don't let anyone edit more than once every other minute.
......@@ -13,7 +13,9 @@ Not implemented - will be ASAP
* Key-value meta data
* Index views for urlpaths
* Searching
* South migrations
* South migrations **Soon**
* View source for read-only articles + locked status
* Global moderator permission **Almost done** (need to add grant form for users with *grant* permissions)
Ideas
=====
......@@ -31,4 +33,9 @@ Management script
* Cleanup deleted Image's image files
* Cleanup attachments
* Cleanup revisions + plugin revisions
* django_notify: send out email notifications
Postponed
=================
* Make dependency on django_notify optional
......@@ -86,6 +86,7 @@ TEMPLATE_CONTEXT_PROCESSORS =(
)
INSTALLED_APPS = (
'django.contrib.humanize',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
......
from django.conf.urls import patterns, include, url
from django.conf import settings
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.contrib import admin
admin.autodiscover()
......@@ -10,5 +12,14 @@ urlpatterns = patterns('',
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)),
url(r'^notify/', include(notify_pattern())),
url(r'', include(wiki_pattern())),
)
if settings.DEBUG:
urlpatterns += staticfiles_urlpatterns()
urlpatterns += patterns('',
url(r'^media/(?P<path>.*)$', 'django.views.static.serve', {
'document_root': settings.MEDIA_ROOT,
}),
)
urlpatterns += patterns('', url(r'', include(wiki_pattern())),)
\ No newline at end of file
import os
import mimetypes
from datetime import datetime
from django.http import HttpResponse
from django.utils.http import http_date
from django.utils import dateformat
def send_file(request, filepath, last_modified=None, filename=None):
fullpath = filepath
# Respect the If-Modified-Since header.
statobj = os.stat(fullpath)
if filename:
mimetype, encoding = mimetypes.guess_type(filename)
else:
mimetype, encoding = mimetypes.guess_type(fullpath)
mimetype = mimetype or 'application/octet-stream'
response = HttpResponse(open(fullpath, 'rb').read(), mimetype=mimetype)
if not last_modified:
response["Last-Modified"] = http_date(statobj.st_mtime)
else:
if isinstance(last_modified, datetime):
print last_modified
last_modified = float(dateformat.format(last_modified, 'U'))
print last_modified
response["Last-Modified"] = http_date(epoch_seconds=last_modified)
response["Content-Length"] = statobj.st_size
if encoding:
response["Content-Encoding"] = encoding
# TODO: Escape filename
if filename:
response["Content-Disposition"] = "attachment; filename=%s" % filename
return response
......@@ -214,7 +214,7 @@ class PermissionsForm(forms.ModelForm):
kwargs['instance'] = article
super(PermissionsForm, self).__init__(*args, **kwargs)
print self.data
if user.is_superuser:
if user.has_perm("wiki.admin"):
self.fields['group'].queryset = models.Group.objects.all()
else:
self.fields['group'].queryset = models.Group.objects.filter(user=user)
......
from django.db import models
# First, define the Manager subclass.
class ActiveObjectsManager(models.Manager):
def get_query_set(self):
return super(ActiveObjectsManager, self).get_query_set().filter(current_revision__deleted=False)
......@@ -23,6 +23,9 @@ if not 'sekizai' in django_settings.INSTALLED_APPS:
if not 'django_notify' in django_settings.INSTALLED_APPS:
raise ImproperlyConfigured('django-wiki: needs django_notify in INSTALLED_APPS')
if not 'django.contrib.humanize' in django_settings.INSTALLED_APPS:
raise ImproperlyConfigured('django-wiki: needs django.contrib.humanize in INSTALLED_APPS')
if not 'django.contrib.contenttypes' in django_settings.INSTALLED_APPS:
raise ImproperlyConfigured('django-wiki: needs django.contrib.contenttypes in INSTALLED_APPS')
......
......@@ -127,6 +127,11 @@ class Article(models.Model):
class Meta:
app_label = settings.APP_LABEL
permissions = (
("moderator", "Can edit all articles and lock/unlock/restore"),
("admin", "Can change ownership of any article"),
("grant", "Can assign permissions to other users"),
)
def render(self, preview_content=None):
if not self.current_revision:
......@@ -172,6 +177,20 @@ class BaseRevision(models.Model):
previous_revision = models.ForeignKey('self', blank=True, null=True)
# NOTE! The semantics of these fields are not related to the revision itself
# but the actual related object. If the latest revision says "deleted=True" then
# the related object should be regarded as deleted.
deleted = models.BooleanField(verbose_name=_(u'deleted'))
locked = models.BooleanField(verbose_name=_(u'locked'))
def set_from_request(self, request):
if request.user.is_authenticated():
self.user = request.user
if settings.LOG_IPS_USERS:
self.ip_address = request.META.get('REMOTE_ADDR', None)
elif settings.LOG_IPS_ANONYMOUS:
self.ip_address = request.META.get('REMOTE_ADDR', None)
class Meta:
abstract = True
app_label = settings.APP_LABEL
......@@ -193,10 +212,6 @@ class ArticleRevision(BaseRevision):
title = models.CharField(max_length=512, verbose_name=_(u'article title'),
null=False, blank=False, help_text=_(u'Each revision contains a title field that must be filled out, even if the title has not changed'))
# Simple properties
deleted = models.BooleanField(verbose_name=_(u'article deleted'))
locked = models.BooleanField(verbose_name=_(u'article locked'))
# Allow a revision to redirect to another *article*. This
# way, we can redirects and still maintain old content.
redirect = models.ForeignKey('Article', null=True, blank=True,
......@@ -228,6 +243,9 @@ class ArticleRevision(BaseRevision):
except ArticleRevision.DoesNotExist:
self.revision_number = 1
if not self.previous_revision and self.article.current_revision != self:
self.previous_revision = self.article.current_revision
super(ArticleRevision, self).save(*args, **kwargs)
if not self.article.current_revision:
......
......@@ -26,32 +26,32 @@ class ArticlePlugin(models.Model):
created = models.DateTimeField(auto_now_add=True, verbose_name=_(u"created"))
modified = models.DateTimeField(auto_now=True, verbose_name=_(u"created"))
deleted = models.BooleanField(default=False)
class Meta:
abstract = True
class ReusablePlugin(models.Model):
class ReusablePlugin(ArticlePlugin):
# The article on which the plugin was originally created.
# Used to apply permissions.
original_article = models.ForeignKey(Article, on_delete=models.SET_NULL,
verbose_name=_(u'original article'), null=True, blank=True,
related_name='original_plugin_set',
help_text=_(u'Permissions are inherited from this article'))
ArticlePlugin.article.verbose_name=_(u'original article')
ArticlePlugin.article.help_text=_(u'Permissions are inherited from this article')
ArticlePlugin.article.on_delete=models.SET_NULL
ArticlePlugin.article.null = True
ArticlePlugin.article.blank = True
articles = models.ManyToManyField(Article)
created = models.DateTimeField(auto_now_add=True, verbose_name=_(u"created"))
modified = models.DateTimeField(auto_now=True, verbose_name=_(u"created"))
articles = models.ManyToManyField(Article, related_name='shared_plugins_set')
# Permission methods - you may override these, if they don't fit your logic.
def can_read(self, *args, **kwargs):
if self.original_article:
return self.original_article.can_read(*args, **kwargs)
if self.article:
return self.article.can_read(*args, **kwargs)
return False
def can_write(self, *args, **kwargs):
if self.original_article:
return self.original_article.can_write(*args, **kwargs)
if self.article:
return self.article.can_write(*args, **kwargs)
return False
class Meta:
......@@ -60,10 +60,10 @@ class ReusablePlugin(models.Model):
def save(self, *args, **kwargs):
# Automatically make the original article the first one in the added set
if not self.original_article:
if not self.article:
articles = self.articles.all()
if articles.count() == 0:
self.original_article = articles[0]
self.article = articles[0]
super(ReusablePlugin, self).save(*args, **kwargs)
......@@ -111,9 +111,3 @@ class RevisionPlugin(models.Model):
class Meta:
abstract = True
def get_editor_media(self, editor):
if editor == 'markitup':
pass
if editor == 'markitup':
pass
# -*- coding: utf-8 -*-
from django import forms
from django.utils.translation import ugettext as _
from wiki.plugins.attachments import models
class AttachmentForm(forms.ModelForm):
description = forms.CharField(label=_(u'Description'),
help_text=_(u'A short summary of what the file contains'),
required=False)
class Meta:
model = models.AttachmentRevision
fields = ('file', 'description',)
class DeleteForm(forms.Form):
"""This form is both used for dereferencing and deleting attachments"""
confirm = forms.BooleanField(label=_(u'Yes I am sure...'),
required=False)
def clean_confirm(self):
if not self.cleaned_data['confirm']:
raise forms.ValidationError(_(u'You are not sure enough!'))
return True
class SearchForm(forms.Form):
query = forms.CharField(label=_(u'Query'),
help_text=_(u'You may search file names and descriptions.'))
......@@ -3,6 +3,7 @@ from django.utils.translation import ugettext_lazy as _
import settings
from wiki import managers
from wiki.conf import settings as wiki_settings
from wiki.models.pluginbase import ReusablePlugin
from wiki.models.article import BaseRevision
......@@ -13,6 +14,10 @@ class IllegalFileExtension(Exception):
class Attachment(ReusablePlugin):
objects = models.Manager()
active_objects = managers.ActiveObjectsManager()
current_revision = models.OneToOneField('AttachmentRevision',
verbose_name=_(u'current revision'),
blank=True, null=True, related_name='current_set',
......@@ -21,8 +26,6 @@ class Attachment(ReusablePlugin):
original_filename = models.CharField(max_length=256, verbose_name=_(u'original filename'), blank=True, null=True)
description = models.TextField(blank=True)
class Meta:
verbose_name = _(u'attachment')
verbose_name_plural = _(u'attachments')
......@@ -30,43 +33,71 @@ class Attachment(ReusablePlugin):
def upload_path(instance, filename):
from os import path
try:
extension = filename.split(".")[-1]
except IndexError:
# No extension
raise IllegalFileExtension()
raise IllegalFileExtension("No file extension found in filename. That's not okay!")
# Must be an allowed extension
if not extension.lower() in map(lambda x: x.lower(), settings.FILE_EXTENSIONS):
raise IllegalFileExtension()
raise IllegalFileExtension("The following filename is illegal: %s. Extension has to be on of %s" %
(filename, str(settings.FILE_EXTENSIONS)))
# Has to match original extension filename
if instance.attachment and instance.attachment.original_filename:
original_extension = ".".split(instance.attachment.original_filename)[-1]
if instance.id and instance.attachment and instance.attachment.original_filename:
original_extension = instance.attachment.original_filename.split(".")[-1]
if not extension.lower() == original_extension:
raise IllegalFileExtension("File extension has to be %s" % original_extension)
raise IllegalFileExtension("File extension has to be '%s', not '%s'." %
(original_extension, extension.lower()))
elif instance.attachment:
instance.attachment.original_filename = filename
upload_path = settings.UPLOAD_PATH
upload_path = upload_path.replace('%aid', str(instance.original_article.id))
upload_path = upload_path.replace('%aid', str(instance.attachment.article.id))
if settings.UPLOAD_PATH_OBSCURIFY:
import random, hashlib
m=hashlib.md5(str(random.randint(0,100000000000000)))
upload_path = path.join(upload_path, m.hexdigest())
return path.join(upload_path, filename + '.upload')
class AttachmentRevision(BaseRevision):
attachment = models.ForeignKey('Attachment')
file = models.FileField(upload_to=upload_path, #@ReservedAssignment
verbose_name=_(u'file'))
description = models.TextField(blank=True)
class Meta:
verbose_name = _(u'attachment revision')
verbose_name_plural = _(u'attachment revisions')
get_latest_by = ('revision_number',)
app_label = wiki_settings.APP_LABEL
def get_filename(self):
if not self.file:
return None
filename = self.file.path.split("/")[-1]
return ".".join(filename.split(".")[:-1])
def save(self, *args, **kwargs):
if not self.revision_number:
try:
previous_revision = self.attachment.attachmentrevision_set.latest()
self.revision_number = previous_revision.revision_number + 1
# NB! The above should not raise the below exception, but somehow it does.
except Attachment.DoesNotExist:
self.revision_number = 1
if not self.previous_revision and self.attachment.current_revision != self:
self.previous_revision = self.attachment.current_revision
super(AttachmentRevision, self).save(*args, **kwargs)
if not self.attachment.current_revision:
# If I'm saved from Django admin, then article.current_revision is me!
self.attachment.current_revision = self
self.attachment.save()
from django.conf import settings as django_settings
SLUG = "attachments"
# Where to store article attachments, relative to MEDIA_ROOT
UPLOAD_PATH = getattr(django_settings, 'WIKI_UPLOAD_PATH', 'wiki/uploads/%aid/')
......
{% extends "wiki/base.html" %}
{% load wiki_tags i18n humanize %}
{% load url from future %}
{% block pagetitle %}{% trans "Delete" %} "{{ attachment.current_revision.get_filename }}"{% endblock %}
{% block wiki_breadcrumbs %}
{% include "wiki/includes/breadcrumbs.html" %}
{% endblock %}
{% block wiki_contents %}
{% article_for_object urlpath as article %}
{% if article %}
<div class="tabbable tabs-top" style="margin-top: 40px;">
<ul class="nav nav-tabs">
{% with "attachments" as selected %}
{% include "wiki/includes/article_menu.html" %}
{% endwith %}
<li>
<h1 style="margin-top: -10px;">
{{ article.current_revision.title }}
</h1>
</li>
</ul>
<div class="tab-content">
{% if attachment.article == article %}
<h2>{% trans "Delete" %} "{{ attachment.current_revision.get_filename }}"?</h2>
<p class="lead">
{% blocktrans with attachment.original_filename as filename %}
The file may be referenced on other articles. Deleting it means that they will loose their references to this file. The following articles reference this file:
{% endblocktrans %}
</p>
<ul>
{% for a in attachment.articles.all %}
<li style="font-size: 150%;">{{ a.current_revision.title }}</li>
{% endfor %}
</ul>
<hr />
<form method="POST" class="form-horizontal" id="attachment_form" enctype="multipart/form-data">
{% wiki_form form %}
<div class="form-actions">
<a href="{% url 'wiki:attachments_index' path=urlpath.path %}" class="btn">
<span class="icon-arrow-left"></span>
{% trans "Go back" %}
</a>
<button class="btn btn-danger btn-large">
<span class="icon-upload"></span>
{% trans "Delete it!" %}
</button>
</div>
</form>
{% else %}
<h2>{% trans "Remove" %} "{{ attachment.current_revision.get_filename }}"?</h2>
<p class="lead">
{% blocktrans with attachment.original_filename as filename %}
You can remove a reference to a file, but it will retain its references on other articles.
{% endblocktrans %}
</p>
<form method="POST" class="form-horizontal" id="attachment_form" enctype="multipart/form-data">
{% wiki_form form %}
<div class="form-actions">
<a href="{% url 'wiki:attachments_index' path=urlpath.path %}" class="btn">
<span class="icon-arrow-left"></span>
{% trans "Go back" %}
</a>
<button class="btn btn-danger btn-large">
<span class="icon-upload"></span>
{% trans "Remove reference" %}
</button>
</div>
</form>
{% endif %}
</div>
</div>
<div class="tabbable tabs-below" style="margin-top: 20px;">
<ul class="nav nav-tabs">
<li style="margin-top: 10px;"><em>{% trans "Article last modified:" %} {{ article.current_revision.modified }}</em></li>
</ul>
</div>
{% else %}
{% trans "An article for this path does not exist." %}
{% endif %}
{% endblock %}
{% extends "wiki/base.html" %}
{% load wiki_tags i18n humanize %}
{% load url from future %}
{% block pagetitle %}{% trans "History of" %} "{{ attachment.current_revision.get_filename }}"{% endblock %}
{% block wiki_breadcrumbs %}
{% include "wiki/includes/breadcrumbs.html" %}
{% endblock %}
{% block wiki_contents %}
{% article_for_object urlpath as article %}
{% if article %}
<div class="tabbable tabs-top" style="margin-top: 40px;">
<ul class="nav nav-tabs">
{% with "attachments" as selected %}
{% include "wiki/includes/article_menu.html" %}
{% endwith %}
<li>
<h1 style="margin-top: -10px;">
{{ article.current_revision.title }}
</h1>
</li>
</ul>
<div class="tab-content">
<h2>{% trans "History of" %} "{{ attachment.current_revision.get_filename }}"</h2>
<table class="table table-striped table-bordered">
<tr>
<th>{% trans "Date" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Description" %}</th>
<th>{% trans "File" %}</th>
<th>{% trans "Size" %}</th>
<th style="text-align: right">{% trans "Action" %}</th>
</tr>
{% for revision in revisions %}
<tr>
<td>
{{ revision.created }}
{% if revision.deleted %}<span class="badge badge-important">{% trans "deleted" %}</span>{% endif %}
</td>
<td>
{% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
</td>
<td>{{ revision.description|default:_("<em>No description</em>")|safe }}</td>
<td>{{ revision.get_filename }}</td>
<td>{{ revision.file.size|filesizeformat }}</td>
<td style="text-align: right">
<form method="POST" action="{% url 'wiki:attachments_revision_change' path=urlpath.path attachment_id=attachment.id revision_id=revision.id %}">
{% csrf_token %}
<a href="{% url 'wiki:attachments_download' path=urlpath.path attachment_id=attachment.id revision_id=revision.id %}" class="btn btn-primary">
<span class="icon-download"></span>
{% trans "Download" %}
</a>
{% if revision.attachment.article|can_write:user %}
<button{% if revision == attachment.current_revision %} disabled="disabled"{% endif %} class="btn">
<span class="icon-flag"></span>
{% trans "Use this!" %}
</button>
{% endif %}
</form>
</td>
</tr>
{% endfor %}
</table>
<a href="{% url 'wiki:attachments_index' path=urlpath.path %}"><span class="icon-arrow-left"></span> {% trans "Go back" %}</a>
</div>
</div>
<div class="tabbable tabs-below" style="margin-top: 20px;">
<ul class="nav nav-tabs">
<li style="margin-top: 10px;"><em>{% trans "Article last modified:" %} {{ article.current_revision.modified }}</em></li>
</ul>
</div>
{% else %}
{% trans "An article for this path does not exist." %}
{% endif %}
{% endblock %}
{% extends "wiki/base.html" %}
{% load wiki_tags i18n humanize %}
{% load url from future %}
{% block pagetitle %}{% trans "Attachments" %}: {% article_for_object urlpath as article %}{{ article.current_revision.title }}{% endblock %}
{% block wiki_breadcrumbs %}
{% include "wiki/includes/breadcrumbs.html" %}
{% endblock %}
{% block wiki_contents %}
{% article_for_object urlpath as article %}
{% if article %}
<div class="tabbable tabs-top" style="margin-top: 40px;">
<ul class="nav nav-tabs">
{% with "attachments" as selected %}
{% include "wiki/includes/article_menu.html" %}
{% endwith %}
<li>
<h1 style="margin-top: -10px;">
{{ article.current_revision.title }}
</h1>
</li>
</ul>
<div class="tab-content">
<div class="row-fluid">
<div class="span6">
<h2>{% trans "Attachments" %}</h2>
{% for attachment in attachments %}
<table class="table table-bordered table-striped" style="width: 100%;">
<tr>
<td colspan="4">
<h3>
<a href="{% url 'wiki:attachments_download' path=urlpath.path attachment_id=attachment.id %}">{{ attachment.current_revision.get_filename }}</a>
<span class="badge">{{ attachment.current_revision.created|naturaltime }}</span>
<span class="badge badge-info">{{ attachment.current_revision.file.size|filesizeformat }}</span>
{% if attachment.current_revision.deleted %}
<span class="badge badge-important">{% trans "deleted" %}</span>
{% endif %}
</h3>
{{ attachment.current_revision.description }}
</td>
</tr>
<tr>
<th>{% trans "Markdown tag" %}</th>
<th>{% trans "Uploaded by" %}</th>
<th style="text-align: right;">{% trans "Actions" %}</th>
</tr>
<tr>
<td><code>[attachment:{{ attachment.id }}]</code></td>
<td>
{% if attachment.current_revision.user %}{{ attachment.current_revision.user }}{% else %}{% if user|is_moderator %}{{ attachment.current_revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
</td>
<td style="text-align: right;">
{% if attachment|can_write:user %}
{% if not attachment.current_revision.deleted %}
<a href="{% url 'wiki:attachments_replace' path=urlpath.path attachment_id=attachment.id %}" class="btn"><span class="icon-upload"></span> {% trans "Replace" %}</a>
{% if attachment.article = article %}
<a href="{% url 'wiki:attachments_delete' path=urlpath.path attachment_id=attachment.id %}" class="btn"><span class="icon-remove"></span> {% trans "Delete" %}</a>
{% else %}
<a href="{% url 'wiki:attachments_delete' path=urlpath.path attachment_id=attachment.id %}" class="btn"><span class="icon-minus-sign"></span> {% trans "Detach" %}</a>
{% endif %}
{% else %}
<form method="POST" action="{% url 'wiki:attachments_revision_change' path=urlpath.path attachment_id=attachment.id revision_id=attachment.current_revision.previous_revision.id %}">
{% csrf_token %}
<button class="btn">
<span class="icon-flag"></span>
{% trans "Restore" %}
</button>
</form>
{% endif %}
{% else %}
<em>{% trans "none" %}</em>
{% endif %}
</td>
</tr>
<tr>
<td colspan="4">
<a href="{% url 'wiki:attachments_history' path=urlpath.path attachment_id=attachment.id %}">
<span class="icon-time"></span>
{% trans "View file history" %} ({{ attachment.attachmentrevision_set.all.count }} {% trans "revisions" %})
</a>
</td>
</tr>
</table>
{% empty %}
<p style="margin-bottom: 20px;"><em>{% trans "There are no attachments for this article." %}</em></p>
{% endfor %}
</div>
<div class="span6">
<div class="well">
<h2>{% trans "Add from upload" %}</h2>
<form method="POST" class="form-vertical" id="attachment_form" enctype="multipart/form-data">
{% wiki_form form %}
<button type="submit" name="save" value="1" class="btn btn-primary">
<span class="icon-upload"></span>
{% trans "Upload file" %}
</button>
</form>
</div>
<div class="well">
<h2>{% trans "Add from existing attachments..." %}</h2>
<p>{% trans "You can reuse files from other articles. Please note that these files are subject to updates on other articles." %}</p>
</div>
</div>
</div>
</div>
</div>
<div class="tabbable tabs-below" style="margin-top: 20px;">
<ul class="nav nav-tabs">
<li style="margin-top: 10px;"><em>{% trans "Article last modified:" %} {{ article.current_revision.modified }}</em></li>
</ul>
</div>
{% else %}
{% trans "An article for this path does not exist." %}
{% endif %}
{% endblock %}
{% extends "wiki/base.html" %}
{% load wiki_tags i18n %}
{% load wiki_tags i18n humanize %}
{% load url from future %}
{% block pagetitle %}{% trans "Settings" %}: {% article_for_object urlpath as article %}{{ article.current_revision.title }}{% endblock %}
{% block pagetitle %}{% trans "Replace" %} "{{ attachment.current_revision.get_filename }}"{% endblock %}
{% block wiki_breadcrumbs %}
{% include "wiki/includes/breadcrumbs.html" %}
......@@ -25,8 +25,38 @@
</li>
</ul>
<div class="tab-content">
<h2>{% trans "Replace" %} "{{ attachment.current_revision.get_filename }}"</h2>
{% if attachment.articles.count > 1 %}
<p class="lead">
{% blocktrans with attachment.original_filename as filename %}
Replacing an attachment means adding a new file that will be used in its place. All references to the file will be replaced by the one you upload and the file will be downloaded as <strong>{{ filename }}</strong>. Please note that this attachment is in use on other articles, you may distort contents. However, do not hestitate to take advantage of this and make replacements for the listed articles where necessary. This way of working is more efficient....
{% endblocktrans %}
</p>
<h3>{% trans "Articles using" %} {{ attachment.current_revision.get_filename }}</h3>
<ul>
{% for a in attachment.articles.all %}<li>{{ a.current_revision.title }}</li>{% endfor %}
</ul>
{% else %}
<p class="lead">
{% blocktrans with attachment.original_filename as filename %}
Replacing an attachment means adding a new file that will be used in its place. All references to the file will be replaced by the one you upload and the file will be downloaded as <strong>{{ filename }}</strong>.
{% endblocktrans %}
</p>
{% endif %}
Upload form coming up! List of attachments coming here and in the article text + a special markdown tag for including a link to each attachment in the article text.
<form method="POST" class="form-horizontal" id="attachment_form" enctype="multipart/form-data">
{% wiki_form form %}
<div class="form-actions">
<a href="{% url 'wiki:attachments_index' path=urlpath.path %}" class="btn">
<span class="icon-arrow-left"></span>
{% trans "Go back" %}
</a>
<button class="btn btn-primary">
<span class="icon-upload"></span>
{% trans "Upload replacement" %}
</button>
</div>
</form>
</div>
</div>
......
# -*- coding: utf-8 -*-
from django.shortcuts import redirect, get_object_or_404
from django.utils.decorators import method_decorator
from django.utils.translation import ugettext as _
from django.views.generic.edit import FormView
from wiki.views.mixins import ArticleMixin
from wiki.decorators import get_article
from wiki.plugins.attachments import forms
from wiki.plugins.attachments import models
from wiki.plugins.attachments import settings
from django.contrib import messages
from django.views.generic.base import TemplateView, View
from wiki.core.http import send_file
from django.http import Http404
class AttachmentView(ArticleMixin, FormView):
form_class = forms.AttachmentForm
template_name="wiki/plugins/attachments/index.html"
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, *args, **kwargs):
if request.user.has_perm('wiki.moderator'):
self.attachments = models.Attachment.objects.filter(articles=article).order_by('current_revision__deleted', 'original_filename')
else:
self.attachments = models.Attachment.active_objects.filter(articles=article)
return super(AttachmentView, self).dispatch(request, article, *args, **kwargs)
def form_valid(self, form):
attachment_revision = form.save(commit=False)
attachment = models.Attachment()
attachment.article = self.article
attachment.original_filename = attachment_revision.get_filename()
attachment.save()
attachment.articles.add(self.article)
attachment_revision.attachment = attachment
attachment_revision.set_from_request(self.request)
attachment_revision.save()
messages.success(self.request, _(u'%s was successfully added.') % attachment_revision.get_filename())
return redirect("wiki:plugin_url", self.urlpath.path, settings.SLUG)
def get_context_data(self, **kwargs):
kwargs['attachments'] = self.attachments
return super(AttachmentView, self).get_context_data(**kwargs)
class AttachmentHistoryView(ArticleMixin, TemplateView):
template_name="wiki/plugins/attachments/history.html"
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
if request.user.has_perm('wiki.moderator'):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
else:
self.attachment = get_object_or_404(models.Attachment.active_objects, id=attachment_id, articles=article)
return super(AttachmentHistoryView, self).dispatch(request, article, *args, **kwargs)
def get_context_data(self, **kwargs):
kwargs['attachment'] = self.attachment
kwargs['revisions'] = self.attachment.attachmentrevision_set.all().order_by('-revision_number')
return super(AttachmentHistoryView, self).get_context_data(**kwargs)
class AttachmentReplaceView(ArticleMixin, FormView):
form_class = forms.AttachmentForm
template_name="wiki/plugins/attachments/replace.html"
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
self.attachment = get_object_or_404(models.Attachment.active_objects, id=attachment_id, articles=article)
return super(AttachmentReplaceView, self).dispatch(request, article, *args, **kwargs)
def form_valid(self, form):
attachment_revision = form.save(commit=False)
attachment_revision.attachment = self.attachment
attachment_revision.set_from_request(self.request)
attachment_revision.previous_revision = self.attachment.current_revision
attachment_revision.save()
self.attachment.current_revision = attachment_revision
self.attachment.save()
messages.success(self.request, _(u'%s uploaded and replaces old attachment.') % attachment_revision.get_filename())
if self.urlpath:
return redirect("wiki:attachments_index", self.urlpath.path, settings.SLUG)
# TODO: What if we do not have a urlpath?
def get_form(self, form_class):
form = FormView.get_form(self, form_class)
form.fields['file'].help_text = _(u'Your new file will automatically be renamed to match the file already present. Files with different extensions are not allowed.')
return form
def get_initial(self, **kwargs):
return {'description': self.attachment.current_revision.description}
def get_context_data(self, **kwargs):
kwargs['attachment'] = self.attachment
return super(AttachmentReplaceView, self).get_context_data(**kwargs)
class AttachmentDownloadView(ArticleMixin, View):
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
revision_id = kwargs.get('revision_id', None)
if revision_id:
self.revision = get_object_or_404(models.AttachmentRevision, id=revision_id, attachment__articles=article)
else:
self.revision = self.attachment.current_revision
return super(AttachmentDownloadView, self).dispatch(request, article, *args, **kwargs)
def get(self, request, *args, **kwargs):
if self.revision:
return send_file(request, self.revision.file.path,
self.revision.created, self.attachment.original_filename)
raise Http404
class AttachmentChangeRevisionView(ArticleMixin, View):
form_class = forms.AttachmentForm
template_name="wiki/plugins/attachments/replace.html"
@method_decorator(get_article(can_write=True))
def dispatch(self, request, article, attachment_id, revision_id, *args, **kwargs):
if request.user.has_perm('wiki.moderator'):
self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article)
else:
self.attachment = get_object_or_404(models.Attachment.active_objects, id=attachment_id, articles=article)
self.revision = get_object_or_404(models.AttachmentRevision, id=revision_id, attachment__articles=article)
return super(AttachmentChangeRevisionView, self).dispatch(request, article, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.attachment.current_revision = self.revision
self.attachment.save()
messages.success(self.request, _(u'Current revision changed for %s.') % self.attachment.original_filename)
if self.urlpath:
return redirect("wiki:attachments_index", path=self.urlpath.path)
# TODO: What if this hasn't got a urlpath.
else:
pass
class AttachmentDeleteView(ArticleMixin, FormView):
form_class = forms.DeleteForm
template_name="wiki/plugins/attachments/delete.html"
@method_decorator(get_article(can_write=True))
def dispatch(self, request, article, attachment_id, *args, **kwargs):
self.attachment = get_object_or_404(models.Attachment.active_objects, id=attachment_id, articles=article)
return super(AttachmentDeleteView, self).dispatch(request, article, *args, **kwargs)
def form_valid(self, form):
if self.attachment.article == self.article:
revision = models.AttachmentRevision()
revision.attachment = self.attachment
revision.set_from_request(self.request)
revision.deleted = True
revision.file = self.attachment.current_revision.file
revision.description = self.attachment.current_revision.description
revision.save()
self.attachment.current_revision = revision
self.attachment.save()
messages.info(self.request, _(u'The file %s was deleted.') % self.attachment.original_filename)
else:
self.attachment.articles.remove(self.article)
messages.info(self.request, _(u'This article is no longer related to the file %s.') % self.attachment.original_filename)
if self.urlpath:
return redirect("wiki:get_url", path=self.urlpath.path)
# TODO: No urlpath?
def get_context_data(self, **kwargs):
kwargs['attachment'] = self.attachment
return super(AttachmentDeleteView, self).get_context_data(**kwargs)
# -*- coding: utf-8 -*-
from django.conf.urls.defaults import patterns, url
from django.utils.translation import ugettext as _
from django.views.generic.base import TemplateView
from django.utils.decorators import method_decorator
from wiki.core import plugins_registry
from wiki.views.mixins import ArticleMixin
from wiki.decorators import get_article
from wiki.plugins.attachments import views
from wiki.plugins.attachments import settings
class AttachmentView(ArticleMixin, TemplateView):
template_name="wiki/plugins/attachments/tab.html"
@method_decorator(get_article(can_read=True))
def dispatch(self, request, article, *args, **kwargs):
return super(AttachmentView, self).dispatch(request, article, *args, **kwargs)
class AttachmentPlugin(plugins_registry.BasePlugin):
#settings_form = 'wiki.plugins.notifications.forms.SubscriptionForm'
slug = 'attachments'
slug = settings.SLUG
urlpatterns = patterns('',
url('^$', views.AttachmentView.as_view(), name='attachments_index'),
url('^replace/(?P<attachment_id>\d+)/$', views.AttachmentReplaceView.as_view(), name='attachments_replace'),
url('^history/(?P<attachment_id>\d+)/$', views.AttachmentHistoryView.as_view(), name='attachments_history'),
url('^download/(?P<attachment_id>\d+)/$', views.AttachmentDownloadView.as_view(), name='attachments_download'),
url('^delete/(?P<attachment_id>\d+)/$', views.AttachmentDeleteView.as_view(), name='attachments_delete'),
url('^download/(?P<attachment_id>\d+)/revision/(?P<revision_id>\d+)/$', views.AttachmentDownloadView.as_view(), name='attachments_download'),
url('^change/(?P<attachment_id>\d+)/revision/(?P<revision_id>\d+)/$', views.AttachmentChangeRevisionView.as_view(), name='attachments_revision_change'),
)
article_tab = (_(u'Attachments'), "icon-file")
article_view = AttachmentView().dispatch
article_view = views.AttachmentView().dispatch
article_template_append = 'wiki/plugins/attachments/append.html'
def __init__(self):
......
ARTICLE_EDIT = "article_edit"
ARTICLE_CREATE = "article_create"
from django import forms
from django.utils.translation import ugettext as _
import settings
from django_notify.models import Settings, NotificationType
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import mark_safe
from wiki.plugins.notifications import ARTICLE_EDIT
class SubscriptionForm(forms.Form):
settings_form_id = "notifications"
......@@ -19,7 +20,7 @@ class SubscriptionForm(forms.Form):
self.user = user
initial = kwargs.pop('initial', None)
self.settings = Settings.objects.get_or_create(user=user,)[0]
self.notification_type = NotificationType.objects.get_or_create(key=settings.ARTICLE_EDIT,
self.notification_type = NotificationType.objects.get_or_create(key=ARTICLE_EDIT,
content_type=ContentType.objects.get_for_model(article))[0]
self.edit_notifications=models.ArticleSubscription.objects.filter(settings=self.settings,
article=article,
......
......@@ -6,9 +6,9 @@ from django.db.models import signals
from django_notify import notify
from django_notify.models import Subscription
from wiki import models as wiki_models
from wiki.plugins.notifications import ARTICLE_CREATE, ARTICLE_EDIT
import settings
from wiki import models as wiki_models
class ArticleSubscription(wiki_models.pluginbase.ArticlePlugin, Subscription):
......@@ -25,7 +25,7 @@ def post_article_save(instance, **kwargs):
url = reverse('wiki:get_url', urlpath.path)
else:
url = None
notify(_(u'New article created: %s') % instance.title, settings.ARTICLE_CREATE,
notify(_(u'New article created: %s') % instance.title, ARTICLE_CREATE,
target_object=instance, url=url)
def post_article_revision_save(instance, **kwargs):
......@@ -35,7 +35,7 @@ def post_article_revision_save(instance, **kwargs):
url = reverse('wiki:get_url', args=(urlpath.path,))
except wiki_models.URLPath.DoesNotExist:
url = None
notify(_(u'Article modified: %s') % instance.title, settings.ARTICLE_EDIT,
notify(_(u'Article modified: %s') % instance.title, ARTICLE_EDIT,
target_object=instance.article, url=url)
# Create notifications when new articles are saved. We do NOT care
......
#from django.conf import settings as django_settings
# These are actually just constants...
ARTICLE_EDIT = "article_edit"
ARTICLE_CREATE = "article_create"
......@@ -13,4 +13,7 @@
</button>
</div>
</form>
<script type="text/javascript">
$('#id_username').focus();
</script>
{% endblock %}
......@@ -20,7 +20,11 @@
#settings_form select {}
.asteriskField { font-size: 20px; margin-left: 5px; }
#attachment_form #id_description
{ width: 95% }
input[type=file] {float: none; width: auto;}
.asteriskField { font-size: 20px; margin-left: 5px;}
.notification-list .since {
font-size: 80%;
......
......@@ -76,7 +76,7 @@
<div class="accordion-group">
<div class="accordion-heading">
<a class="accordion-toggle" style="float: left;" href="#collapse{{ revision.revision_number }}" onclick="get_diff_json('{% url 'wiki:diff' revision.id %}', $('#collapse{{ revision.revision_number }}'))">
{{ revision.created }} (#{{ revision.revision_number }}) by {% if revision.user %}{{ revision.user }}{% else %}{% if user.is_superuser %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{{ revision.created }} (#{{ revision.revision_number }}) by {% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{% if revision == article.current_revision %}
<strong>*</strong>
{% endif %}
......
......@@ -3,15 +3,15 @@
{% block wiki_body %}
{% if revision %}
<div class="alert alert-info">
<strong>{% trans "Previewing revision" %}:</strong> {{ revision.created }} (#{{ revision.revision_number }}) by {% if revision.user %}{{ revision.user }}{% else %}{% if user.is_superuser %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
<strong>{% trans "Previewing revision" %}:</strong> {{ revision.created }} (#{{ revision.revision_number }}) by {% if revision.user %}{{ revision.user }}{% else %}{% if user|is_moderator %}{{ revision.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
</div>
{% endif %}
{% if merge %}
<div class="alert alert-info">
<strong>{% trans "Previewing merge between" %}:</strong>
{{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user.is_superuser %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user|is_moderator %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
<strong>{% trans "and" %}</strong>
{{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user.is_superuser %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
{{ merge1.created }} (#{{ merge1.revision_number }}) by {% if merge1.user %}{{ merge1.user }}{% else %}{% if user|is_moderator %}{{ merge1.ip_address|default:"anonymous (IP not logged)" }}{% else %}{% trans "anonymous (IP logged)" %}{% endif %}{% endif %}
</div>
{% endif %}
<h1 class="page-header">{{ title }}</h1>
......
......@@ -60,3 +60,8 @@ def can_read(obj, user):
def can_write(obj, user):
"""Articles and plugins have a can_write method..."""
return obj.can_write(user)
@register.filter
def is_moderator(user):
"""Tells if a user is a moderator"""
return user.has_perm('wiki.moderator')
# -*- coding: utf-8 -*-
from django.conf.urls.defaults import patterns, url
from django.conf.urls.defaults import patterns, url, include
from wiki.views import article, accounts
from wiki.conf import settings
from wiki.core import plugins_registry
urlpatterns = patterns('',
url('^$', article.ArticleView.as_view(), name='root', kwargs={'path': ''}),
......@@ -29,8 +30,18 @@ urlpatterns += patterns('',
url('^(?P<path>.+/|)_history/$', article.History.as_view(), name='history_url'),
url('^(?P<path>.+/|)_settings/$', article.Settings.as_view(), name='settings_url'),
url('^(?P<path>.+/|)_revision/change/(?P<revision_id>\d+)/$', 'wiki.views.article.change_revision', name='change_revision_url'),
url('^(?P<path>.+/|)_revision/merge/(?P<revision_id>\d+)/$', 'wiki.views.article.merge', name='merge_revision_url'),
url('^(?P<path>.+/|)_revision/merge/(?P<revision_id>\d+)/$', 'wiki.views.article.merge', name='merge_revision_url'),
url('^(?P<path>.+/|)_plugin/(?P<slug>\w+)/$', article.Plugin.as_view(), name='plugin_url'),
)
for plugin in plugins_registry._cache.values():
slug = getattr(plugin, 'slug', None)
plugin_urlpatterns = getattr(plugin, 'urlpatterns', None)
if slug and plugin_urlpatterns:
urlpatterns += patterns('',
url('^(?P<path>.+/|)_plugin/'+slug+'/', include(plugin_urlpatterns)),
)
urlpatterns += patterns('',
url('^(?P<path>.+/|)$', article.ArticleView.as_view(), name='get_url'),
)
......
......@@ -2,7 +2,6 @@
import difflib
from django.contrib import messages
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render_to_response, redirect, get_object_or_404
from django.template.context import RequestContext
from django.utils.decorators import method_decorator
......@@ -17,6 +16,7 @@ from wiki.conf import settings
from wiki.core import plugins_registry
from wiki.core.diff import simple_merge
from wiki.decorators import get_article, json_view
from django.core.urlresolvers import reverse
class ArticleView(ArticleMixin, TemplateView, ):
......@@ -97,12 +97,7 @@ class Edit(FormView, ArticleMixin):
revision.title = form.cleaned_data['title']
revision.content = form.cleaned_data['content']
revision.user_message = form.cleaned_data['summary']
if not self.request.user.is_anonymous:
revision.user = self.request.user
if settings.LOG_IPS_USERS:
revision.ip_address = self.request.META.get('REMOTE_ADDR', None)
elif settings.LOG_IPS_ANONYMOUS:
revision.ip_address = self.request.META.get('REMOTE_ADDR', None)
revision.set_from_request(self.request)
self.article.add_revision(revision)
messages.success(self.request, _(u'A new revision of the article was succesfully added.'))
return self.get_success_url()
......@@ -166,7 +161,7 @@ class Settings(ArticleMixin, TemplateView):
Return all settings forms that can be filled in
"""
settings_forms = [F for F in plugins_registry._settings_forms]
if (self.request.user and self.request.user.is_superuser or
if (self.request.user.has_perm('wiki.admin') or
self.article.owner == self.request.user):
settings_forms.append(self.permission_form_class)
settings_forms.sort(key=lambda form: form.settings_order)
......@@ -304,8 +299,9 @@ def merge(request, article, revision_id, urlpath=None, template_file="wiki/previ
'content': content})
return render_to_response(template_file, c)
@permission_required('wiki.add_article')
def root_create(request):
if not request.user.has_perm('wiki.add_article'):
return redirect(reverse("wiki:login") + "?next=" + reverse("wiki:root_create"))
if request.method == 'POST':
create_form = forms.CreateRoot(request.POST)
if create_form.is_valid():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment