Rewrite django-announcements #2

Merged
merged 3 commits into from Jul 1, 2012
View
10 announcements/__init__.py
@@ -1,9 +1 @@
-VERSION = (0, 1, 0, "final")
-
-def get_version():
- if VERSION[3] != "final":
- return "%s.%s.%s%s" % (VERSION[0], VERSION[1], VERSION[2], VERSION[3])
- else:
- return "%s.%s.%s" % (VERSION[0], VERSION[1], VERSION[2])
-
-__version__ = get_version()
+__version__ = "0.2.1"
View
11 announcements/admin.py
@@ -1,20 +1,14 @@
from django.contrib import admin
-from announcements.models import Announcement
-from announcements.forms import AnnouncementAdminForm
+from announcements.models import Announcement, Dismissal
class AnnouncementAdmin(admin.ModelAdmin):
list_display = ("title", "creator", "creation_date", "members_only")
list_filter = ("members_only",)
- form = AnnouncementAdminForm
fieldsets = [
(None, {
- "fields": ["title", "content", "site_wide", "members_only"],
- }),
-
- ("Manage announcement", {
- "fields": ["send_now"],
+ "fields": ["title", "content", "site_wide", "members_only", "publish_start", "publish_end", "dismissal_type"],
}),
]
@@ -26,3 +20,4 @@ def save_model(self, request, obj, form, change):
admin.site.register(Announcement, AnnouncementAdmin)
+admin.site.register(Dismissal)
View
9 announcements/context_processors.py
@@ -1,9 +0,0 @@
-from announcements.models import current_announcements_for_request
-
-
-def site_wide_announcements(request):
- """
- Adds the site-wide announcements to the global context of templates.
- """
- ctx = {"site_wide_announcements": current_announcements_for_request(request, site_wide=True)}
- return ctx
View
14 announcements/feeds.py
@@ -1,3 +1,7 @@
+import datetime
+
+from django.db.models import Q
+
from atomformat import Feed
from announcements.models import Announcement
@@ -16,7 +20,15 @@ class AnnouncementsBase(Feed):
# def item_links
def items(self):
- return Announcement.objects.order_by("-creation_date")[:10]
+ return Announcement.objects.filter(
+ publish_start__lte=datetime.datetime.now()
+ ).filter(
+ Q(publish_end__isnull=True)|Q(publish_end__gt=datetime.datetime.now())
+ ).filter(
+ site_wide=True
+ ).exclude(
+ members_only=True
+ ).order_by("-creation_date")[:10]
def item_title(self, item):
return item.title
View
42 announcements/forms.py
@@ -1,40 +1,18 @@
from django import forms
-from django.contrib.auth.models import User
-from django.utils.translation import ugettext_lazy as _
-
-try:
- from notification import models as notification
-except ImportError:
- notification = None
from announcements.models import Announcement
-class AnnouncementAdminForm(forms.ModelForm):
- """
- A custom form for the admin of the Announcement model. Has an extra field
- called send_now that when checked will send out the announcement allowing
- the user to decide when that happens.
- """
-
- send_now = forms.BooleanField(required=False,
- help_text=_("Tick this box to send out this announcement now."))
+class AnnouncementForm(forms.ModelForm):
class Meta:
model = Announcement
- exclude = ("creator", "creation_date")
-
- def save(self, commit=True):
- """
- Checks the send_now field in the form and when True sends out the
- announcement through notification if present.
- """
-
- announcement = super(AnnouncementAdminForm, self).save(commit)
- if self.cleaned_data["send_now"]:
- if notification:
- users = User.objects.all()
- notification.send(users, "announcement", {
- "announcement": announcement,
- }, on_site=False, queue=True)
- return announcement
+ fields = [
+ "title",
+ "content",
+ "site_wide",
+ "members_only",
+ "dismissal_type",
+ "publish_start",
+ "publish_end"
+ ]
View
16 announcements/management.py
@@ -1,16 +0,0 @@
-from django.db.models import get_models, signals
-
-
-try:
- from notification import models as notification
-
- def create_notice_types(app, created_models, verbosity, **kwargs):
- """
- Create the announcement notice type for sending notifications when
- announcements occur.
- """
- notification.create_notice_type("announcement", "Announcement", "you have received an announcement")
-
- signals.post_syncdb.connect(create_notice_types, sender=notification)
-except ImportError:
- print "Skipping creation of NoticeTypes as notification app not found"
View
30 announcements/mixins.py
@@ -0,0 +1,30 @@
+from django.conf import settings
+from django.utils.importlib import import_module
+
+
+def _resolve(mixin_setting):
+ if isinstance(mixin_setting, basestring):
+ try:
+ mod_name, klass_name = mixin_setting.rsplit(".", 1)
+ except ValueError:
+ raise Exception("Improperly configured.")
+ try:
+ mod = import_module(mod_name)
+ except ImportError:
+ raise Exception("Could not import %s" % mod_name)
+ try:
+ klass = getattr(mod, klass_name)
+ except AttributeError:
+ raise Exception("The module '%s' does not contain '%s'." % (mod_name, klass_name))
+ mixin_setting = klass
+ return mixin_setting
+
+
+class DefaultProtectedMixin(object):
+
+ pass
+
+
+ProtectedMixin = _resolve(
+ getattr(settings, "ANNOUNCEMENTS_PROTECTED_MIXIN", DefaultProtectedMixin)
+)
View
83 announcements/models.py
@@ -1,63 +1,41 @@
-from datetime import datetime
+import datetime
from django.db import models
-from django.conf import settings
from django.contrib.auth.models import User
from django.core.urlresolvers import reverse
from django.utils.translation import ugettext_lazy as _
-try:
- set
-except NameError:
- from sets import Set as set # Python 2.3 fallback
-
-
-class AnnouncementManager(models.Manager):
- """
- A basic manager for dealing with announcements.
- """
- def current(self, exclude=[], site_wide=False, for_members=False):
- """
- Fetches and returns a queryset with the current announcements. This
- method takes the following parameters:
-
- ``exclude``
- A list of IDs that should be excluded from the queryset.
-
- ``site_wide``
- A boolean flag to filter to just site wide announcements.
-
- ``for_members``
- A boolean flag to allow member only announcements to be returned
- in addition to any others.
- """
- queryset = self.all()
- if site_wide:
- queryset = queryset.filter(site_wide=True)
- if exclude:
- queryset = queryset.exclude(pk__in=exclude)
- if not for_members:
- queryset = queryset.filter(members_only=False)
- queryset = queryset.order_by("-creation_date")
- return queryset
-
class Announcement(models.Model):
"""
A single announcement.
"""
+ DISMISSAL_NO = 1
+ DISMISSAL_SESSION = 2
+ DISMISSAL_PERMANENT = 3
+
+ DISMISSAL_CHOICES = [
+ (DISMISSAL_NO, "No Dismissals Allowed"),
+ (DISMISSAL_SESSION, "Session Only Dismissal"),
+ (DISMISSAL_PERMANENT, "Permanent Dismissal Allowed")
+ ]
+
title = models.CharField(_("title"), max_length=50)
content = models.TextField(_("content"))
creator = models.ForeignKey(User, verbose_name=_("creator"))
- creation_date = models.DateTimeField(_("creation_date"), default=datetime.now)
+ creation_date = models.DateTimeField(_("creation_date"), default=datetime.datetime.now)
site_wide = models.BooleanField(_("site wide"), default=False)
members_only = models.BooleanField(_("members only"), default=False)
-
- objects = AnnouncementManager()
+ dismissal_type = models.IntegerField(choices=DISMISSAL_CHOICES, default=DISMISSAL_SESSION)
+ publish_start = models.DateTimeField(_("publish_start"), default=datetime.datetime.now)
+ publish_end = models.DateTimeField(_("publish_end"), blank=True, null=True)
def get_absolute_url(self):
- return ("announcement_detail", [str(self.pk)])
- get_absolute_url = models.permalink(get_absolute_url)
+ return reverse("announcement_detail", args=[self.pk])
+
+ def dismiss_url(self):
+ if self.dismissal_type != Announcement.DISMISSAL_NO:
+ return reverse("announcement_dismiss", args=[self.pk])
def __unicode__(self):
return self.title
@@ -67,20 +45,9 @@ class Meta:
verbose_name_plural = _("announcements")
-def current_announcements_for_request(request, **kwargs):
- """
- A helper function to get the current announcements based on some data from
- the HttpRequest.
+class Dismissal(models.Model):
- If request.user is authenticated then allow the member only announcements
- to be returned.
-
- Exclude announcements that have already been viewed by the user based on
- the ``excluded_announcements`` session variable.
- """
- defaults = {}
- if request.user.is_authenticated():
- defaults["for_members"] = True
- defaults["exclude"] = request.session.get("excluded_announcements", set())
- defaults.update(kwargs)
- return Announcement.objects.current(**defaults)
+ user = models.ForeignKey(User, related_name="announcement_dismissals")
+ announcement = models.ForeignKey(Announcement, related_name="dismissals")
+ dismissed_at = models.DateTimeField(default=datetime.datetime.now)
+
View
6 announcements/signals.py
@@ -0,0 +1,6 @@
+import django.dispatch
+
+
+announcement_created = django.dispatch.Signal(providing_args=["announcement", "request"])
+announcement_updated = django.dispatch.Signal(providing_args=["announcement", "request"])
+announcement_deleted = django.dispatch.Signal(providing_args=["announcement", "request"])
View
3 announcements/templates/notification/announcement/full.txt
@@ -1,3 +0,0 @@
-{{ announcement.title }}
-
-{{ announcement.content }}
View
36 announcements/templatetags/announcement_tags.py
@@ -1,36 +0,0 @@
-from django.template import Library, Node
-
-from announcements.models import current_announcements_for_request
-
-
-register = Library()
-
-
-class FetchAnnouncementsNode(Node):
- def __init__(self, context_var, limit=None):
- self.context_var = context_var
- self.limit = limit
-
- def render(self, context):
- try:
- request = context["request"]
- except KeyError:
- raise Exception("{% fetch_announcements %} requires the HttpRequest in context.")
- kwargs = {}
- announcements = current_announcements_for_request(request, **kwargs)
- if self.limit:
- announcements = announcements[:self.limit]
- context[self.context_var] = announcements
- return ""
-
-@register.tag
-def fetch_announcements(parser, token):
- bits = token.split_contents()
- # @@@ very naive parsing
- if len(bits) == 5:
- limit = bits[2]
- context_var = bits[4]
- elif len(bits) == 3:
- limit = None
- context_var = bits[2]
- return FetchAnnouncementsNode(context_var, limit)
View
52 announcements/templatetags/announcements_tags.py
@@ -0,0 +1,52 @@
+import datetime
+
+from django import template
+from django.db.models import Q
+
+from announcements.models import Announcement
+
+
+register = template.Library()
+
+
+class AnnouncementsNode(template.Node):
+
+ @classmethod
+ def handle_token(cls, parser, token):
+ bits = token.split_contents()
+ if len(bits) != 3:
+ raise template.TemplateSyntaxError
+ return cls(as_var = bits[2])
+
+ def __init__(self, as_var):
+ self.as_var = as_var
+
+ def render(self, context):
+ request = context["request"]
+ qs = Announcement.objects.filter(
+ publish_start__lte=datetime.datetime.now()
+ ).filter(
+ Q(publish_end__isnull=True)|Q(publish_end__gt=datetime.datetime.now())
+ ).filter(
+ site_wide=True
+ )
+
+ exclusions = request.session.get("excluded_announcements", set())
+ if request.user.is_authenticated():
+ for dismissal in request.user.announcement_dismissals.all():
+ exclusions.add(dismissal.announcement.pk)
+ else:
+ qs = qs.exclude(members_only=True)
+ context[self.as_var] = qs.exclude(pk__in=exclusions)
+ return ""
+
+
+@register.tag
+def announcements(parser, token):
+ """
+ Usage::
+ {% announcements as var %}
+
+ Returns a list of announcements
+ """
+ return AnnouncementsNode.handle_token(parser, token)
View
35 announcements/tests.py
@@ -1,35 +0,0 @@
-__test__ = {"ANNOUNCEMENT_TESTS": r"""
->>> from django.contrib.auth.models import User
->>> from announcements.models import Announcement
-
-# create ourselves a user to associate to the announcements
->>> superuser = User.objects.create_user("brosner", "brosner@gmail.com")
-
->>> a1 = Announcement.objects.create(title="Down for Maintenance", creator=superuser)
->>> a2 = Announcement.objects.create(title="Down for Maintenance Again", creator=superuser)
->>> a3 = Announcement.objects.create(title="Down for Maintenance Again And Again", creator=superuser, site_wide=True)
->>> a4 = Announcement.objects.create(title="Members Need to Fill Out New Profile Info", creator=superuser, members_only=True)
->>> a5 = Announcement.objects.create(title="Expected Down Time", creator=superuser, members_only=True, site_wide=True)
-
-# get the announcements that are publically viewable. this is the same as
-# calling as using site_wide=False, for_members=False
->>> Announcement.objects.current()
-[<Announcement: Down for Maintenance Again And Again>, <Announcement: Down for Maintenance Again>, <Announcement: Down for Maintenance>]
-
-# get just the publically viewable site wide announcements
->>> Announcement.objects.current(site_wide=True)
-[<Announcement: Down for Maintenance Again And Again>]
-
-# get the announcements that authenticated users can see.
->>> Announcement.objects.current(for_members=True)
-[<Announcement: Expected Down Time>, <Announcement: Members Need to Fill Out New Profile Info>, <Announcement: Down for Maintenance Again And Again>, <Announcement: Down for Maintenance Again>, <Announcement: Down for Maintenance>]
-
-# get just site wide announcements that authenticated users can see.
->>> Announcement.objects.current(site_wide=True, for_members=True)
-[<Announcement: Expected Down Time>, <Announcement: Down for Maintenance Again And Again>]
-
-# exclude a couple of announcements from the publically viewabled messages.
->>> Announcement.objects.current(exclude=[a1.pk, a5.pk])
-[<Announcement: Down for Maintenance Again And Again>, <Announcement: Down for Maintenance Again>]
-
-"""}
View
23 announcements/urls.py
@@ -1,18 +1,15 @@
from django.conf.urls.defaults import *
-from django.views.generic import list_detail
-from announcements.models import Announcement
-from announcements.views import *
-
-
-announcement_detail_info = {
- "queryset": Announcement.objects.all(),
-}
+from announcements.views import detail, dismiss
+from announcements.views import CreateAnnouncementView, UpdateAnnouncementView
+from announcements.views import DeleteAnnouncementView, AnnouncementListView
urlpatterns = patterns("",
- url(r"^(?P<object_id>\d+)/$", list_detail.object_detail,
- announcement_detail_info, name="announcement_detail"),
- url(r"^(?P<object_id>\d+)/hide/$", announcement_hide,
- name="announcement_hide"),
- url(r"^$", announcement_list, name="announcement_home"),
+ url(r"announcement/(?P<pk>\d+)/$", detail, name="announcements_detail"),
+ url(r"announcement/(?P<pk>\d+)/hide/$", dismiss, name="announcement_dismiss"),
+
+ url(r"announcement/create/$", CreateAnnouncementView.as_view(), name="announcements_create"),
+ url(r"announcement/(?P<pk>\d+)/update/$", UpdateAnnouncementView.as_view(), name="announcements_update"),
+ url(r"announcement/(?P<pk>\d+)/delete/$", DeleteAnnouncementView.as_view(), name="announcements_delete"),
+ url(r"", AnnouncementListView.as_view(), name="announcements_list"),
)
View
121 announcements/views.py
@@ -1,36 +1,101 @@
-from django.http import HttpResponseRedirect
-from django.views.generic import list_detail
+from django.core.urlresolvers import reverse
+from django.http import HttpResponse, HttpResponseNotAllowed
from django.shortcuts import get_object_or_404
+from django.template.response import TemplateResponse
+from django.views.generic.edit import CreateView, UpdateView, DeleteView
+from django.views.generic.list import ListView
-from announcements.models import Announcement, current_announcements_for_request
+from announcements.forms import AnnouncementForm
+from announcements.mixins import ProtectedMixin
+from announcements.models import Announcement
+from announcements.signals import announcement_created, announcement_updated, announcement_deleted
-try:
- set
-except NameError:
- from sets import Set as set # Python 2.3 fallback
+def dismiss(request, pk):
+ if request.method != "POST":
+ return HttpResponseNotAllowed(["POST"])
+
+ announcement = get_object_or_404(Announcement, pk=pk)
+
+ if announcement.dismissal_type == Announcement.DISMISSAL_SESSION:
+ excluded = request.session.get("excluded_announcements", set())
+ excluded.add(announcement.pk)
+ request.session["excluded_announcements"] = excluded
+ status = 200
+ elif announcement.dismissal_type == Announcement.DISMISSAL_PERMANENT and request.user.is_authenticated():
+ announcement.dismissals.create(user=request.user)
+ status = 200
+ else:
+ status = 409
+
+ return HttpResponse(status=status)
-def announcement_list(request):
- """
- A basic view that wraps ``django.views.list_detail.object_list`` and
- uses ``current_announcements_for_request`` to get the current
- announcements.
- """
- queryset = current_announcements_for_request(request)
- return list_detail.object_list(request, **{
- "queryset": queryset,
- "allow_empty": True,
+
+def detail(request, pk):
+ announcement = get_object_or_404(Announcement, pk=pk)
+ return TemplateResponse(request, "announcements/detail.html", {
+ "announcement": announcement
})
-def announcement_hide(request, object_id):
- """
- Mark this announcement hidden in the session for the user.
- """
- announcement = get_object_or_404(Announcement, pk=object_id)
- # TODO: perform some basic security checks here to ensure next is not bad
- redirect_to = request.GET.get("next")
- excluded_announcements = request.session.get("excluded_announcements", set())
- excluded_announcements.add(announcement.pk)
- request.session["excluded_announcements"] = excluded_announcements
- return HttpResponseRedirect(redirect_to)
+class CreateAnnouncementView(CreateView, ProtectedMixin):
+
+ model = Announcement
+ form_class = AnnouncementForm
+
+ def form_valid(self, form):
+ self.object = form.save(commit=False)
+ self.object.creator = self.request.user
+ self.object.save()
+
+ announcement_created.send(
+ sender=self.object,
+ announcement=self.object,
+ request=self.request
+ )
+ return super(CreateAnnouncementView, self).form_valid(form)
+
+ def get_success_url(self):
+ return reverse("announcements_list")
+
+
+class UpdateAnnouncementView(UpdateView, ProtectedMixin):
+
+ model = Announcement
+ form_class = AnnouncementForm
+
+ def form_valid(self, form):
+ response = super(UpdateAnnouncementView, self).form_valid(form)
+ announcement_updated.send(
+ sender=self.object,
+ announcement=self.object,
+ request=self.request
+ )
+ return response
+
+ def get_success_url(self):
+ return reverse("announcements_list")
+
+
+class DeleteAnnouncementView(DeleteView, ProtectedMixin):
+
+ model = Announcement
+
+ def form_valid(self, form):
+ response = super(DeleteAnnouncementView, self).form_valid(form)
+ announcement_deleted.send(
+ sender=self.object,
+ announcement=self.object,
+ request=self.request
+ )
+ return response
+
+ def get_success_url(self):
+ return reverse("announcements_list")
+
+
+class AnnouncementListView(ListView, ProtectedMixin):
+
+ model = Announcement
+ queryset = Announcement.objects.all().order_by("-creation_date")
+ paginate_by = 50
View
131 docs/Makefile
@@ -0,0 +1,131 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = _build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
+PROJECT = django-announcements
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/$(PROJECT).qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/$(PROJECT).qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/$(PROJECT)"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/$(PROJECT)"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
View
45 docs/changelog.rst
@@ -0,0 +1,45 @@
+.. _changelog:
+
+ChangeLog
+=========
+
+0.2
+---
+
+- added ability to publish for periods of time
+- added model to store permanent clearings (see migration below)
+- added ability to control how announcements are cleared (no
+ clearing, session based, or permanent) (see migration below)
+- changed view `announcement_hide` to `dismiss`
+- changed url name of `announcement_hide` to `announcement_dismiss`
+- changed template tag from fetch_announcements to announcements
+- removed send now functionality
+- removed notifications
+- removed context processor
+- removed list view
+- removed AnnouncementsManager
+- removed current_announcements_for_request
+
+
+Migrations
+^^^^^^^^^^
+
+ ALTER TABLE "announcements_announcement" ADD COLUMN "dismissal_type" int DEFAULT 2 NOT NULL;
+ ALTER TABLE "announcements_announcement" ADD COLUMN "publish_start" timestamp with time zone NOT NULL;
+ ALTER TABLE "announcements_announcement" ADD COLUMN "publish_end" timestamp with time zone;
+ ### New Model: announcements.Dismissal
+ CREATE TABLE "announcements_dismissal" (
+ "id" serial NOT NULL PRIMARY KEY,
+ "user_id" integer NOT NULL REFERENCES "auth_user" ("id") DEFERRABLE INITIALLY DEFERRED,
+ "announcement_id" integer NOT NULL REFERENCES "announcements_announcement" ("id") DEFERRABLE INITIALLY DEFERRED,
+ "dismissed_at" timestamp with time zone NOT NULL
+ )
+ ;
+ CREATE INDEX "announcements_dismissal_user_id" ON "announcements_dismissal" ("user_id");
+ CREATE INDEX "announcements_dismissal_announcement_id" ON "announcements_dismissal" ("announcement_id");
+
+
+0.1
+---
+
+- initial release
View
28 docs/conf.py
@@ -0,0 +1,28 @@
+import sys, os
+
+extensions = []
+templates_path = []
+source_suffix = '.rst'
+master_doc = 'index'
+project = u'django-announcements'
+package = 'announcements'
+copyright_holder = 'Eldarion'
+copyright = u'2011, %s' % copyright_holder
+exclude_patterns = ['_build']
+pygments_style = 'sphinx'
+html_theme = 'default'
+htmlhelp_basename = '%sdoc' % project
+latex_documents = [
+ ('index', '%s.tex' % project, u'%s Documentation' % project,
+ copyright_holder, 'manual'),
+]
+man_pages = [
+ ('index', project, u'%s Documentation' % project,
+ [copyright_holder], 1)
+]
+
+sys.path.insert(0, os.pardir)
+m = __import__(package)
+
+version = m.__version__
+release = version
View
29 docs/index.rst
@@ -0,0 +1,29 @@
+====================
+django-announcements
+====================
+
+Some sites need the ability to broadcast an announcement to all of their
+users. django-announcements was created precisely for this reason. How you
+present the announcement is up to you as the site-developer. There are two
+different types of filtering of announcements:
+
+ * site-wide (this can be presented to anonymous users)
+ * members only (announcements for only logged in users)
+
+
+Development
+-----------
+
+The source repository can be found at https://github.com/pinax/django-announcements
+
+
+Contents
+========
+
+.. toctree::
+ :maxdepth: 1
+
+ changelog
+ installation
+ templatetags
+ usage
View
23 docs/index.txt
@@ -1,23 +0,0 @@
-
-====================
-django-announcements
-====================
-
-Some sites need the ability to broadcast an announcement to all of their
-users. django-announcements was created precisely for this reason. How you
-present the announcement is up to you as the site-developer. When working with
-announcements that are presented on the website one feature is that they are
-only viewed once. A session variable will hold which announcements an user has
-viewed and exclude that from their display. announcements supports two
-different types of filtering of announcements:
-
- * site-wide (this can be presented to anonymous users)
- * non site-wide (these can be used a strictly a mailing if so desired)
- * members only (announcements are filtered based on the value of
- ``request.user``)
-
-Contents:
-
-.. toctree::
-
- usage
View
21 docs/installation.rst
@@ -0,0 +1,21 @@
+.. _installation:
+
+Installation
+============
+
+* To install django-announcements::
+
+ pip install django-announcements
+
+* Add ``'announcements'`` to your ``INSTALLED_APPS`` setting::
+
+ INSTALLED_APPS = (
+ # other apps
+ "announcements",
+ )
+
+* Finally::
+
+ ...
+ url(r"^announcements/", include("announcements.urls")),
+ ...
View
9 docs/templatetags.rst
@@ -0,0 +1,9 @@
+.. _templatetags:
+
+Template Tags
+=============
+
+announcements
+-------------
+
+ {% announcements as announcements_list %}
View
39 docs/usage.rst
@@ -0,0 +1,39 @@
+.. _usage:
+
+Usage
+=====
+
+Displaying announcements is done via a template tag that fetches the
+announcements::
+
+ {% load announcements_tags %}
+
+ <h3>Announcements</h3>
+
+ {% announcements as announcements_list %}
+
+ {% if announcements_list %}
+ <div class="announcements">
+ {% for announcement in announcements_list %}
+ <div class="announcement">
+ <strong>{{ announcement.title }}</strong><br />
+ {{ announcement.content }}
+ {% if announcement.dismiss_url %}
+ <a href="{{ announcement.dismiss_url }}" class="dismiss">
+ Clear
+ </a>
+ {% endif %}
+ </div>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+If you expect your announcement to be more detail oriented rather than
+just a few sentences then it might be better a link in the mark up to
+the supplied detail view::
+
+ <a href="{{ announcement.get_absolute_url }}">Read more...</a>
+
+
+The `announcement.clear_url` is intended to be called via an AJAX POST
+and will dismiss the announcement based on it's dismissal properties.
View
124 docs/usage.txt
@@ -1,124 +0,0 @@
-
-=====
-Usage
-=====
-
-Basic usage
-===========
-
-Integrating announcements is very simple. announcements provides to you a
-context processor to get template global access::
-
- TEMPLATE_CONTEXT_PROCESSORS = (
- # ...
- "announcements.context_processors.site_wide_announcements",
- # ...
- )
-
-Once that is hooked up you now have access ``{{ site_wide_announcements }}``
-which is a simple queryset that has filtered the announcements to give you
-just the right ones. If the user viewing the page is authenticated it will
-additionally pull out announcements that have been marked ``for_members``.
-
-Here is a quick snippet of how this can be used in a template. Typically in
-a base template like ``base.html`` or some sort::
-
- {% if site_wide_announcements %}
- <div id="site_wide_announcements">
- <ul>
- {% for announcement in site_wide_announcements %}
- <li>
- <a href="{{ announcement.get_absolute_url }}">{{ announcement }}</a> -
- <a href="{% url announcement_hide announcement.pk %}?next={{ request.path }}">Hide announcement</a>
- </li>
- {% endfor %}
- </ul>
- </div>
- {% endif %}
-
-The above template sample uses the views discussed a little bit later on.
-
-To give a bit of internals, the ``site_wide_announcements`` context processor
-is simply a wrapper around ``current_announcements_for_request`` which is
-located in ``announcements.models``.
-
-Announcement model
-------------------
-
-To store announcements in the database, announcements comes with a model that
-deals with this. It contains these fields:
-
- * ``title`` - The title of the announcement. This is limited to 50 characters.
- The title is completely optional since some types of announcements don't
- really need one.
- * ``content`` - The main content of the announcement.
- * ``creator`` - The user who created the announcement.
- * ``creation_date`` - A ``DateTimeField`` indicating when the announcement
- was created.
- * ``site_wide`` - A boolean value indicating whether the announcement should
- be site-wide and used in the context processor.
- * ``members_only`` - This will tag an announcement as for member eyes only.
-
-Additional uses
-===============
-
-There are a couple of ways that announcements can be used outside of its basic
-usage described above.
-
-E-mailing users
----------------
-
-When you are creating a new announcement via the admin interface you are given
-the option to send now. What this means is that announcements has optional
-support of django-notification. If it is available it can send a notification
-of the announcement. This then in turn can be e-mail to the user.
-
-.. note::
-
- Due to the possibility of large user bases, even 20+, can cause the
- sending of a notification to take a bit of time. This could in turn cause
- the request to time out. To avoid that announcements uses the queuing
- feature of notifications. To send out the notifications you will need to
- use the ``emit_notices`` management command notifications provides.
-
-URLconf, views and templates
-----------------------------
-
-announcements comes with three pre-defined URLs. They enable you the ability
-to list, view and hide announcements. You can hook up these views very
-simply in your ``urls.py``::
-
- # example urls.py.
- from django.conf.urls.defaults import *
-
- urlpatterns = patterns("",
- # ...
- url(r"^announcements/", include("announcements.urls")),
- # ...
- )
-
-``announcement_home``
-~~~~~~~~~~~~~~~~~~~~~
-
-View: ``announcements.views.announcement_list``
-
-It uses ``current_announcements_for_request`` to get a queryset of
-announcements appropriate to the ``HttpRequest``.
-
-``announcement_detail``
-~~~~~~~~~~~~~~~~~~~~~~~
-
-View: ``django.views.generic.list_detail.object_detail``
-
-Displays a single announcement. Reference the Django object_detail_
-documentation for more information.
-
-.. _object_detail: http://docs.djangoproject.com/en/dev/ref/generic-views/#django-views-generic-list-detail-object-detail
-
-``announcement_hide``
-~~~~~~~~~~~~~~~~~~~~~
-
-View: ``announcements.views.announcement_hide``
-
-This view will mark a given announcement as hidden and redirect the user
-to the provide ``next`` ``GET`` argument.
View
140 setup.py
@@ -1,25 +1,137 @@
-from distutils.core import setup
+import codecs
+import os
+import sys
+
+from distutils.util import convert_path
+from fnmatch import fnmatchcase
+from setuptools import setup, find_packages
+
+
+def read(fname):
+ return codecs.open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+
+# Provided as an attribute, so you can append to these instead
+# of replicating them:
+standard_exclude = ["*.py", "*.pyc", "*$py.class", "*~", ".*", "*.bak"]
+standard_exclude_directories = [
+ ".*", "CVS", "_darcs", "./build", "./dist", "EGG-INFO", "*.egg-info"
+]
+
+
+# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
+# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
+# Note: you may want to copy this into your setup.py file verbatim, as
+# you can't import this from another package, when you don't know if
+# that package is installed yet.
+def find_package_data(
+ where=".",
+ package="",
+ exclude=standard_exclude,
+ exclude_directories=standard_exclude_directories,
+ only_in_packages=True,
+ show_ignored=False):
+ """
+ Return a dictionary suitable for use in ``package_data``
+ in a distutils ``setup.py`` file.
+
+ The dictionary looks like::
+
+ {"package": [files]}
+
+ Where ``files`` is a list of all the files in that package that
+ don"t match anything in ``exclude``.
+
+ If ``only_in_packages`` is true, then top-level directories that
+ are not packages won"t be included (but directories under packages
+ will).
+
+ Directories matching any pattern in ``exclude_directories`` will
+ be ignored; by default directories with leading ``.``, ``CVS``,
+ and ``_darcs`` will be ignored.
+
+ If ``show_ignored`` is true, then all the files that aren"t
+ included in package data are shown on stderr (for debugging
+ purposes).
+
+ Note patterns use wildcards, or can be exact paths (including
+ leading ``./``), and all searching is case-insensitive.
+ """
+ out = {}
+ stack = [(convert_path(where), "", package, only_in_packages)]
+ while stack:
+ where, prefix, package, only_in_packages = stack.pop(0)
+ for name in os.listdir(where):
+ fn = os.path.join(where, name)
+ if os.path.isdir(fn):
+ bad_name = False
+ for pattern in exclude_directories:
+ if (fnmatchcase(name, pattern)
+ or fn.lower() == pattern.lower()):
+ bad_name = True
+ if show_ignored:
+ print >> sys.stderr, (
+ "Directory %s ignored by pattern %s"
+ % (fn, pattern))
+ break
+ if bad_name:
+ continue
+ if (os.path.isfile(os.path.join(fn, "__init__.py"))
+ and not prefix):
+ if not package:
+ new_package = name
+ else:
+ new_package = package + "." + name
+ stack.append((fn, "", new_package, False))
+ else:
+ stack.append((fn, prefix + name + "/", package, only_in_packages))
+ elif package or not only_in_packages:
+ # is a file
+ bad_name = False
+ for pattern in exclude:
+ if (fnmatchcase(name, pattern)
+ or fn.lower() == pattern.lower()):
+ bad_name = True
+ if show_ignored:
+ print >> sys.stderr, (
+ "File %s ignored by pattern %s"
+ % (fn, pattern))
+ break
+ if bad_name:
+ continue
+ out.setdefault(package, []).append(prefix+name)
+ return out
+
+
+PACKAGE = "announcements"
+NAME = "django-announcements"
+DESCRIPTION = "Announcements for your Django powered website."
+AUTHOR = "Brian Rosner"
+AUTHOR_EMAIL = "brosner@gmail.com"
+URL = "https://github.com/pinax/django-announcements"
+VERSION = __import__(PACKAGE).__version__
setup(
- name = "django-announcements",
- version = __import__("announcements").__version__,
- author = "Brian Rosner",
- author_email = "brosner@gmail.com",
- description = "Announcements for your Django powered website.",
- long_description = open("README").read(),
- license = "MIT",
- url = "http://code.google.com/p/django-announcements",
- packages = [
- "announcements",
- ],
- classifiers = [
+ name=NAME,
+ version=VERSION,
+ description=DESCRIPTION,
+ long_description=read("README"),
+ author=AUTHOR,
+ author_email=AUTHOR_EMAIL,
+ license="MIT",
+ url=URL,
+ packages=find_packages(exclude=["tests.*", "tests"]),
+ package_data=find_package_data(PACKAGE, only_in_packages=False),
+ classifiers=[
"Development Status :: 3 - Alpha",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Framework :: Django",
- ]
+ ],
+ zip_safe=False,
)
+