class PurgeView(ArticleMixin, FormView): template_name = "wiki/plugins/images/purge.html" permanent = False form_class = forms.PurgeForm @method_decorator(get_article(can_write=True, can_moderate=True)) def dispatch(self, request, article, *args, **kwargs): self.image = get_object_or_404(models.Image, article=article, id=kwargs.get("image_id", None)) return super().dispatch(request, article, *args, **kwargs) def form_valid(self, form): for revision in self.image.revision_set.all().select_related( "imagerevision"): revision.imagerevision.image.delete(save=False) revision.imagerevision.delete() if self.urlpath: return redirect("wiki:images_index", path=self.urlpath.path) return redirect("wiki:images_index", article_id=self.article_id) def get_context_data(self, **kwargs): # Needed since Django 1.9 because get_context_data is no longer called # with the form instance if "form" not in kwargs: kwargs["form"] = self.get_form() kwargs = ArticleMixin.get_context_data(self, **kwargs) kwargs.update(FormView.get_context_data(self, **kwargs)) return kwargs
class ImageView(ArticleMixin, ListView): template_name = "wiki/plugins/images/index.html" allow_empty = True context_object_name = "images" paginator_class = WikiPaginator paginate_by = 10 @method_decorator(get_article(can_read=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): return super().dispatch(request, article, *args, **kwargs) def get_queryset(self): if self.article.can_moderate( self.request.user) or self.article.can_delete( self.request.user): images = models.Image.objects.filter(article=self.article) else: images = models.Image.objects.filter( article=self.article, current_revision__deleted=False) images.select_related() return images.order_by("-current_revision__imagerevision__created") def get_context_data(self, **kwargs): kwargs.update(ArticleMixin.get_context_data(self, **kwargs)) return ListView.get_context_data(self, **kwargs)
class ChangeRevisionView(RedirectView): permanent = False @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): self.article = article self.urlpath = kwargs.pop("kwargs", False) self.change_revision() return super().dispatch(request, *args, **kwargs) def get_redirect_url(self, **kwargs): if self.urlpath: return reverse("wiki:history", kwargs={"path": self.urlpath.path}) else: return reverse("wiki:history", kwargs={"article_id": self.article.id}) def change_revision(self): revision = get_object_or_404(models.ArticleRevision, article=self.article, id=self.kwargs["revision_id"]) self.article.current_revision = revision self.article.save() messages.success( self.request, _("The article %(title)s is now set to display revision #%(revision_number)d" ) % { "title": revision.title, "revision_number": revision.revision_number, }, )
class AttachmentAddView(ArticleMixin, View): @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): self.attachment = get_object_or_404( models.Attachment.objects.active().can_write(request.user), id=attachment_id) return super().dispatch(request, article, *args, **kwargs) def post(self, request, *args, **kwargs): if not self.attachment.articles.filter(id=self.article.id): self.attachment.articles.add(self.article) self.attachment.save() self.article.clear_cache() messages.success( self.request, _('Added a reference to "%(att)s" from "%(art)s".') % { "att": self.attachment.original_filename, "art": self.article.current_revision.title, }, ) else: messages.error( self.request, _('"%(att)s" is already referenced.') % {"att": self.attachment.original_filename}, ) return redirect("wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id)
class AttachmentSearchView(ArticleMixin, ListView): template_name = "wiki/plugins/attachments/search.html" allow_empty = True context_object_name = "attachments" paginator_class = WikiPaginator paginate_by = 10 @method_decorator(get_article(can_write=True)) def dispatch(self, request, article, *args, **kwargs): return super().dispatch(request, article, *args, **kwargs) def get_queryset(self): self.query = self.request.GET.get("query", None) if not self.query: qs = models.Attachment.objects.none() else: qs = models.Attachment.objects.active().can_read(self.request.user) qs = qs.filter( Q(original_filename__contains=self.query) | Q(current_revision__description__contains=self.query) | Q(article__current_revision__title__contains=self.query)) return qs.order_by("original_filename") def get_context_data(self, **kwargs): # Is this a bit of a hack? Use better inheritance? kwargs_article = ArticleMixin.get_context_data(self, **kwargs) kwargs_listview = ListView.get_context_data(self, **kwargs) kwargs["search_form"] = forms.SearchForm(self.request.GET) kwargs["query"] = self.query kwargs.update(kwargs_article) kwargs.update(kwargs_listview) kwargs["selected_tab"] = "attachments" return kwargs
class RevisionChangeView(ArticleMixin, RedirectView): permanent = False @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): self.image = get_object_or_404(models.Image, article=article, id=kwargs.get("image_id", None)) self.revision = get_object_or_404(models.ImageRevision, plugin__article=article, id=kwargs.get("rev_id", None)) return ArticleMixin.dispatch(self, request, article, *args, **kwargs) def get_redirect_url(self, **kwargs): self.image.current_revision = self.revision self.image.save() messages.info( self.request, _("%(file)s has been changed to revision #%(revision)d") % { "file": self.image.current_revision.imagerevision.get_filename(), "revision": self.revision.revision_number, }, ) if self.urlpath: return reverse("wiki:images_index", kwargs={"path": self.urlpath.path}) return reverse("wiki:images_index", kwargs={"article_id": self.article.id})
class Source(ArticleMixin, TemplateView): template_name = "wiki/source.html" @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, *args, **kwargs): return super().dispatch(request, article, *args, **kwargs) def get_context_data(self, **kwargs): kwargs["selected_tab"] = "source" return super().get_context_data(**kwargs)
class AttachmentDeleteView(ArticleMixin, FormView): form_class = forms.DeleteForm template_name = "wiki/plugins/attachments/delete.html" @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article) if not self.attachment.can_delete(request.user): return response_forbidden(request, article, kwargs.get("urlpath", None)) return super().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 if self.attachment.current_revision else None) revision.description = ( self.attachment.current_revision.description if self.attachment.current_revision else "") revision.save() self.attachment.current_revision = revision self.attachment.save() self.article.clear_cache() messages.info( self.request, _("The file %s was deleted.") % self.attachment.original_filename, ) else: self.attachment.articles.remove(self.article) messages.info( self.request, _("This article is no longer related to the file %s.") % self.attachment.original_filename, ) self.article.clear_cache() return redirect("wiki:get", path=self.urlpath.path, article_id=self.article.id) def get_context_data(self, **kwargs): kwargs["attachment"] = self.attachment kwargs["selected_tab"] = "attachments" if "form" not in kwargs: kwargs["form"] = self.get_form() return super().get_context_data(**kwargs)
class Preview(ArticleMixin, TemplateView): template_name = "wiki/preview_inline.html" @method_decorator(xframe_options_sameorigin) @method_decorator(get_article(can_read=True, deleted_contents=True)) def dispatch(self, request, article, *args, **kwargs): revision_id = request.GET.get("r", None) self.title = None self.content = None self.preview = False if revision_id: try: revision_id = int(revision_id) except ValueError: # ValueError only happens because someone put garbage in the # querystring raise Http404() self.revision = get_object_or_404(models.ArticleRevision, article=article, id=revision_id) else: self.revision = None return super().dispatch(request, article, *args, **kwargs) def post(self, request, *args, **kwargs): edit_form = forms.EditForm(request, self.article.current_revision, request.POST, preview=True) if edit_form.is_valid(): self.title = edit_form.cleaned_data["title"] self.content = edit_form.cleaned_data["content"] self.preview = True return super().get(request, *args, **kwargs) def get(self, request, *args, **kwargs): if self.revision and not self.title: self.title = self.revision.title if self.revision and not self.content: self.content = self.revision.content return super().get(request, *args, **kwargs) def get_context_data(self, **kwargs): kwargs["title"] = self.title kwargs["revision"] = self.revision kwargs["content"] = self.content kwargs["preview"] = self.preview return ArticleMixin.get_context_data(self, **kwargs)
class DeleteView(ArticleMixin, RedirectView): permanent = False @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): self.image = get_object_or_404(models.Image, article=article, id=kwargs.get("image_id", None)) self.restore = kwargs.get("restore", False) return ArticleMixin.dispatch(self, request, article, *args, **kwargs) def get_redirect_url(self, **kwargs): if not self.image.current_revision: logger.critical( "Encountered an image without current revision set, ID: {}". format(self.image.id)) latest_revision = RevisionPluginRevision.objects.filter( plugin=self.image).latest("pk") self.image.current_revision = latest_revision new_revision = models.ImageRevision() new_revision.inherit_predecessor(self.image) new_revision.set_from_request(self.request) new_revision.revision_number = RevisionPluginRevision.objects.filter( plugin=self.image).count() new_revision.deleted = not self.restore new_revision.save() self.image.current_revision = new_revision self.image.save() if self.restore: messages.info( self.request, _("%s has been restored") % new_revision.get_filename()) else: messages.info( self.request, _("%s has been marked as deleted") % new_revision.get_filename(), ) if self.urlpath: return reverse("wiki:images_index", kwargs={"path": self.urlpath.path}) return reverse("wiki:images_index", kwargs={"article_id": self.article.id})
class Dir(ListView, ArticleMixin): template_name = "wiki/dir.html" allow_empty = True context_object_name = "directory" model = models.URLPath paginator_class = WikiPaginator paginate_by = 30 @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, *args, **kwargs): self.filter_form = forms.DirFilterForm(request.GET) if self.filter_form.is_valid(): self.query = self.filter_form.cleaned_data["query"] else: self.query = None return super().dispatch(request, article, *args, **kwargs) def get_queryset(self): children = self.urlpath.get_children().can_read(self.request.user) if self.query: children = children.filter( Q(article__current_revision__title__icontains=self.query) | Q(slug__icontains=self.query)) if not self.article.can_moderate(self.request.user): children = children.active() children = children.select_related_common().order_by( "article__current_revision__title") return children def get_context_data(self, **kwargs): kwargs_article = ArticleMixin.get_context_data(self, **kwargs) kwargs_listview = ListView.get_context_data(self, **kwargs) kwargs.update(kwargs_article) kwargs.update(kwargs_listview) kwargs["filter_query"] = self.query kwargs["filter_form"] = self.filter_form # Update each child's ancestor cache so the lookups don't have # to be repeated. updated_children = kwargs[self.context_object_name] for child in updated_children: child.set_cached_ancestors_from_parent(self.urlpath) kwargs[self.context_object_name] = updated_children return kwargs
class RevisionAddView(ArticleMixin, FormView): template_name = "wiki/plugins/images/revision_add.html" form_class = forms.RevisionForm @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): self.image = get_object_or_404(models.Image, article=article, id=kwargs.get("image_id", None)) if not self.image.can_write(request.user): return redirect(wiki_settings.LOGIN_URL) return ArticleMixin.dispatch(self, request, article, *args, **kwargs) def get_form_kwargs(self, **kwargs): kwargs = super().get_form_kwargs(**kwargs) kwargs["image"] = self.image kwargs["request"] = self.request return kwargs def get_context_data(self, **kwargs): # Needed since Django 1.9 because get_context_data is no longer called # with the form instance if "form" not in kwargs: kwargs["form"] = self.get_form() kwargs = super().get_context_data(**kwargs) kwargs["image"] = self.image return kwargs def form_valid(self, form, **kwargs): form.save() messages.info( self.request, _("%(file)s has been saved.") % { "file": self.image.current_revision.imagerevision.get_filename(), }, ) if self.urlpath: return redirect("wiki:edit", path=self.urlpath.path) return redirect("wiki:edit", article_id=self.article.id)
class AttachmentChangeRevisionView(ArticleMixin, View): form_class = forms.AttachmentForm template_name = "wiki/plugins/attachments/replace.html" @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, attachment_id, revision_id, *args, **kwargs): if article.can_moderate(request.user): self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article) else: self.attachment = get_object_or_404( models.Attachment.objects.active(), id=attachment_id, articles=article) self.revision = get_object_or_404(models.AttachmentRevision, id=revision_id, attachment__articles=article) return super().dispatch(request, article, *args, **kwargs) def post(self, request, *args, **kwargs): self.attachment.current_revision = self.revision self.attachment.save() self.article.clear_cache() messages.success( self.request, _("Current revision changed for %s.") % self.attachment.original_filename, ) return redirect("wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id) def get_context_data(self, **kwargs): kwargs["selected_tab"] = "attachments" if "form" not in kwargs: kwargs["form"] = self.get_form() return ArticleMixin.get_context_data(self, **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 article.can_moderate(request.user): self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article) else: self.attachment = get_object_or_404( models.Attachment.objects.active(), id=attachment_id, articles=article) return super().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") kwargs["selected_tab"] = "attachments" return super().get_context_data(**kwargs)
class QueryUrlPath(View): @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, *args, **kwargs): max_num = kwargs.pop("max_num", 20) query = request.GET.get("query", None) matches = [] if query: matches = (models.URLPath.objects.can_read( request.user).active().filter( article__current_revision__title__contains=query, article__current_revision__deleted=False, )) matches = matches.select_related_common() matches = [ "[{title:s}](wiki:{url:s})".format( title=m.article.current_revision.title, url="/" + m.path.strip("/")) for m in matches[:max_num] ] return object_to_json_response(matches)
class History(ListView, ArticleMixin): template_name = "wiki/history.html" allow_empty = True context_object_name = "revisions" paginator_class = WikiPaginator paginate_by = 10 def get_queryset(self): return models.ArticleRevision.objects.filter( article=self.article).order_by("-created") def get_context_data(self, **kwargs): # Is this a bit of a hack? Use better inheritance? kwargs_article = ArticleMixin.get_context_data(self, **kwargs) kwargs_listview = ListView.get_context_data(self, **kwargs) kwargs.update(kwargs_article) kwargs.update(kwargs_listview) kwargs["selected_tab"] = "history" return kwargs @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, *args, **kwargs): return super().dispatch(request, article, *args, **kwargs)
class AttachmentDownloadView(ArticleMixin, View): @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): if article.can_moderate(request.user): self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article) else: self.attachment = get_object_or_404( models.Attachment.objects.active(), 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().dispatch(request, article, *args, **kwargs) def get(self, request, *args, **kwargs): if self.revision: if settings.USE_LOCAL_PATH: try: return send_file( request, self.revision.file.path, self.revision.created, self.attachment.original_filename, ) except OSError: pass else: return HttpResponseRedirect(self.revision.file.url) raise Http404
class EditSection(EditView): def locate_section(self, article, text): """Search for the header self.location (which is not deeper than settings.MAX_LEVEL) in text, compare the header text with self.header_id, and return the start position and the end position+1 of the complete section started by the header. """ text = text.replace("\r\n", " \n").replace("\r", "\n") + "\n\n" text_len = len(text) headers = [] pos = 0 while pos < text_len: # Get meta information and start position of the next section header = FindHeader(text, pos) pos = header.pos if pos >= text_len: break if header.level > settings.MAX_LEVEL: continue headers.append(header) for e in get_markdown_extensions(): if isinstance(e, EditSectionExtension): e.config["headers"] = headers e.config["location"] = self.location e.config["header_id"] = self.header_id article_markdown(text, article) return e.config["location"] return None def _redirect_to_article(self): if self.urlpath: return redirect("wiki:get", path=self.urlpath.path) return redirect("wiki:get", article_id=self.article.id) @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): self.location = kwargs.pop("location", 0) self.header_id = kwargs.pop("header", 0) self.urlpath = kwargs.get("urlpath") kwargs["path"] = self.urlpath.path if request.method == "GET": text = article.current_revision.content location = self.locate_section(article, text) if location: self.orig_section = text[location[0]:location[1]] # Pass the to be used content to EditSection kwargs["content"] = self.orig_section request.session["editsection_content"] = self.orig_section else: messages.error( request, " ".format(ERROR_SECTION_CHANGED, ERROR_TRY_AGAIN)) return self._redirect_to_article() else: kwargs["content"] = request.session.get("editsection_content") self.orig_section = kwargs.get("content") return super().dispatch(request, article, *args, **kwargs) def form_valid(self, form): super().form_valid(form) section = self.article.current_revision.content if not section.endswith("\n"): section += "\r\n\r\n" text = get_object_or_404( models.ArticleRevision, article=self.article, id=self.article.current_revision.previous_revision.id, ).content location = self.locate_section(self.article, text) if location: if self.orig_section != text[location[0]:location[1]]: messages.warning( self.request, " ".format(ERROR_SECTION_CHANGED, ERROR_SECTION_UNSAVED, ERROR_TRY_AGAIN), ) # Include the edited section into the complete previous article self.article.current_revision.content = (text[0:location[0]] + section + text[location[1]:]) self.article.current_revision.save() else: # Back to the version before replacing the article with the section self.article.current_revision = ( self.article.current_revision.previous_revision) self.article.save() messages.error(self.request, " ".format(ERROR_ARTICLE_CHANGED, ERROR_TRY_AGAIN)) return self._redirect_to_article()
class Edit(ArticleMixin, FormView): """Edit an article and process sidebar plugins.""" form_class = forms.EditForm template_name = "wiki/edit.html" @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): self.orig_content = kwargs.pop("content", None) self.sidebar_plugins = plugin_registry.get_sidebar() self.sidebar = [] return super().dispatch(request, article, *args, **kwargs) def get_initial(self): initial = FormView.get_initial(self) for field_name in ["title", "content"]: session_key = "unsaved_article_%s_%d" % (field_name, self.article.id) if session_key in self.request.session: content = self.request.session[session_key] initial[field_name] = content del self.request.session[session_key] return initial def get_form(self, form_class=None): """ Checks from querystring data that the edit form is actually being saved, otherwise removes the 'data' and 'files' kwargs from form initialisation. """ if form_class is None: form_class = self.get_form_class() kwargs = self.get_form_kwargs() if (self.request.POST.get("save", "") != "1" and self.request.POST.get("preview") != "1"): kwargs["data"] = None kwargs["files"] = None kwargs["no_clean"] = True kwargs["content"] = self.orig_content return form_class(self.request, self.article.current_revision, **kwargs) def get_sidebar_form_classes(self): """Returns dictionary of form classes for the sidebar. If no form class is specified, puts None in dictionary. Keys in the dictionary are used to identify which form is being saved.""" form_classes = {} for cnt, plugin in enumerate(self.sidebar_plugins): form_classes["form%d" % cnt] = ( plugin, plugin.sidebar.get("form_class", None), ) return form_classes def get(self, request, *args, **kwargs): # Generate sidebar forms self.sidebar_forms = [] for form_id, (plugin, Form) in self.get_sidebar_form_classes().items(): if Form: form = Form(self.article, self.request.user) setattr(form, "form_id", form_id) else: form = None self.sidebar.append((plugin, form)) return super().get(request, *args, **kwargs) def post(self, request, *args, **kwargs): # Generate sidebar forms self.sidebar_forms = [] for form_id, (plugin, Form) in self.get_sidebar_form_classes().items(): if Form: if form_id == self.request.GET.get("f", None): form = Form( self.article, self.request, data=self.request.POST, files=self.request.FILES, ) if form.is_valid(): form.save() usermessage = form.get_usermessage() if usermessage: messages.success(self.request, usermessage) else: messages.success(self.request, _("Your changes were saved.")) title = form.cleaned_data["unsaved_article_title"] content = form.cleaned_data["unsaved_article_content"] orig_content = self.orig_content if not orig_content: orig_content = self.article.current_revision.content if (title != self.article.current_revision.title or content != orig_content): request.session["unsaved_article_title_%d" % self.article.id] = title request.session["unsaved_article_content_%d" % self.article.id] = content messages.warning( request, _("Please note that your article text has not yet been saved!" ), ) if self.urlpath: return redirect("wiki:edit", path=self.urlpath.path) return redirect("wiki:edit", article_id=self.article.id) else: form = Form(self.article, self.request) setattr(form, "form_id", form_id) else: form = None self.sidebar.append((plugin, form)) return super().post(request, *args, **kwargs) def form_valid(self, form): """Create a new article revision when the edit form is valid (does not concern any sidebar forms!).""" revision = models.ArticleRevision() revision.inherit_predecessor(self.article) revision.title = form.cleaned_data["title"] revision.content = form.cleaned_data["content"] revision.user_message = form.cleaned_data["summary"] revision.deleted = False revision.set_from_request(self.request) self.article.add_revision(revision) messages.success( self.request, _("A new revision of the article was successfully added.")) return self.get_success_url() def get_success_url(self): """Go to the article view page when the article has been saved""" if self.urlpath: return redirect("wiki:get", path=self.urlpath.path) return redirect("wiki:get", article_id=self.article.id) def get_context_data(self, **kwargs): kwargs["form"] = self.get_form() kwargs["edit_form"] = kwargs["form"] kwargs["editor"] = editors.getEditor() kwargs["selected_tab"] = "edit" kwargs["sidebar"] = self.sidebar return super().get_context_data(**kwargs)
class Move(ArticleMixin, FormView): form_class = forms.MoveForm template_name = "wiki/move.html" @method_decorator(login_required) @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, *args, **kwargs): return super().dispatch(request, article, *args, **kwargs) def get_form(self, form_class=None): if form_class is None: form_class = self.get_form_class() kwargs = self.get_form_kwargs() return form_class(**kwargs) def get_context_data(self, **kwargs): kwargs["form"] = self.get_form() kwargs["root_path"] = models.URLPath.root() return super().get_context_data(**kwargs) @transaction.atomic def form_valid(self, form): if not self.urlpath.parent: messages.error( self.request, _("This article cannot be moved because it is a root article." ), ) return redirect("wiki:get", article_id=self.article.id) dest_path = get_object_or_404(models.URLPath, pk=form.cleaned_data["destination"]) tmp_path = dest_path while tmp_path.parent: if tmp_path == self.urlpath: messages.error( self.request, _("This article cannot be moved to a child of itself."), ) return redirect("wiki:move", article_id=self.article.id) tmp_path = tmp_path.parent # Clear cache to update article lists (Old links) for ancestor in self.article.ancestor_objects(): ancestor.article.clear_cache() # Save the old path for later old_path = self.urlpath.path self.urlpath.parent = dest_path self.urlpath.slug = form.cleaned_data["slug"] self.urlpath.save() # Reload url path form database self.urlpath = models.URLPath.objects.get(pk=self.urlpath.pk) # Use a copy of ourself (to avoid cache) and update article links again for ancestor in models.Article.objects.get( pk=self.article.pk).ancestor_objects(): ancestor.article.clear_cache() # Create a redirect page for every moved article # /old-slug # /old-slug/child # /old-slug/child/grand-child if form.cleaned_data["redirect"]: # NB! Includes self! descendants = list( self.urlpath.get_descendants( include_self=True).order_by("level")) root_len = len(descendants[0].path) for descendant in descendants: # Without this descendant.get_ancestors() and as a result # descendant.path is wrong after the first create_article() due # to path caching descendant.refresh_from_db() dst_path = descendant.path src_path = urljoin(old_path, dst_path[root_len:]) src_len = len(src_path) pos = src_path.rfind("/", 0, src_len - 1) slug = src_path[pos + 1:src_len - 1] parent_urlpath = models.URLPath.get_by_path( src_path[0:max(pos, 0)]) link = "[wiki:/{path}](wiki:/{path})".format(path=dst_path) urlpath_new = models.URLPath._create_urlpath_from_request( self.request, self.article, parent_urlpath, slug, _("Moved: {title}").format(title=descendant.article), _("Article moved to {link}").format(link=link), _("Created redirect (auto)"), ) urlpath_new.moved_to = descendant urlpath_new.save() messages.success( self.request, ngettext( "Article successfully moved! Created {n} redirect.", "Article successfully moved! Created {n} redirects.", len(descendants), ).format(n=len(descendants)), ) else: messages.success(self.request, _("Article successfully moved!")) return redirect("wiki:get", path=self.urlpath.path)
class Create(FormView, ArticleMixin): form_class = forms.CreateForm template_name = "wiki/create.html" @method_decorator(get_article(can_write=True, can_create=True)) def dispatch(self, request, article, *args, **kwargs): return super().dispatch(request, article, *args, **kwargs) def get_form(self, form_class=None): """ Returns an instance of the form to be used in this view. """ if form_class is None: form_class = self.get_form_class() kwargs = self.get_form_kwargs() initial = kwargs.get("initial", {}) initial["slug"] = self.request.GET.get("slug", None) kwargs["initial"] = initial form = form_class(self.request, self.urlpath, **kwargs) form.fields["slug"].widget = forms.TextInputPrepend( prepend="/" + self.urlpath.path, attrs={ # Make patterns force lowercase if we are case insensitive to bless the user with a # bit of strictness, anyways "pattern": "[a-z0-9_-]+" if not settings.URL_CASE_SENSITIVE else "[a-zA-Z0-9_-]+", "title": "Lowercase letters, numbers, hyphens and underscores" if not settings.URL_CASE_SENSITIVE else "Letters, numbers, hyphens and underscores", }, ) return form def get_object(self, project_name): # That works! try: return Project.objects.get(short_name=project_name) except Project.DoesNotExist: pass def form_valid(self, form): try: self.newpath = models.URLPath._create_urlpath_from_request( self.request, self.article, self.urlpath, form.cleaned_data["slug"], form.cleaned_data["title"], form.cleaned_data["content"], form.cleaned_data["summary"], ) print(form.cleaned_data["project"]) print(self.newpath.article) print(self.get_object(form.cleaned_data["project"])) self.newpath.article.set_project(form.cleaned_data["project"]) messages.success( self.request, _("New article '%s' created.") % self.newpath.article.current_revision.title, ) # TODO: Handle individual exceptions better and give good feedback. except Exception as e: log.exception("Exception creating article.") if self.request.user.is_superuser: messages.error( self.request, _("There was an error creating this article: %s") % str(e), ) else: messages.error(self.request, _("There was an error creating this article.")) return redirect("wiki:get", "") return self.get_success_url() def get_success_url(self): return redirect("wiki:get", self.newpath.path) def get_context_data(self, **kwargs): c = ArticleMixin.get_context_data(self, **kwargs) c["form"] = self.get_form() c["parent_urlpath"] = self.urlpath c["parent_article"] = self.article c["create_form"] = c.pop("form", None) c["editor"] = editors.getEditor() return c
class Deleted(Delete): """Tell a user that an article has been deleted. If user has permissions, let user restore and possibly purge the deleted article and children.""" template_name = "wiki/deleted.html" form_class = forms.DeleteForm @method_decorator(get_article(can_read=True, deleted_contents=True)) def dispatch(self, request, article, *args, **kwargs): self.urlpath = kwargs.get("urlpath", None) self.article = article if self.urlpath: deleted_ancestor = self.urlpath.first_deleted_ancestor() if deleted_ancestor is None: # No one is deleted! return redirect("wiki:get", path=self.urlpath.path) elif deleted_ancestor != self.urlpath: # An ancestor was deleted, so redirect to that deleted page return redirect("wiki:deleted", path=deleted_ancestor.path) else: if not article.current_revision.deleted: return redirect("wiki:get", article_id=article.id) # Restore if request.GET.get("restore", False): can_restore = not article.current_revision.locked and article.can_delete( request.user) can_restore = can_restore or article.can_moderate(request.user) if can_restore: revision = models.ArticleRevision() revision.inherit_predecessor(self.article) revision.set_from_request(request) revision.deleted = False revision.automatic_log = _("Restoring article") self.article.add_revision(revision) messages.success( request, _('The article "%s" and its children are now restored.') % revision.title, ) if self.urlpath: return redirect("wiki:get", path=self.urlpath.path) else: return redirect("wiki:get", article_id=article.id) return super().dispatch1(request, article, *args, **kwargs) def get_initial(self): return { "revision": self.article.current_revision, "purge": True, } def get_context_data(self, **kwargs): kwargs["purge_form"] = self.get_form() kwargs["form"] = kwargs["purge_form"] return super().get_context_data(**kwargs)
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 article.can_moderate(request.user): self.attachments = (models.Attachment.objects.filter( articles=article, current_revision__deleted=False).exclude( current_revision__file=None).order_by("original_filename")) self.form_class = forms.AttachmentArchiveForm else: self.attachments = models.Attachment.objects.active().filter( articles=article) # Fixing some weird transaction issue caused by adding commit_manually # to form_valid return super().dispatch(request, article, *args, **kwargs) def form_valid(self, form): if (self.request.user.is_anonymous and not settings.ANONYMOUS or not self.article.can_write(self.request.user) or self.article.current_revision.locked): return response_forbidden(self.request, self.article, self.urlpath) attachment_revision = form.save() if isinstance(attachment_revision, list): messages.success( self.request, _("Successfully added: %s") % (", ".join([ar.get_filename() for ar in attachment_revision])), ) else: messages.success( self.request, _("%s was successfully added.") % attachment_revision.get_filename(), ) self.article.clear_cache() return redirect("wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["article"] = self.article kwargs["request"] = self.request return kwargs def get_context_data(self, **kwargs): # Needed since Django 1.9 because get_context_data is no longer called # with the form instance if "form" not in kwargs: kwargs["form"] = self.get_form() kwargs["attachments"] = self.attachments kwargs["deleted_attachments"] = models.Attachment.objects.filter( articles=self.article, current_revision__deleted=True) kwargs["search_form"] = forms.SearchForm() kwargs["selected_tab"] = "attachments" kwargs["anonymous_disallowed"] = (self.request.user.is_anonymous and not settings.ANONYMOUS) return super().get_context_data(**kwargs)
class AttachmentReplaceView(ArticleMixin, FormView): form_class = forms.AttachmentForm template_name = "wiki/plugins/attachments/replace.html" @method_decorator(get_article(can_write=True, not_locked=True)) def dispatch(self, request, article, attachment_id, *args, **kwargs): if request.user.is_anonymous and not settings.ANONYMOUS: return response_forbidden(request, article, kwargs.get("urlpath", None)) if article.can_moderate(request.user): self.attachment = get_object_or_404(models.Attachment, id=attachment_id, articles=article) self.can_moderate = True else: self.attachment = get_object_or_404( models.Attachment.objects.active(), id=attachment_id, articles=article) self.can_moderate = False return super().dispatch(request, article, *args, **kwargs) def get_form_class(self): if self.can_moderate: return forms.AttachmentReplaceForm else: return forms.AttachmentForm def form_valid(self, form): try: attachment_revision = form.save(commit=True) 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, _("%s uploaded and replaces old attachment.") % attachment_revision.get_filename(), ) self.article.clear_cache() except models.IllegalFileExtension as e: messages.error(self.request, _("Your file could not be saved: %s") % e) return redirect( "wiki:attachments_replace", attachment_id=self.attachment.id, path=self.urlpath.path, article_id=self.article.id, ) if self.can_moderate: if form.cleaned_data["replace"]: # form has no cleaned_data field unless self.can_moderate is True try: most_recent_revision = self.attachment.attachmentrevision_set.exclude( id=attachment_revision.id, created__lte=attachment_revision.created, ).latest() most_recent_revision.delete() except ObjectDoesNotExist: msg = "{attachment} does not contain any revisions.".format( attachment=str(self.attachment.original_filename)) messages.error(self.request, msg) return redirect("wiki:attachments_index", path=self.urlpath.path, article_id=self.article.id) def get_form(self, form_class=None): form = super().get_form(form_class=form_class) form.fields["file"].help_text = _( "Your new file will automatically be renamed to match the file already present. Files with different extensions are not allowed." ) return form def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["article"] = self.article kwargs["request"] = self.request kwargs["attachment"] = self.attachment return kwargs def get_initial(self, **kwargs): return {"description": self.attachment.current_revision.description} def get_context_data(self, **kwargs): if "form" not in kwargs: kwargs["form"] = self.get_form() kwargs["attachment"] = self.attachment kwargs["selected_tab"] = "attachments" return super().get_context_data(**kwargs)
class Settings(ArticleMixin, TemplateView): permission_form_class = forms.PermissionsForm template_name = "wiki/settings.html" @method_decorator(login_required) @method_decorator(get_article(can_read=True)) def dispatch(self, request, article, *args, **kwargs): return super().dispatch(request, article, *args, **kwargs) def get_form_classes(self): """ Return all settings forms that can be filled in """ settings_forms = [] if permissions.can_change_permissions(self.article, self.request.user): settings_forms.append(self.permission_form_class) plugin_forms = [F for F in plugin_registry.get_settings_forms()] plugin_forms.sort(key=lambda form: form.settings_order) settings_forms += plugin_forms for i in range(len(settings_forms)): # TODO: Do not set an attribute on a form class - this # could be mixed up with a different instance # Use strategy from Edit view... setattr(settings_forms[i], "action", "form%d" % i) return settings_forms def post(self, *args, **kwargs): self.forms = [] for form_class in self.get_form_classes(): if form_class.action == self.request.GET.get("f", None): form = form_class(self.article, self.request, self.request.POST) if form.is_valid(): form.save() usermessage = form.get_usermessage() if usermessage: messages.success(self.request, usermessage) if self.urlpath: return redirect("wiki:settings", path=self.urlpath.path) return redirect("wiki:settings", article_id=self.article.id) else: form = form_class(self.article, self.request) self.forms.append(form) return super().get(*args, **kwargs) def get(self, *args, **kwargs): self.forms = [] # There is a bug where articles fetched with select_related have bad boolean field https://code.djangoproject.com/ticket/15040 # We fetch a fresh new article for this reason new_article = models.Article.objects.get(id=self.article.id) for Form in self.get_form_classes(): self.forms.append(Form(new_article, self.request)) return super().get(*args, **kwargs) def get_success_url(self): if self.urlpath: return redirect("wiki:settings", path=self.urlpath.path) return redirect("wiki:settings", article_id=self.article.id) def get_context_data(self, **kwargs): kwargs["selected_tab"] = "settings" kwargs["forms"] = self.forms return super().get_context_data(**kwargs)
class MergeView(View): preview = False template_name = "wiki/preview_inline.html" template_error_name = "wiki/error.html" urlpath = None @method_decorator(get_article(can_write=True)) def dispatch(self, request, article, revision_id, *args, **kwargs): return super().dispatch(request, article, revision_id, *args, **kwargs) def get(self, request, article, revision_id, *args, **kwargs): revision = get_object_or_404(models.ArticleRevision, article=article, id=revision_id) current_text = (article.current_revision.content if article.current_revision else "") new_text = revision.content content = simple_merge(current_text, new_text) # Save new revision if not self.preview: old_revision = article.current_revision if revision.deleted: c = { "error_msg": _("You cannot merge with a deleted revision"), "article": article, "urlpath": self.urlpath, } return render(request, self.template_error_name, context=c) new_revision = models.ArticleRevision() new_revision.inherit_predecessor(article) new_revision.deleted = False new_revision.locked = False new_revision.title = article.current_revision.title new_revision.content = content new_revision.automatic_log = _( "Merge between revision #%(r1)d and revision #%(r2)d") % { "r1": revision.revision_number, "r2": old_revision.revision_number } article.add_revision(new_revision, save=True) old_revision.simpleplugin_set.all().update( article_revision=new_revision) revision.simpleplugin_set.all().update( article_revision=new_revision) messages.success( request, _("A new revision was created: Merge between revision #%(r1)d and revision #%(r2)d" ) % { "r1": revision.revision_number, "r2": old_revision.revision_number }, ) if self.urlpath: return redirect("wiki:edit", path=self.urlpath.path) else: return redirect("wiki:edit", article_id=article.id) c = { "article": article, "title": article.current_revision.title, "revision": None, "merge1": revision, "merge2": article.current_revision, "merge": True, "content": content, } return render(request, self.template_name, c)
class Delete(FormView, ArticleMixin): form_class = forms.DeleteForm template_name = "wiki/delete.html" @method_decorator( get_article(can_write=True, not_locked=True, can_delete=True)) def dispatch(self, request, article, *args, **kwargs): return self.dispatch1(request, article, *args, **kwargs) def dispatch1(self, request, article, *args, **kwargs): """Deleted view needs to access this method without a decorator, therefore it is separate.""" urlpath = kwargs.get("urlpath", None) # Where to go after deletion... self.next = "" self.cannot_delete_root = False if urlpath and urlpath.parent: self.next = reverse("wiki:get", kwargs={"path": urlpath.parent.path}) elif urlpath: # We are a urlpath with no parent. This is the root self.cannot_delete_root = True else: # We have no urlpath. Get it if a urlpath exists for art_obj in article.articleforobject_set.filter(is_mptt=True): if art_obj.content_object.parent: self.next = reverse( "wiki:get", kwargs={ "article_id": art_obj.content_object.parent.article.id }, ) else: self.cannot_delete_root = True return super().dispatch(request, article, *args, **kwargs) def get_initial(self): return {"revision": self.article.current_revision} def get_form(self, form_class=None): form = super().get_form(form_class=form_class) if self.article.can_moderate(self.request.user): form.fields["purge"].widget = forms.forms.CheckboxInput() return form def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["article"] = self.article kwargs["has_children"] = bool(self.children_slice) return kwargs def form_valid(self, form): cd = form.cleaned_data purge = cd["purge"] # If we are purging, only moderators can delete articles with children cannot_delete_children = False can_moderate = self.article.can_moderate(self.request.user) if purge and self.children_slice and not can_moderate: cannot_delete_children = True if self.cannot_delete_root or cannot_delete_children: messages.error( self.request, _("This article cannot be deleted because it has children or is a root article." ), ) return redirect("wiki:get", article_id=self.article.id) if can_moderate and purge: # First, remove children if self.urlpath: self.urlpath.delete_subtree() self.article.delete() messages.success( self.request, _("This article together with all its contents are now completely gone! Thanks!" ), ) else: revision = models.ArticleRevision() revision.inherit_predecessor(self.article) revision.set_from_request(self.request) revision.deleted = True self.article.add_revision(revision) messages.success( self.request, _('The article "%s" is now marked as deleted! Thanks for keeping the site free from unwanted material!' ) % revision.title, ) return self.get_success_url() def get_success_url(self): return redirect(self.next) def get_context_data(self, **kwargs): cannot_delete_children = False if self.children_slice and not self.article.can_moderate( self.request.user): cannot_delete_children = True kwargs["delete_form"] = self.get_form() kwargs["form"] = kwargs["delete_form"] kwargs["cannot_delete_root"] = self.cannot_delete_root kwargs["delete_children"] = self.children_slice[:20] kwargs["delete_children_more"] = len(self.children_slice) > 20 kwargs["cannot_delete_children"] = cannot_delete_children return super().get_context_data(**kwargs)