├── test_project ├── __init__.py ├── local_settings.py ├── manage.py ├── urls.py └── settings.py ├── mezzanine_agenda ├── templatetags │ ├── __init__.py │ └── event_tags.py ├── __init__.py ├── fixtures │ └── events_page.json ├── admin.py ├── urls.py ├── defaults.py ├── templates │ └── agenda │ │ ├── includes │ │ └── filter_panel.html │ │ ├── event_list.html │ │ └── event_detail.html ├── feeds.py ├── tests.py ├── migrations │ └── 0001_initial.py ├── views.py └── models.py ├── .gitignore ├── .travis.yml ├── setup.py └── README.mdown /test_project/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /mezzanine_agenda/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info/* 3 | *.db 4 | .DS_Store 5 | setuptools_git* 6 | -------------------------------------------------------------------------------- /mezzanine_agenda/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | Provides a agenda app with events, keywords, locations and comments. 3 | Events can be listed by month, keyword, location or author. 4 | """ 5 | from __future__ import unicode_literals 6 | 7 | __version__ = "0.1.2" 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: '2.7' 3 | env: 4 | - DJANGO_VERSION="django>=1.5,<1.5.99" 5 | - DJANGO_VERSION="django>=1.6,<1.6.99" 6 | install: 7 | - 'pip install $DJANGO_VERSION' 8 | - 'pip install -q -e . --use-mirrors' 9 | script: 10 | - 'cd test_project' 11 | - 'python manage.py test mezzanine_agenda' 12 | -------------------------------------------------------------------------------- /mezzanine_agenda/fixtures/events_page.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pk": 9, 4 | "model": "pages.page", 5 | "fields": { 6 | "status": 2, 7 | "_order": 1, 8 | "parent": null, 9 | "description": "Events", 10 | "title": "Events", 11 | "titles": "Events", 12 | "content_model": "richtextpage", 13 | "in_menus": [1, 2, 3], 14 | "slug": "events", 15 | "site": 1 16 | } 17 | }, 18 | { 19 | "pk": 9, 20 | "model": "pages.richtextpage", 21 | "fields": { 22 | "content": "

Events

" 23 | } 24 | } 25 | ] 26 | 27 | -------------------------------------------------------------------------------- /test_project/local_settings.py: -------------------------------------------------------------------------------- 1 | 2 | DEBUG = True 3 | 4 | # Make these unique, and don't share it with anybody. 5 | SECRET_KEY = "%(SECRET_KEY)s" 6 | NEVERCACHE_KEY = "%(NEVERCACHE_KEY)s" 7 | 8 | DATABASES = { 9 | "default": { 10 | # Ends with "postgresql_psycopg2", "mysql", "sqlite3" or "oracle". 11 | "ENGINE": "django.db.backends.sqlite3", 12 | # DB name or path to database file if using sqlite3. 13 | "NAME": "dev.db", 14 | # Not used with sqlite3. 15 | "USER": "", 16 | # Not used with sqlite3. 17 | "PASSWORD": "", 18 | # Set to empty string for localhost. Not used with sqlite3. 19 | "HOST": "", 20 | # Set to empty string for default. Not used with sqlite3. 21 | "PORT": "", 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import absolute_import, unicode_literals 3 | 4 | import os 5 | import sys 6 | 7 | 8 | # Corrects some pathing issues in various contexts, such as cron jobs, 9 | # and the project layout still being in Django 1.3 format. 10 | from settings import PROJECT_ROOT, PROJECT_DIRNAME 11 | os.chdir(PROJECT_ROOT) 12 | sys.path.insert(0, os.path.abspath(os.path.join(PROJECT_ROOT, ".."))) 13 | 14 | 15 | # Add the site ID CLI arg to the environment, which allows for the site 16 | # used in any site related queries to be manually set for management 17 | # commands. 18 | for i, arg in enumerate(sys.argv): 19 | if arg.startswith("--site"): 20 | os.environ["MEZZANINE_SITE_ID"] = arg.split("=")[1] 21 | sys.argv.pop(i) 22 | 23 | 24 | # Run Django. 25 | if __name__ == "__main__": 26 | settings_module = "%s.settings" % PROJECT_DIRNAME 27 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", settings_module) 28 | from django.core.management import execute_from_command_line 29 | execute_from_command_line(sys.argv) 30 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from mezzanine_agenda import __version__ 3 | import subprocess 4 | 5 | def get_long_desc(): 6 | """Use Pandoc to convert the readme to ReST for the PyPI.""" 7 | try: 8 | return subprocess.check_output(['pandoc', '-f', 'markdown', '-t', 'rst', 'README.mdown']) 9 | except: 10 | print "WARNING: The long readme wasn't converted properly" 11 | 12 | setup(name='mezzanine-agenda', 13 | version=__version__, 14 | description='Events for the Mezzanine CMS', 15 | long_description=get_long_desc(), 16 | author='James Pells', 17 | author_email='jimmy@jamespells.com', 18 | url='https://github.com/jpells/mezzanine-agenda', 19 | packages=find_packages(), 20 | include_package_data=True, 21 | setup_requires=[ 22 | 'setuptools_git>=0.3', 23 | ], 24 | install_requires=[ 25 | 'mezzanine', 26 | 'icalendar==3.0.1b2', 27 | 'geopy==0.95.1', 28 | 'pytz', 29 | ], 30 | classifiers = [ 31 | 'Development Status :: 4 - Beta', 32 | 'Environment :: Web Environment', 33 | 'Framework :: Django', 34 | 'License :: OSI Approved :: MIT License', 35 | 'Natural Language :: English', 36 | 'Operating System :: OS Independent', 37 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 38 | ], 39 | ) 40 | -------------------------------------------------------------------------------- /mezzanine_agenda/admin.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from copy import deepcopy 4 | 5 | from django.contrib import admin 6 | from django.utils.translation import ugettext_lazy as _ 7 | 8 | from mezzanine_agenda.models import Event, EventLocation 9 | from mezzanine.conf import settings 10 | from mezzanine.core.admin import DisplayableAdmin, OwnableAdmin 11 | 12 | 13 | event_fieldsets = deepcopy(DisplayableAdmin.fieldsets) 14 | event_fieldsets[0][1]["fields"].insert(1, ("start", "end")) 15 | event_fieldsets[0][1]["fields"].insert(2, "location") 16 | event_fieldsets[0][1]["fields"].insert(3, "facebook_event") 17 | event_fieldsets[0][1]["fields"].extend(["content", "allow_comments"]) 18 | event_list_display = ["title", "user", "status", "admin_link"] 19 | if settings.EVENT_USE_FEATURED_IMAGE: 20 | event_fieldsets[0][1]["fields"].insert(-2, "featured_image") 21 | event_list_display.insert(0, "admin_thumb") 22 | event_fieldsets = list(event_fieldsets) 23 | event_list_filter = deepcopy(DisplayableAdmin.list_filter) + ("location",) 24 | 25 | 26 | class EventAdmin(DisplayableAdmin, OwnableAdmin): 27 | """ 28 | Admin class for events. 29 | """ 30 | 31 | fieldsets = event_fieldsets 32 | list_display = event_list_display 33 | list_filter = event_list_filter 34 | 35 | def save_form(self, request, form, change): 36 | """ 37 | Super class ordering is important here - user must get saved first. 38 | """ 39 | OwnableAdmin.save_form(self, request, form, change) 40 | return DisplayableAdmin.save_form(self, request, form, change) 41 | 42 | 43 | class EventLocationAdmin(admin.ModelAdmin): 44 | """ 45 | Admin class for event locations. Hides itself from the admin menu 46 | unless explicitly specified. 47 | """ 48 | 49 | fieldsets = ((None, {"fields": ("title", "address", "mappable_location", "lat", "lon")}),) 50 | 51 | def in_menu(self): 52 | """ 53 | Hide from the admin menu unless explicitly set in ``ADMIN_MENU_ORDER``. 54 | """ 55 | for (name, items) in settings.ADMIN_MENU_ORDER: 56 | if "mezzanine_agenda.EventLocation" in items: 57 | return True 58 | return False 59 | 60 | 61 | admin.site.register(Event, EventAdmin) 62 | admin.site.register(EventLocation, EventLocationAdmin) 63 | -------------------------------------------------------------------------------- /mezzanine_agenda/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf.urls import patterns, url 4 | 5 | from mezzanine.conf import settings 6 | 7 | 8 | # Trailing slash for urlpatterns based on setup. 9 | _slash = "/" if settings.APPEND_SLASH else "" 10 | 11 | # Agenda patterns. 12 | urlpatterns = patterns("mezzanine_agenda.views", 13 | url("^feeds/(?P.*)%s$" % _slash, 14 | "event_feed", name="event_feed"), 15 | url("^tag/(?P.*)/feeds/(?P.*)%s$" % _slash, 16 | "event_feed", name="event_feed_tag"), 17 | url("^tag/(?P.*)%s$" % _slash, "event_list", 18 | name="event_list_tag"), 19 | url("^tag/(?P.*)/calendar.ics$", "icalendar", 20 | name="icalendar_tag"), 21 | url("^location/(?P.*)/feeds/(?P.*)%s$" % _slash, 22 | "event_feed", name="event_feed_location"), 23 | url("^location/(?P.*)%s$" % _slash, 24 | "event_list", name="event_list_location"), 25 | url("^location/(?P.*)/calendar.ics$", 26 | "icalendar", name="icalendar_location"), 27 | url("^author/(?P.*)/feeds/(?P.*)%s$" % _slash, 28 | "event_feed", name="event_feed_author"), 29 | url("^author/(?P.*)%s$" % _slash, 30 | "event_list", name="event_list_author"), 31 | url("^author/(?P.*)/calendar.ics$", 32 | "icalendar", name="icalendar_author"), 33 | url("^archive/(?P\d{4})/(?P\d{1,2})%s$" % _slash, 34 | "event_list", name="event_list_month"), 35 | url("^archive/(?P\d{4})/(?P\d{1,2})/calendar.ics$", 36 | "icalendar", name="icalendar_month"), 37 | url("^archive/(?P\d{4})%s$" % _slash, 38 | "event_list", name="event_list_year"), 39 | url("^archive/(?P\d{4})/calendar.ics$", 40 | "icalendar", name="icalendar_year"), 41 | url("^(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/" 42 | "(?P.*)%s$" % _slash, 43 | "event_detail", name="event_detail_day"), 44 | url("^(?P\d{4})/(?P\d{1,2})/(?P.*)%s$" % _slash, 45 | "event_detail", name="event_detail_month"), 46 | url("^(?P\d{4})/(?P.*)%s$" % _slash, 47 | "event_detail", name="event_detail_year"), 48 | url("^(?P\d{4})/(?P\d{1,2})/(?P\d{1,2})/" 49 | "(?P.*)/event.ics$", "icalendar_event", name="icalendar_event_day"), 50 | url("^(?P\d{4})/(?P\d{1,2})/(?P.*)/event.ics$", 51 | "icalendar_event", name="icalendar_event_month"), 52 | url("^(?P\d{4})/(?P.*)/event.ics$", 53 | "icalendar_event", name="icalendar_event_year"), 54 | url("^(?P.*)/event.ics$", "icalendar_event", name="icalendar_event"), 55 | url("^calendar.ics$", "icalendar", name="icalendar"), 56 | url("^(?P.*)%s$" % _slash, "event_detail", 57 | name="event_detail"), 58 | url("^$", "event_list", name="event_list"), 59 | ) 60 | -------------------------------------------------------------------------------- /mezzanine_agenda/defaults.py: -------------------------------------------------------------------------------- 1 | """ 2 | Default settings for the ``mezzanine_agenda`` app. Each of these can be 3 | overridden in your project's settings module, just like regular 4 | Django settings. The ``editable`` argument for each controls whether 5 | the setting is editable via Django's admin. 6 | 7 | Thought should be given to how a setting is actually used before 8 | making it editable, as it may be inappropriate - for example settings 9 | that are only read during startup shouldn't be editable, since changing 10 | them would require an application reload. 11 | """ 12 | from __future__ import unicode_literals 13 | 14 | from django.conf import settings 15 | from django.utils.translation import ugettext_lazy as _ 16 | 17 | from mezzanine.conf import register_setting 18 | 19 | 20 | register_setting( 21 | name="ADMIN_MENU_ORDER", 22 | description=_("Controls the ordering and grouping of the admin menu."), 23 | editable=False, 24 | default=( 25 | (_("Content"), ("pages.Page", "blog.BlogPost", "mezzanine_agenda.Event", 26 | "generic.ThreadedComment", (_("Media Library"), "fb_browse"),)), 27 | (_("Site"), ("sites.Site", "redirects.Redirect", "conf.Setting")), 28 | (_("Users"), ("auth.User", "auth.Group",)), 29 | ), 30 | ) 31 | 32 | register_setting( 33 | name="EVENT_USE_FEATURED_IMAGE", 34 | description=_("Enable featured images in events"), 35 | editable=False, 36 | default=False, 37 | ) 38 | 39 | register_setting( 40 | name="EVENT_URLS_DATE_FORMAT", 41 | label=_("Event URL date format"), 42 | description=_("A string containing the value ``year``, ``month``, or " 43 | "``day``, which controls the granularity of the date portion in the " 44 | "URL for each event. Eg: ``year`` will define URLs in the format " 45 | "/events/yyyy/slug/, while ``day`` will define URLs with the format " 46 | "/events/yyyy/mm/dd/slug/. An empty string means the URLs will only " 47 | "use the slug, and not contain any portion of the date at all."), 48 | editable=False, 49 | default="", 50 | ) 51 | 52 | register_setting( 53 | name="EVENT_PER_PAGE", 54 | label=_("Events per page"), 55 | description=_("Number of events shown on a event listing page."), 56 | editable=True, 57 | default=5, 58 | ) 59 | 60 | register_setting( 61 | name="EVENT_RSS_LIMIT", 62 | label=_("Events RSS limit"), 63 | description=_("Number of most recent events shown in the RSS feed. " 64 | "Set to ``None`` to display all events in the RSS feed."), 65 | editable=False, 66 | default=20, 67 | ) 68 | 69 | register_setting( 70 | name="EVENT_SLUG", 71 | description=_("Slug of the page object for the events."), 72 | editable=False, 73 | default="events", 74 | ) 75 | 76 | register_setting( 77 | name="EVENT_GOOGLE_MAPS_DOMAIN", 78 | description="The Google Maps country domain to geocode addresses with", 79 | editable=True, 80 | default="maps.google.com", 81 | ) 82 | 83 | register_setting( 84 | name="EVENT_TIME_ZONE", 85 | description="The timezone that event times are written in, if different from the timezone in settings.TIME_ZONE", 86 | editable=True, 87 | default="", 88 | ) 89 | 90 | register_setting( 91 | name="EVENT_HIDPI_STATIC_MAPS", 92 | description="Generate maps suitable for Retina displays", 93 | editable=True, 94 | default=True, 95 | ) 96 | 97 | register_setting( 98 | name="EVENT_GOOGLE_MAPS_API_KEY", 99 | description="If set, interactive google maps embed will be used instead of static images", 100 | editable=True, 101 | default=True, 102 | ) 103 | 104 | -------------------------------------------------------------------------------- /mezzanine_agenda/templates/agenda/includes/filter_panel.html: -------------------------------------------------------------------------------- 1 | {% load event_tags keyword_tags i18n future %} 2 | 3 | {% block upcoming_events %} 4 | {% upcoming_events 5 as upcoming_events %} 5 | {% if upcoming_events %} 6 |

{% trans "Upcoming Events" %}

7 | 13 | {% endif %} 14 | {% endblock %} 15 | 16 | {% block recent_events %} 17 | {% recent_events 5 as recent_events %} 18 | {% if recent_events %} 19 |

{% trans "Recent Events" %}

20 | 26 | {% endif %} 27 | {% endblock %} 28 | 29 | {% block event_months %} 30 | {% event_months as months %} 31 | {% if months %} 32 |

{% trans "Archive" %}

33 | {% for month in months %} 34 | {% ifchanged month.date.year %} 35 | {% if not forloop.first %}{% endif %} 36 |
{{ month.date.year }}
42 | {% endif %} 43 | {% endblock %} 44 | 45 | {% block event_locations %} 46 | {% event_locations as locations %} 47 | {% if locations %} 48 |

{% trans "Locations" %}

49 |
    50 | {% for location in locations %} 51 |
  • {{ location }} ({{ location.event_count }})
  • 53 | {% endfor %} 54 |
55 | {% endif %} 56 | {% endblock %} 57 | 58 | {% block event_keywords %} 59 | {% keywords_for mezzanine_agenda.event as tags %} 60 | {% if tags %} 61 |

{% trans "Tags" %}

62 |
    63 | {% for tag in tags %} 64 |
  • 65 | {{ tag }} 67 | ({{ tag.item_count }}) 68 |
  • 69 | {% endfor %} 70 |
71 | {% endif %} 72 | {% endblock %} 73 | 74 | {% block event_authors %} 75 | {% event_authors as authors %} 76 | {% if authors %} 77 |

{% trans "Authors" %}

78 | 85 | {% endif %} 86 | {% endblock %} 87 | 88 | {% block event_feeds %} 89 |

{% trans "Feeds" %}

90 | {% if tag %} 91 | {% trans "RSS" %} / 92 | {% trans "Atom" %} 93 | {% endif %} 94 | {% if location %} 95 | {% trans "RSS" %} / 96 | {% trans "Atom" %} 97 | {% endif %} 98 | {% if author %} 99 | {% trans "RSS" %} / 100 | {% trans "Atom" %} 101 | {% endif %} 102 | {% if not tag and not location and not author %} 103 | {% trans "RSS" %} / 104 | {% trans "Atom" %} 105 | {% endif %} 106 | {% endblock %} 107 | -------------------------------------------------------------------------------- /mezzanine_agenda/feeds.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.contrib.syndication.views import Feed 4 | from django.core.urlresolvers import reverse 5 | from django.shortcuts import get_object_or_404 6 | from django.utils.feedgenerator import Atom1Feed 7 | from django.utils.html import strip_tags 8 | 9 | from mezzanine.core.templatetags.mezzanine_tags import richtext_filters 10 | from mezzanine_agenda.models import Event, EventLocation 11 | from mezzanine.generic.models import Keyword 12 | from mezzanine.pages.models import Page 13 | from mezzanine.conf import settings 14 | from mezzanine.utils.models import get_user_model 15 | 16 | 17 | User = get_user_model() 18 | 19 | 20 | class EventsRSS(Feed): 21 | """ 22 | RSS feed for all events. 23 | """ 24 | 25 | def __init__(self, *args, **kwargs): 26 | """ 27 | Use the title and description of the Events page for the feed's 28 | title and description. If the events page has somehow been 29 | removed, fall back to the ``SITE_TITLE`` and ``SITE_TAGLINE`` 30 | settings. 31 | """ 32 | self.tag = kwargs.pop("tag", None) 33 | self.location = kwargs.pop("location", None) 34 | self.username = kwargs.pop("username", None) 35 | super(EventsRSS, self).__init__(*args, **kwargs) 36 | self._public = True 37 | try: 38 | page = Page.objects.published().get(slug=settings.EVENT_SLUG) 39 | except Page.DoesNotExist: 40 | page = None 41 | else: 42 | self._public = not page.login_required 43 | if self._public: 44 | settings.use_editable() 45 | if page is not None: 46 | self._title = "%s | %s" % (page.title, settings.SITE_TITLE) 47 | self._description = strip_tags(page.description) 48 | else: 49 | self._title = settings.SITE_TITLE 50 | self._description = settings.SITE_TAGLINE 51 | 52 | def title(self): 53 | return self._title 54 | 55 | def description(self): 56 | return self._description 57 | 58 | def link(self): 59 | return reverse("event_list") 60 | 61 | def items(self): 62 | if not self._public: 63 | return [] 64 | events = Event.objects.published().select_related("user") 65 | if self.tag: 66 | tag = get_object_or_404(Keyword, slug=self.tag) 67 | events = events.filter(keywords__keyword=tag) 68 | if self.location: 69 | location = get_object_or_404(EventLocation, slug=self.location) 70 | events = events.filter(location=location) 71 | if self.username: 72 | author = get_object_or_404(User, username=self.username) 73 | events = events.filter(user=author) 74 | limit = settings.EVENT_RSS_LIMIT 75 | if limit is not None: 76 | events = events[:settings.EVENT_RSS_LIMIT] 77 | return events 78 | 79 | def item_description(self, item): 80 | return richtext_filters(item.content) 81 | 82 | def locations(self): 83 | if not self._public: 84 | return [] 85 | return EventLocations.objects.all() 86 | 87 | def item_author_name(self, item): 88 | return item.user.get_full_name() or item.user.username 89 | 90 | def item_author_link(self, item): 91 | username = item.user.username 92 | return reverse("event_list_author", kwargs={"username": username}) 93 | 94 | def item_pubdate(self, item): 95 | return item.publish_date 96 | 97 | def item_location(self, item): 98 | return item.location 99 | 100 | 101 | class EventsAtom(EventsRSS): 102 | """ 103 | Atom feed for all events. 104 | """ 105 | 106 | feed_type = Atom1Feed 107 | 108 | def subtitle(self): 109 | return self.description() 110 | -------------------------------------------------------------------------------- /test_project/urls.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.conf.urls import patterns, include, url 4 | from django.conf.urls.i18n import i18n_patterns 5 | from django.contrib import admin 6 | 7 | from mezzanine.conf import settings 8 | from mezzanine.core.views import direct_to_template 9 | 10 | 11 | admin.autodiscover() 12 | 13 | # Add the urlpatterns for any custom Django applications here. 14 | # You can also change the ``home`` view to add your own functionality 15 | # to the project's homepage. 16 | 17 | urlpatterns = i18n_patterns("", 18 | # Change the admin prefix here to use an alternate URL for the 19 | # admin interface, which would be marginally more secure. 20 | ("^admin/", include(admin.site.urls)), 21 | ) 22 | 23 | urlpatterns += patterns('', 24 | 25 | # We don't want to presume how your homepage works, so here are a 26 | # few patterns you can use to set it up. 27 | 28 | # HOMEPAGE AS STATIC TEMPLATE 29 | # --------------------------- 30 | # This pattern simply loads the index.html template. It isn't 31 | # commented out like the others, so it's the default. You only need 32 | # one homepage pattern, so if you use a different one, comment this 33 | # one out. 34 | 35 | url("^$", direct_to_template, {"template": "index.html"}, name="home"), 36 | 37 | # HOMEPAGE AS AN EDITABLE PAGE IN THE PAGE TREE 38 | # --------------------------------------------- 39 | # This pattern gives us a normal ``Page`` object, so that your 40 | # homepage can be managed via the page tree in the admin. If you 41 | # use this pattern, you'll need to create a page in the page tree, 42 | # and specify its URL (in the Meta Data section) as "/", which 43 | # is the value used below in the ``{"slug": "/"}`` part. 44 | # Also note that the normal rule of adding a custom 45 | # template per page with the template name using the page's slug 46 | # doesn't apply here, since we can't have a template called 47 | # "/.html" - so for this case, the template "pages/index.html" 48 | # should be used if you want to customize the homepage's template. 49 | 50 | # url("^$", "mezzanine.pages.views.page", {"slug": "/"}, name="home"), 51 | 52 | # HOMEPAGE FOR A BLOG-ONLY SITE 53 | # ----------------------------- 54 | # This pattern points the homepage to the blog post listing page, 55 | # and is useful for sites that are primarily blogs. If you use this 56 | # pattern, you'll also need to set BLOG_SLUG = "" in your 57 | # ``settings.py`` module, and delete the blog page object from the 58 | # page tree in the admin if it was installed. 59 | 60 | # url("^$", "mezzanine.blog.views.blog_post_list", name="home"), 61 | 62 | # MEZZANINE'S URLS 63 | # ---------------- 64 | # ADD YOUR OWN URLPATTERNS *ABOVE* THE LINE BELOW. 65 | # ``mezzanine.urls`` INCLUDES A *CATCH ALL* PATTERN 66 | # FOR PAGES, SO URLPATTERNS ADDED BELOW ``mezzanine.urls`` 67 | # WILL NEVER BE MATCHED! 68 | ("^%s/" % settings.EVENT_SLUG, include("mezzanine_agenda.urls")), 69 | 70 | # If you'd like more granular control over the patterns in 71 | # ``mezzanine.urls``, go right ahead and take the parts you want 72 | # from it, and use them directly below instead of using 73 | # ``mezzanine.urls``. 74 | ("^", include("mezzanine.urls")), 75 | 76 | # MOUNTING MEZZANINE UNDER A PREFIX 77 | # --------------------------------- 78 | # You can also mount all of Mezzanine's urlpatterns under a 79 | # URL prefix if desired. When doing this, you need to define the 80 | # ``SITE_PREFIX`` setting, which will contain the prefix. Eg: 81 | # SITE_PREFIX = "my/site/prefix" 82 | # For convenience, and to avoid repeating the prefix, use the 83 | # commented out pattern below (commenting out the one above of course) 84 | # which will make use of the ``SITE_PREFIX`` setting. Make sure to 85 | # add the import ``from django.conf import settings`` to the top 86 | # of this file as well. 87 | # Note that for any of the various homepage patterns above, you'll 88 | # need to use the ``SITE_PREFIX`` setting as well. 89 | 90 | # ("^%s/" % settings.SITE_PREFIX, include("mezzanine.urls")) 91 | 92 | ) 93 | 94 | # Adds ``STATIC_URL`` to the context of error pages, so that error 95 | # pages can use JS, CSS and images. 96 | handler404 = "mezzanine.core.views.page_not_found" 97 | handler500 = "mezzanine.core.views.server_error" 98 | -------------------------------------------------------------------------------- /mezzanine_agenda/templates/agenda/event_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% load i18n future mezzanine_tags event_tags keyword_tags disqus_tags %} 3 | 4 | {% block meta_title %}{% if page %}{{ page.richtextpage.meta_title }}{% else %}{% trans "Events" %}{% endif %}{% endblock %} 5 | 6 | {% block meta_keywords %}{% metablock %} 7 | {% keywords_for page as keywords %} 8 | {% for keyword in keywords %} 9 | {% if not forloop.first %}, {% endif %} 10 | {{ keyword }} 11 | {% endfor %} 12 | {% endmetablock %}{% endblock %} 13 | 14 | {% block meta_description %}{% metablock %} 15 | {{ page.description }} 16 | {% endmetablock %}{% endblock %} 17 | 18 | {% block title %} 19 | {% if page %} 20 | {% editable page.title %}{{ page.title }}{% endeditable %} 21 | {% else %} 22 | {% trans "Events" %} 23 | {% endif %} 24 | {% endblock %} 25 | 26 | {% block breadcrumb_menu %} 27 | {{ block.super }} 28 | {% if tag or location or year or month or author %} 29 |
  • {% spaceless %} 30 | {% if tag %} 31 | {% trans "Tag:" %} {{ tag }} 32 | {% else %}{% if location %} 33 | {% trans "Location:" %} {{ location }} 34 | {% else %}{% if year or month %} 35 | {% if month %}{{ month }}, {% endif %}{{ year }} 36 | {% else %}{% if author %} 37 | {% trans "Author:" %} {{ author.get_full_name|default:author.username }} 38 | {% endif %}{% endif %}{% endif %}{% endif %} 39 | {% endspaceless %} 40 |
  • 41 | {% endif %} 42 | {% endblock %} 43 | 44 | {% block main %} 45 | {% if tag or location or year or month or author %} 46 | {% block event_list_filterinfo %} 47 |

    48 | {% if tag %} 49 | {% trans "Viewing events tagged" %} {{ tag }} 50 | {% else %}{% if location %} 51 | {% trans "Viewing events for the location" %} {{ location }} 52 | {% else %}{% if year or month %} 53 | {% trans "Viewing events from" %} {% if month %}{{ month }}, {% endif %} 54 | {{ year }} 55 | {% else %}{% if author %} 56 | {% trans "Viewing events by" %} 57 | {{ author.get_full_name|default:author.username }} 58 | {% endif %}{% endif %}{% endif %}{% endif %} 59 | {% endblock %} 60 |

    61 | {% else %} 62 | {% if page %} 63 | {% block event_list_pagecontent %} 64 | {% editable page.richtextpage.content %} 65 | {{ page.richtextpage.content|richtext_filters|safe }} 66 | {% endeditable %} 67 | {% endblock %} 68 | {% endif %} 69 | {% endif %} 70 | 71 | {% block event_calendar %} 72 |

    73 | Subscribe to all events in Google Calendar/Outlook/iCal 74 |

    75 | {% endblock %} 76 | 77 | {% for event in events.object_list %} 78 | {% block event_list_event_title %} 79 | {% editable event.title %} 80 |

    81 | {{ event.title }} 82 |

    83 | {% endeditable %} 84 | {% endblock %} 85 | {% block event_list_event_metainfo %} 86 | {% editable event.start event.end event.location %} 87 | 97 | {% endeditable %} 98 | {% endblock %} 99 | 100 | {% if settings.EVENT_USE_FEATURED_IMAGE and event.featured_image %} 101 | {% block event_list_event_featured_image %} 102 | 103 | 104 | 105 | {% endblock %} 106 | {% endif %} 107 | 108 | {% block event_list_event_content %} 109 | {% editable event.content %} 110 | {{ event.description_from_content|safe }} 111 | {% endeditable %} 112 | {% endblock %} 113 | 114 | {% block event_list_event_links %} 115 |
    116 | {% keywords_for event as tags %} 117 | {% if tags %} 118 |
      119 | {% trans "Tags" %}: 120 | {% spaceless %} 121 | {% for tag in tags %} 122 |
    • {{ tag }}
    • 123 | {% endfor %} 124 | {% endspaceless %} 125 |
    126 | {% endif %} 127 |

    128 | {% trans "read more" %} 129 | {% if event.allow_comments %} 130 | / 131 | {% if settings.COMMENTS_DISQUS_SHORTNAME %} 132 | 134 | {% trans "Comments" %} 135 | 136 | {% else %} 137 | 138 | {% blocktrans count comments_count=event.comments_count %}{{ comments_count }} comment{% plural %}{{ comments_count }} comments{% endblocktrans %} 139 | 140 | {% endif %} 141 | {% endif %} 142 |

    143 |
    144 | {% endblock %} 145 | {% endfor %} 146 | 147 | {% pagination_for events %} 148 | 149 | {% if settings.COMMENTS_DISQUS_SHORTNAME %} 150 | {% include "generic/includes/disqus_counts.html" %} 151 | {% endif %} 152 | 153 | {% endblock %} 154 | 155 | {% block right_panel %} 156 | {% include "agenda/includes/filter_panel.html" %} 157 | {% endblock %} 158 | -------------------------------------------------------------------------------- /mezzanine_agenda/templates/agenda/event_detail.html: -------------------------------------------------------------------------------- 1 | {% extends "agenda/event_list.html" %} 2 | {% load mezzanine_tags comment_tags keyword_tags rating_tags i18n future disqus_tags event_tags %} 3 | 4 | {% block meta_title %}{{ event.meta_title }}{% endblock %} 5 | 6 | {% block meta_keywords %}{% metablock %} 7 | {% keywords_for event as tags %} 8 | {% for tag in tags %}{% if not forloop.first %}, {% endif %}{{ tag }}{% endfor %} 9 | {% endmetablock %}{% endblock %} 10 | 11 | {% block meta_description %}{% metablock %} 12 | {{ event.description }} 13 | {% endmetablock %}{% endblock %} 14 | 15 | {% block title %} 16 | {% editable event.title %}{{ event.title }}{% endeditable %} 17 | {% endblock %} 18 | 19 | {% block breadcrumb_menu %} 20 | {{ block.super }} 21 |
  • {{ event.title }}
  • 22 | {% endblock %} 23 | 24 | {% block main %} 25 | 26 | {% block event_detail_postedby %} 27 | {% editable event.start event.end event.location %} 28 | 37 | {% endeditable %} 38 | {% endblock %} 39 | {% block event_detail_commentlink %} 40 |

    41 | {% if event.allow_comments %} 42 | {% if settings.COMMENTS_DISQUS_SHORTNAME %} 43 | ({% spaceless %} 45 | {% trans "Comments" %} 46 | {% endspaceless %}) 47 | {% else %}({% spaceless %} 48 | {% blocktrans count comments_count=event.comments_count %}{{ comments_count }} comment{% plural %}{{ comments_count }} comments{% endblocktrans %} 49 | {% endspaceless %}) 50 | {% endif %} 51 | {% endif %} 52 |

    53 | {% endblock %} 54 | 55 | {% block event_detail_calendar %} 56 |

    57 | Add to Google Calendar 58 | 59 | 60 | Add to Outlook/iCal 61 |

    62 | {% endblock %} 63 | 64 | {% block event_detail_featured_image %} 65 | {% if settings.EVENT_USE_FEATURED_IMAGE and event.featured_image %} 66 |

    67 | {% endif %} 68 | {% endblock %} 69 | 70 | {% if settings.COMMENTS_DISQUS_SHORTNAME %} 71 | {% include "generic/includes/disqus_counts.html" %} 72 | {% endif %} 73 | 74 | {% block event_detail_content %} 75 | {% editable event.content %} 76 | {{ event.content|richtext_filters|safe }} 77 | {% endeditable %} 78 | {% endblock %} 79 | 80 | {% if event.location %} 81 | {% block event_detail_location %} 82 | {% editable event.location %} 83 |
    84 |
    85 |

    86 | {{ event.location.address|linebreaksbr }}
    87 | 88 | Get Directions 89 | 90 |

    91 | {% if settings.EVENT_GOOGLE_MAPS_API_KEY %} 92 | {% google_interactive_map event 621 250 10 %} 93 | {% else %} 94 | {% google_static_map event 621 250 10 %} 95 | {% endif %} 96 |
    97 |
    98 | {% endeditable %} 99 | {% endblock %} 100 | {% endif %} 101 | 102 | {% block event_detail_keywords %} 103 | {% keywords_for event as tags %} 104 | {% if tags %} 105 | {% spaceless %} 106 |
      107 |
    • {% trans "Tags" %}:
    • 108 | {% for tag in tags %} 109 |
    • {{ tag }}
    • 110 | {% endfor %} 111 |
    112 | {% endspaceless %} 113 | {% endif %} 114 | {% endblock %} 115 | 116 | {% block event_detail_rating %} 117 |
    118 |
    119 | {% rating_for event %} 120 |
    121 |
    122 | {% endblock %} 123 | 124 | {% block event_detail_sharebuttons %} 125 | {% set_short_url_for event %} 126 | 127 | 128 | {% endblock %} 129 | 130 | {% block event_previous_next %} 131 |
      132 | {% with event.get_previous_by_publish_date as previous %} 133 | {% if previous %} 134 | 137 | {% endif %} 138 | {% endwith %} 139 | {% with event.get_next_by_publish_date as next %} 140 | {% if next %} 141 | 144 | {% endif %} 145 | {% endwith %} 146 |
    147 | {% endblock %} 148 | 149 | {% block event_detail_comments %} 150 | {% if event.allow_comments %}{% comments_for event %}{% endif %} 151 | {% endblock %} 152 | 153 | {% endblock %} 154 | -------------------------------------------------------------------------------- /mezzanine_agenda/tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | try: 4 | from urllib.parse import urlparse 5 | except ImportError: 6 | from urlparse import urlparse 7 | 8 | from datetime import datetime, timedelta 9 | 10 | from django.core.urlresolvers import reverse 11 | from django.utils.unittest import skipUnless 12 | 13 | from mezzanine_agenda.models import Event, EventLocation 14 | from mezzanine.conf import settings 15 | 16 | from mezzanine.core.models import CONTENT_STATUS_DRAFT, CONTENT_STATUS_PUBLISHED 17 | from mezzanine.pages.models import RichTextPage 18 | from mezzanine.utils.tests import TestCase 19 | 20 | from datetime import datetime 21 | 22 | 23 | class EventTests(TestCase): 24 | 25 | def setUp(self): 26 | super(EventTests, self).setUp() 27 | self.eventlocation = EventLocation.objects.create( 28 | address='1 Susan St\nHindmarsh\nSouth Australia', 29 | ) 30 | self.eventlocation.save() 31 | self.unicode_eventlocation = EventLocation.objects.create( 32 | address='\u30b5\u30f3\u30b7\u30e3\u30a4\u30f360', 33 | ) 34 | self.unicode_eventlocation.save() 35 | self.event = Event.objects.create( 36 | slug='events/blah', 37 | title='THIS IS AN EVENT THAT IS PUBLISHED', 38 | start=datetime.now(), 39 | end=datetime.now()+timedelta(hours=4), 40 | location=self.eventlocation, 41 | status=CONTENT_STATUS_PUBLISHED, 42 | user=self._user, 43 | ) 44 | self.event.save() 45 | self.draft_event = Event.objects.create( 46 | slug='events/draft', 47 | title='THIS IS AN EVENT THAT IS A DRAFT', 48 | start=datetime.now(), 49 | end=datetime.now()+timedelta(hours=4), 50 | location=self.eventlocation, 51 | status=CONTENT_STATUS_DRAFT, 52 | user=self._user, 53 | ) 54 | self.draft_event.save() 55 | self.unicode_event = Event.objects.create( 56 | slug='cont/\u30b5\u30f3\u30b7\u30e3\u30a4\u30f360', 57 | title='\xe9\x9d\x9eASCII\xe3\x82\xbf\xe3\x82\xa4\xe3\x83\x88\xe3\x83\xab', 58 | start=datetime.now(), 59 | end=datetime.now()+timedelta(hours=4), 60 | location=self.unicode_eventlocation, 61 | status=CONTENT_STATUS_PUBLISHED, 62 | user=self._user, 63 | ) 64 | self.unicode_event.save() 65 | self.events = (self.event, self.unicode_event) 66 | self.event_page = RichTextPage.objects.create(title="events", slug=settings.EVENT_SLUG) 67 | 68 | 69 | def test_event_views(self): 70 | """ 71 | Basic status code test for agenda views. 72 | """ 73 | response = self.client.get(reverse("event_list")) 74 | self.assertEqual(response.status_code, 200) 75 | self.assertContains(response, self.event.title) 76 | self.assertNotContains(response, self.draft_event.title) 77 | response = self.client.get(reverse("event_feed", args=("rss",))) 78 | self.assertEqual(response.status_code, 200) 79 | response = self.client.get(reverse("event_feed", args=("atom",))) 80 | self.assertEqual(response.status_code, 200) 81 | event = Event.objects.create(title="Event", start=datetime.now(), user=self._user, 82 | status=CONTENT_STATUS_PUBLISHED) 83 | response = self.client.get(event.get_absolute_url()) 84 | self.assertEqual(response.status_code, 200) 85 | 86 | @skipUnless("mezzanine.accounts" in settings.INSTALLED_APPS and 87 | "mezzanine.pages" in settings.INSTALLED_APPS, 88 | "accounts and pages apps required") 89 | def test_login_protected_event(self): 90 | """ 91 | Test the events is login protected if its page has login_required 92 | set to True. 93 | """ 94 | self.event_page.login_required=True 95 | self.event_page.save() 96 | response = self.client.get(reverse("event_list"), follow=True) 97 | self.assertEqual(response.status_code, 200) 98 | self.assertTrue(len(response.redirect_chain) > 0) 99 | redirect_path = urlparse(response.redirect_chain[0][0]).path 100 | self.assertEqual(redirect_path, settings.LOGIN_URL) 101 | self.event_page.login_required=False 102 | self.event_page.save() 103 | 104 | def test_clean(self): 105 | """ 106 | Test the events geocoding functionality. 107 | """ 108 | self.eventlocation.clean() 109 | self.assertAlmostEqual(self.eventlocation.lat, -34.907924, places=5) 110 | self.assertAlmostEqual(self.eventlocation.lon, 138.567624, places=5) 111 | self.assertEqual(self.eventlocation.mappable_location, '1 Susan Street, Hindmarsh SA 5007, Australia') 112 | self.unicode_eventlocation.clean() 113 | self.assertAlmostEqual(self.unicode_eventlocation.lat, 35.729534, places=5) 114 | self.assertAlmostEqual(self.unicode_eventlocation.lon, 139.718055, places=5) 115 | 116 | def test_icalendars(self): 117 | """ 118 | Test the icalendar views. 119 | """ 120 | for event in self.events: 121 | response = self.client.get(reverse("icalendar_event", args=(event.slug,))) 122 | self.assertEqual(response.status_code, 200) 123 | self.assertEqual(response['Content-Type'], 'text/calendar') 124 | response = self.client.get(reverse("icalendar")) 125 | self.assertEqual(response.status_code, 200) 126 | self.assertEqual(response['Content-Type'], 'text/calendar') 127 | -------------------------------------------------------------------------------- /mezzanine_agenda/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import mezzanine.core.fields 6 | import mezzanine.utils.models 7 | from django.conf import settings 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('sites', '0001_initial'), 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='Event', 20 | fields=[ 21 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 22 | ('comments_count', models.IntegerField(default=0, editable=False)), 23 | ('keywords_string', models.CharField(max_length=500, editable=False, blank=True)), 24 | ('rating_count', models.IntegerField(default=0, editable=False)), 25 | ('rating_sum', models.IntegerField(default=0, editable=False)), 26 | ('rating_average', models.FloatField(default=0, editable=False)), 27 | ('title', models.CharField(max_length=500, verbose_name='Title')), 28 | ('slug', models.CharField(help_text='Leave blank to have the URL auto-generated from the title.', max_length=2000, null=True, verbose_name='URL', blank=True)), 29 | ('_meta_title', models.CharField(help_text='Optional title to be used in the HTML title tag. If left blank, the main title field will be used.', max_length=500, null=True, verbose_name='Title', blank=True)), 30 | ('description', models.TextField(verbose_name='Description', blank=True)), 31 | ('gen_description', models.BooleanField(default=True, help_text='If checked, the description will be automatically generated from content. Uncheck if you want to manually set a custom description.', verbose_name='Generate description')), 32 | ('created', models.DateTimeField(null=True, editable=False)), 33 | ('updated', models.DateTimeField(null=True, editable=False)), 34 | ('status', models.IntegerField(default=2, help_text='With Draft chosen, will only be shown for admin users on the site.', verbose_name='Status', choices=[(1, 'Draft'), (2, 'Published')])), 35 | ('publish_date', models.DateTimeField(help_text="With Published chosen, won't be shown until this time", null=True, verbose_name='Published from', db_index=True, blank=True)), 36 | ('expiry_date', models.DateTimeField(help_text="With Published chosen, won't be shown after this time", null=True, verbose_name='Expires on', blank=True)), 37 | ('short_url', models.URLField(null=True, blank=True)), 38 | ('in_sitemap', models.BooleanField(default=True, verbose_name='Show in sitemap')), 39 | ('content', mezzanine.core.fields.RichTextField(verbose_name='Content')), 40 | ('start', models.DateTimeField(verbose_name='Start')), 41 | ('end', models.DateTimeField(null=True, verbose_name='End', blank=True)), 42 | ('facebook_event', models.BigIntegerField(null=True, verbose_name='Facebook', blank=True)), 43 | ('allow_comments', models.BooleanField(default=True, verbose_name='Allow comments')), 44 | ('featured_image', mezzanine.core.fields.FileField(max_length=255, null=True, verbose_name='Featured Image', blank=True)), 45 | ], 46 | options={ 47 | 'ordering': ('-start',), 48 | 'verbose_name': 'Event', 49 | 'verbose_name_plural': 'Events', 50 | }, 51 | bases=(models.Model, mezzanine.utils.models.AdminThumbMixin), 52 | ), 53 | migrations.CreateModel( 54 | name='EventLocation', 55 | fields=[ 56 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 57 | ('title', models.CharField(max_length=500, verbose_name='Title')), 58 | ('slug', models.CharField(help_text='Leave blank to have the URL auto-generated from the title.', max_length=2000, null=True, verbose_name='URL', blank=True)), 59 | ('address', models.TextField()), 60 | ('mappable_location', models.CharField(help_text='This address will be used to calculate latitude and longitude. Leave blank and set Latitude and Longitude to specify the location yourself, or leave all three blank to auto-fill from the Location field.', max_length=128, blank=True)), 61 | ('lat', models.DecimalField(decimal_places=7, max_digits=10, blank=True, help_text='Calculated automatically if mappable location is set.', null=True, verbose_name='Latitude')), 62 | ('lon', models.DecimalField(decimal_places=7, max_digits=10, blank=True, help_text='Calculated automatically if mappable location is set.', null=True, verbose_name='Longitude')), 63 | ('site', models.ForeignKey(editable=False, to='sites.Site')), 64 | ], 65 | options={ 66 | 'ordering': ('title',), 67 | 'verbose_name': 'Event Location', 68 | 'verbose_name_plural': 'Event Locations', 69 | }, 70 | ), 71 | migrations.AddField( 72 | model_name='event', 73 | name='location', 74 | field=models.ForeignKey(blank=True, to='mezzanine_agenda.EventLocation', null=True), 75 | ), 76 | migrations.AddField( 77 | model_name='event', 78 | name='site', 79 | field=models.ForeignKey(editable=False, to='sites.Site'), 80 | ), 81 | migrations.AddField( 82 | model_name='event', 83 | name='user', 84 | field=models.ForeignKey(related_name='events', verbose_name='Author', to=settings.AUTH_USER_MODEL), 85 | ), 86 | ] 87 | -------------------------------------------------------------------------------- /README.mdown: -------------------------------------------------------------------------------- 1 | # mezzanine-agenda 2 | 3 | This plugin gives you Event functionality for your Mezzanine sites. Very similiar to mezzanine-events but rather than implementing Events as a custom Page object and managing through the Pages admin it is it's own content type that is managed on its own, similiar to Blog Posts. This allows for standard viewing and filtering of these events based on the date. 4 | 5 | ## Features 6 | 7 | * Show your visitors where to go 8 | * Embed a map of the location in one line of code with the Google Static Maps template tag 9 | * Provide a "Get Directions" link so users can go there in one click 10 | * Let your visitors add a single event or subscribe to all future events in Google Calendar, Outlook, iCal and more with Google Calendar and webcal:// URLs and iCalendar files 11 | * Filter events by date, location and author 12 | * RSS/Atom feeds 13 | * Event featured image 14 | * Event comments/ratings 15 | 16 | ## Installation 17 | 18 | * Run `pip install mezzanine-agenda` (or, if you want to hack on mezzanine-agenda, clone it and run `pip install -e path/to/repo`) 19 | * Add `"mezzanine_agenda"` to your `INSTALLED_APPS` 20 | * Add `("^%s/" % settings.EVENT_SLUG, include("mezzanine_agenda.urls"))` to your `urls.py` 21 | * Set either the `TIME_ZONE` or `EVENT_TIME_ZONE` settings. (If neither of these settings are set, the Google Calendar links will not work as expected.) 22 | * Migrate your database 23 | * Create RichText Page `Events` similiar to `Blog` (can be done via python manage.py loaddata events_page or through admin). Ensure the page slug = settings.EVENT_SLUG. 24 | 25 | ## Creating Templates 26 | 27 | In addition to the documentation here, take a look at how the default templates in the `mezzanine_agenda/templates` directory are written. 28 | 29 | ### Event List pages 30 | 31 | The template for an Event List page is `templates/agenda/event_list.html`. 32 | 33 | Iterate over `events` to get at the events inside the container. You can then use all of the properties and template tags listed above on these objects. 34 | 35 | ### Event Detail pages 36 | 37 | The template for an Event Detail page is `templates/agenda/event_detail.html`. 38 | 39 | The Event object is available at `event`. It has the following properties: 40 | 41 | * Dates and times: `start`, `end` 42 | * Location info: `location.address`, `location.mappable_location`, `lat`, `lon` 43 | * Featured Image: `featured_image` 44 | 45 | ## Template Tags 46 | 47 | The following template tags and filters can be used: 48 | - `{% event_months as months %}` - Put a list of dates for events into the template context. 49 | - `{% event_locations as locations %}` - Put a list of locations for events into the template context. 50 | - `{% event_authors as authors %}` - Put a list of authors (users) for events into the template context. 51 | - `{% recent_events limit=5 tag="django" location="home" username="admin" as recent_events %}` - Put a list of recent events into the template context. A tag title or slug, location title or slug or author's username can also be specified to filter the recent events returned. 52 | - `{% upcoming_events limit=5 tag="django" location="home" username="admin" as upcoming_events %}` - Put a list of upcoming events into the template context. A tag title or slug, location title or slug or author's username can also be specified to filter the recent events returned. 53 | - `{% google_static_map event %}` - Produces a Google static map centred around the event location, zoomed to the specified level. Produces the entire `img` tag, not just the URL. 54 | - `{% icalendar_url %}` - Returns the URL to an iCalendar file containing this event. Upon downloading this file, most calendar software including Outlook and iCal will handle this by adding it to their calendars. 55 | - `{{ event|google_calendar_url }}` - Returns a Google Calendar template URL. Google Calendar users can click a link to this URL to add the event to their calendar. 56 | - `{{ event|google_nav_url }}` - Returns the URL to a page on Google Maps showing the location . 57 | 58 | ## Settings 59 | 60 | * `EVENT_USE_FEATURED_IMAGE` - Enable featured images in events. Default: `False`. 61 | * `EVENT_URLS_DATE_FORMAT` - A string containing the value ``year``, ``month``, or ``day``, which controls the granularity of the date portion in the URL for each event. Eg: ``year`` will define URLs in the format /events/yyyy/slug/, while ``day`` will define URLs with the format /events/yyyy/mm/dd/slug/. An empty string means the URLs will only use the slug, and not contain any portion of the date at all. Default: `''`. 62 | * `EVENT_PER_PAGE` - Number of events shown on a event listing page. Default: `5`. 63 | * `EVENT_RSS_LIMIT` - Number of most recent events shown in the RSS feed. Set to ``None`` to display all events in the RSS feed. Default: `20`. 64 | * `EVENT_SLUG` - Enable featured images in events. Default: `'events'`. 65 | * `EVENT_GOOGLE_MAPS_DOMAIN` - The Google Maps country domain to query for geocoding. Setting this accurately improves results when users forget to enter a country in the mappable address. Default: `'maps.google.com'`. 66 | * `EVENT_HIDPI_STATIC_MAPS` - Whether the `{% google_static_map %}` template tag generates a map suitable for high DPI displays such as the MacBook Pro with Retina Display and many newer smartphones. Default: `True`. 67 | * `EVENT_TIME_ZONE` - The time zone that the event dates and times are in. Either this or the `TIME_ZONE` setting needs to be set. 68 | 69 | ## License 70 | 71 | Copyright (C) 2012 St Barnabas Theological College 72 | 73 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 74 | 75 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 76 | 77 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 78 | -------------------------------------------------------------------------------- /mezzanine_agenda/views.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from future.builtins import str 3 | from future.builtins import int 4 | from calendar import month_name 5 | 6 | from datetime import datetime 7 | 8 | from django.contrib.sites.models import Site 9 | from django.db.models import Q 10 | from django.http import Http404, HttpResponse 11 | from django.shortcuts import get_object_or_404 12 | 13 | from icalendar import Calendar 14 | 15 | from mezzanine_agenda import __version__ 16 | from mezzanine_agenda.models import Event, EventLocation 17 | from mezzanine_agenda.feeds import EventsRSS, EventsAtom 18 | from mezzanine.conf import settings 19 | from mezzanine.generic.models import Keyword 20 | from mezzanine.pages.models import Page 21 | from mezzanine.utils.views import render, paginate 22 | from mezzanine.utils.models import get_user_model 23 | from mezzanine.utils.sites import current_site_id 24 | 25 | User = get_user_model() 26 | 27 | 28 | def event_list(request, tag=None, year=None, month=None, username=None, 29 | location=None, template="agenda/event_list.html"): 30 | """ 31 | Display a list of events that are filtered by tag, year, month, 32 | author or location. Custom templates are checked for using the name 33 | ``agenda/event_list_XXX.html`` where ``XXX`` is either the 34 | location slug or author's username if given. 35 | """ 36 | settings.use_editable() 37 | templates = [] 38 | events = Event.objects.published(for_user=request.user) 39 | if tag is not None: 40 | tag = get_object_or_404(Keyword, slug=tag) 41 | events = events.filter(keywords__keyword=tag) 42 | if year is not None: 43 | events = events.filter(start__year=year) 44 | if month is not None: 45 | events = events.filter(start__month=month) 46 | try: 47 | month = month_name[int(month)] 48 | except IndexError: 49 | raise Http404() 50 | if location is not None: 51 | location = get_object_or_404(EventLocation, slug=location) 52 | events = events.filter(location=location) 53 | templates.append(u"agenda/event_list_%s.html" % 54 | str(location.slug)) 55 | author = None 56 | if username is not None: 57 | author = get_object_or_404(User, username=username) 58 | events = events.filter(user=author) 59 | templates.append(u"agenda/event_list_%s.html" % username) 60 | if not tag and not year and not location and not username: 61 | #Get upcoming events/ongoing events 62 | events = events.filter(Q(start__gt=datetime.now()) | Q(end__gt=datetime.now())).order_by("start") 63 | 64 | prefetch = ("keywords__keyword",) 65 | events = events.select_related("user").prefetch_related(*prefetch) 66 | events = paginate(events, request.GET.get("page", 1), 67 | settings.EVENT_PER_PAGE, 68 | settings.MAX_PAGING_LINKS) 69 | context = {"events": events, "year": year, "month": month, 70 | "tag": tag, "location": location, "author": author} 71 | templates.append(template) 72 | return render(request, templates, context) 73 | 74 | 75 | def event_detail(request, slug, year=None, month=None, day=None, 76 | template="agenda/event_detail.html"): 77 | """. Custom templates are checked for using the name 78 | ``agenda/event_detail_XXX.html`` where ``XXX`` is the agenda 79 | events's slug. 80 | """ 81 | events = Event.objects.published( 82 | for_user=request.user).select_related() 83 | event = get_object_or_404(events, slug=slug) 84 | context = {"event": event, "editable_obj": event} 85 | templates = [u"agenda/event_detail_%s.html" % str(slug), template] 86 | return render(request, templates, context) 87 | 88 | 89 | def event_feed(request, format, **kwargs): 90 | """ 91 | Events feeds - maps format to the correct feed view. 92 | """ 93 | try: 94 | return {"rss": EventsRSS, "atom": EventsAtom}[format](**kwargs)(request) 95 | except KeyError: 96 | raise Http404() 97 | 98 | 99 | def _make_icalendar(): 100 | """ 101 | Create an icalendar object. 102 | """ 103 | icalendar = Calendar() 104 | icalendar.add('prodid', 105 | '-//mezzanine-agenda//NONSGML V{}//EN'.format(__version__)) 106 | icalendar.add('version', '2.0') # version of the format, not the product! 107 | return icalendar 108 | 109 | 110 | def icalendar_event(request, slug, year=None, month=None, day=None): 111 | """ 112 | Returns the icalendar for a specific event. 113 | """ 114 | events = Event.objects.published( 115 | for_user=request.user).select_related() 116 | event = get_object_or_404(events, slug=slug) 117 | 118 | icalendar = _make_icalendar() 119 | icalendar_event = event.get_icalendar_event() 120 | icalendar.add_component(icalendar_event) 121 | 122 | return HttpResponse(icalendar.to_ical(), content_type="text/calendar") 123 | 124 | 125 | def icalendar(request, tag=None, year=None, month=None, username=None, 126 | location=None): 127 | """ 128 | Returns the icalendar for a group of events that are filtered by tag, 129 | year, month, author or location. 130 | """ 131 | settings.use_editable() 132 | events = Event.objects.published(for_user=request.user) 133 | if tag is not None: 134 | tag = get_object_or_404(Keyword, slug=tag) 135 | events = events.filter(keywords__keyword=tag) 136 | if year is not None: 137 | events = events.filter(start__year=year) 138 | if month is not None: 139 | events = events.filter(start__month=month) 140 | try: 141 | month = month_name[int(month)] 142 | except IndexError: 143 | raise Http404() 144 | if location is not None: 145 | location = get_object_or_404(EventLocation, slug=location) 146 | events = events.filter(location=location) 147 | author = None 148 | if username is not None: 149 | author = get_object_or_404(User, username=username) 150 | events = events.filter(user=author) 151 | if not tag and not year and not location and not username: 152 | #Get upcoming events/ongoing events 153 | events = events.filter(Q(start__gt=datetime.now()) | Q(end__gt=datetime.now())).order_by("start") 154 | 155 | prefetch = ("keywords__keyword",) 156 | events = events.select_related("user").prefetch_related(*prefetch) 157 | 158 | icalendar = _make_icalendar() 159 | for event in events: 160 | icalendar_event = event.get_icalendar_event() 161 | icalendar.add_component(icalendar_event) 162 | 163 | return HttpResponse(icalendar.to_ical(), content_type="text/calendar") 164 | -------------------------------------------------------------------------------- /mezzanine_agenda/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from future.builtins import str 3 | 4 | from django.db import models 5 | from django.contrib.sites.models import Site 6 | from django.core.exceptions import ValidationError 7 | from django.core.urlresolvers import reverse 8 | from django.utils.translation import ugettext_lazy as _ 9 | 10 | from geopy.geocoders import GoogleV3 as GoogleMaps 11 | from geopy.geocoders.googlev3 import GQueryError 12 | 13 | from icalendar import Event as IEvent 14 | 15 | from mezzanine.conf import settings 16 | from mezzanine.core.fields import FileField 17 | from mezzanine.core.models import Displayable, Ownable, RichText, Slugged 18 | from mezzanine.generic.fields import CommentsField, RatingField 19 | from mezzanine.utils.models import AdminThumbMixin, upload_to 20 | from mezzanine.utils.sites import current_site_id 21 | 22 | 23 | class Event(Displayable, Ownable, RichText, AdminThumbMixin): 24 | """ 25 | A event. 26 | """ 27 | 28 | start = models.DateTimeField(_("Start")) 29 | end = models.DateTimeField(_("End"), blank=True, null=True) 30 | location = models.ForeignKey("EventLocation", blank=True, null=True) 31 | facebook_event = models.BigIntegerField(_('Facebook'), blank=True, null=True) # 32 | allow_comments = models.BooleanField(verbose_name=_("Allow comments"), 33 | default=True) 34 | comments = CommentsField(verbose_name=_("Comments")) 35 | rating = RatingField(verbose_name=_("Rating")) 36 | featured_image = FileField(verbose_name=_("Featured Image"), 37 | upload_to=upload_to("mezzanine_agenda.Event.featured_image", "event"), 38 | format="Image", max_length=255, null=True, blank=True) 39 | 40 | admin_thumb_field = "featured_image" 41 | 42 | class Meta: 43 | verbose_name = _("Event") 44 | verbose_name_plural = _("Events") 45 | ordering = ("-start",) 46 | 47 | def clean(self): 48 | """ 49 | Validate end date is after the start date. 50 | """ 51 | super(Event, self).clean() 52 | 53 | if self.end and self.start > self.end: 54 | raise ValidationError("Start must be sooner than end.") 55 | 56 | def get_absolute_url(self): 57 | """ 58 | URLs for events can either be just their slug, or prefixed 59 | with a portion of the post's publish date, controlled by the 60 | setting ``EVENT_URLS_DATE_FORMAT``, which can contain the value 61 | ``year``, ``month``, or ``day``. Each of these maps to the name 62 | of the corresponding urlpattern, and if defined, we loop through 63 | each of these and build up the kwargs for the correct urlpattern. 64 | The order which we loop through them is important, since the 65 | order goes from least granualr (just year) to most granular 66 | (year/month/day). 67 | """ 68 | url_name = "event_detail" 69 | kwargs = {"slug": self.slug} 70 | date_parts = ("year", "month", "day") 71 | if settings.EVENT_URLS_DATE_FORMAT in date_parts: 72 | url_name = "event_detail_%s" % settings.EVENT_URLS_DATE_FORMAT 73 | for date_part in date_parts: 74 | date_value = str(getattr(self.publish_date, date_part)) 75 | if len(date_value) == 1: 76 | date_value = "0%s" % date_value 77 | kwargs[date_part] = date_value 78 | if date_part == settings.EVENT_URLS_DATE_FORMAT: 79 | break 80 | return reverse(url_name, kwargs=kwargs) 81 | 82 | def get_icalendar_event(self): 83 | """ 84 | Builds an icalendar.event object from event data. 85 | """ 86 | icalendar_event = IEvent() 87 | icalendar_event.add('summary'.encode("utf-8"), self.title) 88 | icalendar_event.add('url', 'http://{domain}{url}'.format( 89 | domain=Site.objects.get(id=current_site_id()).domain, 90 | url=self.get_absolute_url(), 91 | )) 92 | if self.location: 93 | icalendar_event.add('location'.encode("utf-8"), self.location.address) 94 | icalendar_event.add('dtstamp', self.start) 95 | icalendar_event.add('dtstart', self.start) 96 | if self.end: 97 | icalendar_event.add('dtend', self.end) 98 | icalendar_event['uid'.encode("utf-8")] = "event-{id}@{domain}".format( 99 | id=self.id, 100 | domain=Site.objects.get(id=current_site_id()).domain, 101 | ).encode("utf-8") 102 | return icalendar_event 103 | 104 | 105 | class EventLocation(Slugged): 106 | """ 107 | A Event Location. 108 | """ 109 | 110 | address = models.TextField() 111 | mappable_location = models.CharField(max_length=128, blank=True, help_text="This address will be used to calculate latitude and longitude. Leave blank and set Latitude and Longitude to specify the location yourself, or leave all three blank to auto-fill from the Location field.") 112 | lat = models.DecimalField(max_digits=10, decimal_places=7, blank=True, null=True, verbose_name="Latitude", help_text="Calculated automatically if mappable location is set.") 113 | lon = models.DecimalField(max_digits=10, decimal_places=7, blank=True, null=True, verbose_name="Longitude", help_text="Calculated automatically if mappable location is set.") 114 | 115 | class Meta: 116 | verbose_name = _("Event Location") 117 | verbose_name_plural = _("Event Locations") 118 | ordering = ("title",) 119 | 120 | def clean(self): 121 | """ 122 | Validate set/validate mappable_location, longitude and latitude. 123 | """ 124 | super(EventLocation, self).clean() 125 | 126 | if self.lat and not self.lon: 127 | raise ValidationError("Longitude required if specifying latitude.") 128 | 129 | if self.lon and not self.lat: 130 | raise ValidationError("Latitude required if specifying longitude.") 131 | 132 | if not (self.lat and self.lon) and not self.mappable_location: 133 | self.mappable_location = self.address.replace("\n",", ") 134 | 135 | if self.mappable_location and not (self.lat and self.lon): #location should always override lat/long if set 136 | g = GoogleMaps(domain=settings.EVENT_GOOGLE_MAPS_DOMAIN) 137 | try: 138 | mappable_location, (lat, lon) = g.geocode(self.mappable_location.encode('utf-8')) 139 | except GQueryError as e: 140 | raise ValidationError("The mappable location you specified could not be found on {service}: \"{error}\" Try changing the mappable location, removing any business names, or leaving mappable location blank and using coordinates from getlatlon.com.".format(service="Google Maps", error=e.message)) 141 | except ValueError as e: 142 | raise ValidationError("The mappable location you specified could not be found on {service}: \"{error}\" Try changing the mappable location, removing any business names, or leaving mappable location blank and using coordinates from getlatlon.com.".format(service="Google Maps", error=e.message)) 143 | self.mappable_location = mappable_location 144 | self.lat = lat 145 | self.lon = lon 146 | 147 | @models.permalink 148 | def get_absolute_url(self): 149 | return ("event_list_location", (), {"location": self.slug}) 150 | -------------------------------------------------------------------------------- /mezzanine_agenda/templatetags/event_tags.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | from datetime import datetime 3 | 4 | from django import template 5 | from django.contrib.sites.models import Site 6 | from django.core.urlresolvers import reverse 7 | from django.db.models import Count, Q 8 | from django.utils import timezone 9 | from django.utils.http import urlquote as quote 10 | from django.utils.safestring import mark_safe 11 | 12 | from mezzanine_agenda.models import Event, EventLocation 13 | from mezzanine.conf import settings 14 | from mezzanine.core.managers import SearchableQuerySet 15 | from mezzanine.generic.models import Keyword 16 | from mezzanine.pages.models import Page 17 | from mezzanine.template import Library 18 | from mezzanine.utils.models import get_user_model 19 | from mezzanine.utils.sites import current_site_id 20 | 21 | import pytz 22 | 23 | from time import strptime 24 | 25 | User = get_user_model() 26 | 27 | register = Library() 28 | 29 | 30 | @register.as_tag 31 | def event_months(*args): 32 | """ 33 | Put a list of dates for events into the template context. 34 | """ 35 | if settings.EVENT_TIME_ZONE != "": 36 | app_timezone = pytz.timezone(settings.EVENT_TIME_ZONE) 37 | else: 38 | app_timezone = timezone.get_default_timezone() 39 | dates = Event.objects.published().values_list("start", flat=True) 40 | correct_timezone_dates = [timezone.make_naive(date, app_timezone) for date in dates] 41 | date_dicts = [{"date": datetime(date.year, date.month, 1)} for date in correct_timezone_dates] 42 | month_dicts = [] 43 | for date_dict in date_dicts: 44 | if date_dict not in month_dicts: 45 | month_dicts.append(date_dict) 46 | for i, date_dict in enumerate(month_dicts): 47 | month_dicts[i]["event_count"] = date_dicts.count(date_dict) 48 | return month_dicts 49 | 50 | 51 | @register.as_tag 52 | def event_locations(*args): 53 | """ 54 | Put a list of locations for events into the template context. 55 | """ 56 | events = Event.objects.published() 57 | locations = EventLocation.objects.filter(event__in=events) 58 | return list(locations.annotate(event_count=Count("event"))) 59 | 60 | 61 | @register.as_tag 62 | def event_authors(*args): 63 | """ 64 | Put a list of authors (users) for events into the template context. 65 | """ 66 | events = Event.objects.published() 67 | authors = User.objects.filter(events__in=events) 68 | return list(authors.annotate(event_count=Count("events"))) 69 | 70 | 71 | @register.as_tag 72 | def recent_events(limit=5, tag=None, username=None, location=None): 73 | """ 74 | Put a list of recent events into the template 75 | context. A tag title or slug, location title or slug or author's 76 | username can also be specified to filter the recent events returned. 77 | 78 | Usage:: 79 | 80 | {% recent_events 5 as recent_events %} 81 | {% recent_events limit=5 tag="django" as recent_events %} 82 | {% recent_events limit=5 location="home" as recent_events %} 83 | {% recent_events 5 username=admin as recent_pevents %} 84 | 85 | """ 86 | events = Event.objects.published().select_related("user").order_by('-start') 87 | events = events.filter(end__lt=datetime.now()) 88 | title_or_slug = lambda s: Q(title=s) | Q(slug=s) 89 | if tag is not None: 90 | try: 91 | tag = Keyword.objects.get(title_or_slug(tag)) 92 | events = events.filter(keywords__keyword=tag) 93 | except Keyword.DoesNotExist: 94 | return [] 95 | if location is not None: 96 | try: 97 | location = EventLocation.objects.get(title_or_slug(location)) 98 | events = events.filter(location=location) 99 | except EventLocation.DoesNotExist: 100 | return [] 101 | if username is not None: 102 | try: 103 | author = User.objects.get(username=username) 104 | events = events.filter(user=author) 105 | except User.DoesNotExist: 106 | return [] 107 | return list(events[:limit]) 108 | 109 | 110 | @register.as_tag 111 | def upcoming_events(limit=5, tag=None, username=None, location=None): 112 | """ 113 | Put a list of upcoming events into the template 114 | context. A tag title or slug, location title or slug or author's 115 | username can also be specified to filter the upcoming events returned. 116 | 117 | Usage:: 118 | 119 | {% upcoming_events 5 as upcoming_events %} 120 | {% upcoming_events limit=5 tag="django" as upcoming_events %} 121 | {% upcoming_events limit=5 location="home" as upcoming_events %} 122 | {% upcoming_events 5 username=admin as upcoming_events %} 123 | 124 | """ 125 | events = Event.objects.published().select_related("user").order_by('start') 126 | #Get upcoming events/ongoing events 127 | events = events.filter(Q(start__gt=datetime.now()) | Q(end__gt=datetime.now())) 128 | title_or_slug = lambda s: Q(title=s) | Q(slug=s) 129 | if tag is not None: 130 | try: 131 | tag = Keyword.objects.get(title_or_slug(tag)) 132 | events = events.filter(keywords__keyword=tag) 133 | except Keyword.DoesNotExist: 134 | return [] 135 | if location is not None: 136 | try: 137 | location = EventLocation.objects.get(title_or_slug(location)) 138 | events = events.filter(location=location) 139 | except EventLocation.DoesNotExist: 140 | return [] 141 | if username is not None: 142 | try: 143 | author = User.objects.get(username=username) 144 | events = events.filter(user=author) 145 | except User.DoesNotExist: 146 | return [] 147 | return list(events[:limit]) 148 | 149 | 150 | def _get_utc(datetime): 151 | """ 152 | Convert datetime object to be timezone aware and in UTC. 153 | """ 154 | if settings.EVENT_TIME_ZONE != "": 155 | app_timezone = pytz.timezone(settings.EVENT_TIME_ZONE) 156 | else: 157 | app_timezone = timezone.get_default_timezone() 158 | 159 | # make the datetime aware 160 | if timezone.is_naive(datetime): 161 | datetime = timezone.make_aware(datetime, app_timezone) 162 | 163 | # now, make it UTC 164 | datetime = timezone.make_naive(datetime, timezone.utc) 165 | 166 | return datetime 167 | 168 | 169 | @register.filter(is_safe=True) 170 | def google_calendar_url(event): 171 | """ 172 | Generates a link to add the event to your google calendar. 173 | """ 174 | if not isinstance(event, Event): 175 | return '' 176 | title = quote(event.title) 177 | start_date = _get_utc(event.start).strftime("%Y%m%dT%H%M%SZ") 178 | if event.end: 179 | end_date = _get_utc(event.end).strftime("%Y%m%dT%H%M%SZ") 180 | else: 181 | end_date = start_date 182 | url = Site.objects.get(id=current_site_id()).domain + event.get_absolute_url() 183 | if event.location: 184 | location = quote(event.location.mappable_location) 185 | else: 186 | location = None 187 | return "http://www.google.com/calendar/event?action=TEMPLATE&text={title}&dates={start_date}/{end_date}&sprop=website:{url}&location={location}&trp=true".format(**locals()) 188 | 189 | 190 | @register.filter(is_safe=True) 191 | def google_nav_url(event): 192 | """ 193 | Generates a link to get directions to an event with google maps. 194 | """ 195 | if not isinstance(event, Event): 196 | return '' 197 | location = quote(event.location.mappable_location) 198 | return "https://{}/maps?daddr={}".format(settings.EVENT_GOOGLE_MAPS_DOMAIN, location) 199 | 200 | 201 | @register.simple_tag 202 | def google_static_map(event, width, height, zoom): 203 | """ 204 | Generates a static google map for the event location. 205 | """ 206 | marker = quote('{:.6},{:.6}'.format(event.location.lat, event.location.lon)) 207 | if settings.EVENT_HIDPI_STATIC_MAPS: 208 | scale = 2 209 | else: 210 | scale = 1 211 | return mark_safe("".format(**locals())) 212 | 213 | 214 | @register.simple_tag 215 | def google_interactive_map(event, width, height, zoom, map_type='roadmap'): 216 | """ 217 | Generates an interactive google map for the event location. 218 | """ 219 | location = quote(event.location.mappable_location) 220 | api_key = settings.EVENT_GOOGLE_MAPS_API_KEY 221 | center = marker = quote('{:.6},{:.6}'.format(event.location.lat, event.location.lon)) 222 | return mark_safe(''.format(**locals())) 223 | 224 | 225 | @register.simple_tag(takes_context=True) 226 | def icalendar_url(context): 227 | """ 228 | Generates the link to the icalendar view for the current page. 229 | """ 230 | if context.get("event"): 231 | return "%sevent.ics" % context["event"].get_absolute_url() 232 | else: 233 | if context.get("tag"): 234 | return reverse("icalendar_tag", args=(context["tag"],)) 235 | elif context.get("year") and context.get("month"): 236 | return reverse("icalendar_month", args=(context["year"], strptime(context["month"], '%B').tm_mon)) 237 | elif context.get("year"): 238 | return reverse("icalendar_year", args=(context["year"],)) 239 | elif context.get("location"): 240 | return reverse("icalendar_location", args=(context["location"].slug,)) 241 | elif context.get("author"): 242 | return reverse("icalendar_author", args=(context["author"],)) 243 | else: 244 | return reverse("icalendar") 245 | -------------------------------------------------------------------------------- /test_project/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | ###################### 4 | # MEZZANINE SETTINGS # 5 | ###################### 6 | 7 | # The following settings are already defined with default values in 8 | # the ``defaults.py`` module within each of Mezzanine's apps, but are 9 | # common enough to be put here, commented out, for convenient 10 | # overriding. Please consult the settings documentation for a full list 11 | # of settings Mezzanine implements: 12 | # http://mezzanine.jupo.org/docs/configuration.html#default-settings 13 | 14 | # Controls the ordering and grouping of the admin menu. 15 | # 16 | # ADMIN_MENU_ORDER = ( 17 | # ("Content", ("pages.Page", "blog.BlogPost", 18 | # "generic.ThreadedComment", ("Media Library", "fb_browse"),)), 19 | # ("Site", ("sites.Site", "redirects.Redirect", "conf.Setting")), 20 | # ("Users", ("auth.User", "auth.Group",)), 21 | # ) 22 | 23 | # A three item sequence, each containing a sequence of template tags 24 | # used to render the admin dashboard. 25 | # 26 | # DASHBOARD_TAGS = ( 27 | # ("blog_tags.quick_blog", "mezzanine_tags.app_list"), 28 | # ("comment_tags.recent_comments",), 29 | # ("mezzanine_tags.recent_actions",), 30 | # ) 31 | 32 | # A sequence of templates used by the ``page_menu`` template tag. Each 33 | # item in the sequence is a three item sequence, containing a unique ID 34 | # for the template, a label for the template, and the template path. 35 | # These templates are then available for selection when editing which 36 | # menus a page should appear in. Note that if a menu template is used 37 | # that doesn't appear in this setting, all pages will appear in it. 38 | 39 | # PAGE_MENU_TEMPLATES = ( 40 | # (1, "Top navigation bar", "pages/menus/dropdown.html"), 41 | # (2, "Left-hand tree", "pages/menus/tree.html"), 42 | # (3, "Footer", "pages/menus/footer.html"), 43 | # ) 44 | 45 | # A sequence of fields that will be injected into Mezzanine's (or any 46 | # library's) models. Each item in the sequence is a four item sequence. 47 | # The first two items are the dotted path to the model and its field 48 | # name to be added, and the dotted path to the field class to use for 49 | # the field. The third and fourth items are a sequence of positional 50 | # args and a dictionary of keyword args, to use when creating the 51 | # field instance. When specifying the field class, the path 52 | # ``django.models.db.`` can be omitted for regular Django model fields. 53 | # 54 | # EXTRA_MODEL_FIELDS = ( 55 | # ( 56 | # # Dotted path to field. 57 | # "mezzanine.blog.models.BlogPost.image", 58 | # # Dotted path to field class. 59 | # "somelib.fields.ImageField", 60 | # # Positional args for field class. 61 | # ("Image",), 62 | # # Keyword args for field class. 63 | # {"blank": True, "upload_to": "blog"}, 64 | # ), 65 | # # Example of adding a field to *all* of Mezzanine's content types: 66 | # ( 67 | # "mezzanine.pages.models.Page.another_field", 68 | # "IntegerField", # 'django.db.models.' is implied if path is omitted. 69 | # ("Another name",), 70 | # {"blank": True, "default": 1}, 71 | # ), 72 | # ) 73 | 74 | # Setting to turn on featured images for blog posts. Defaults to False. 75 | # 76 | # BLOG_USE_FEATURED_IMAGE = True 77 | 78 | # If True, the south application will be automatically added to the 79 | # INSTALLED_APPS setting. 80 | USE_SOUTH = True 81 | 82 | 83 | ######################## 84 | # MAIN DJANGO SETTINGS # 85 | ######################## 86 | 87 | # People who get code error notifications. 88 | # In the format (('Full Name', 'email@example.com'), 89 | # ('Full Name', 'anotheremail@example.com')) 90 | ADMINS = ( 91 | # ('Your Name', 'your_email@domain.com'), 92 | ) 93 | MANAGERS = ADMINS 94 | 95 | # Hosts/domain names that are valid for this site; required if DEBUG is False 96 | # See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts 97 | ALLOWED_HOSTS = [] 98 | 99 | # Local time zone for this installation. Choices can be found here: 100 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 101 | # although not all choices may be available on all operating systems. 102 | # On Unix systems, a value of None will cause Django to use the same 103 | # timezone as the operating system. 104 | # If running in a Windows environment this must be set to the same as your 105 | # system time zone. 106 | TIME_ZONE = None 107 | 108 | # If you set this to True, Django will use timezone-aware datetimes. 109 | USE_TZ = True 110 | 111 | # Language code for this installation. All choices can be found here: 112 | # http://www.i18nguy.com/unicode/language-identifiers.html 113 | LANGUAGE_CODE = "en" 114 | 115 | # Supported languages 116 | _ = lambda s: s 117 | LANGUAGES = ( 118 | ('en', _('English')), 119 | ) 120 | 121 | # A boolean that turns on/off debug mode. When set to ``True``, stack traces 122 | # are displayed for error pages. Should always be set to ``False`` in 123 | # production. Best set to ``True`` in local_settings.py 124 | DEBUG = False 125 | 126 | # Whether a user's session cookie expires when the Web browser is closed. 127 | SESSION_EXPIRE_AT_BROWSER_CLOSE = True 128 | 129 | SITE_ID = 1 130 | 131 | # If you set this to False, Django will make some optimizations so as not 132 | # to load the internationalization machinery. 133 | USE_I18N = False 134 | 135 | # Tuple of IP addresses, as strings, that: 136 | # * See debug comments, when DEBUG is true 137 | # * Receive x-headers 138 | INTERNAL_IPS = ("127.0.0.1",) 139 | 140 | # List of callables that know how to import templates from various sources. 141 | TEMPLATE_LOADERS = ( 142 | "django.template.loaders.filesystem.Loader", 143 | "django.template.loaders.app_directories.Loader", 144 | ) 145 | 146 | AUTHENTICATION_BACKENDS = ("mezzanine.core.auth_backends.MezzanineBackend",) 147 | 148 | # List of finder classes that know how to find static files in 149 | # various locations. 150 | STATICFILES_FINDERS = ( 151 | "django.contrib.staticfiles.finders.FileSystemFinder", 152 | "django.contrib.staticfiles.finders.AppDirectoriesFinder", 153 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 154 | ) 155 | 156 | # The numeric mode to set newly-uploaded files to. The value should be 157 | # a mode you'd pass directly to os.chmod. 158 | FILE_UPLOAD_PERMISSIONS = 0o644 159 | 160 | 161 | ############# 162 | # DATABASES # 163 | ############# 164 | 165 | DATABASES = { 166 | "default": { 167 | # Add "postgresql_psycopg2", "mysql", "sqlite3" or "oracle". 168 | "ENGINE": "django.db.backends.", 169 | # DB name or path to database file if using sqlite3. 170 | "NAME": "", 171 | # Not used with sqlite3. 172 | "USER": "", 173 | # Not used with sqlite3. 174 | "PASSWORD": "", 175 | # Set to empty string for localhost. Not used with sqlite3. 176 | "HOST": "", 177 | # Set to empty string for default. Not used with sqlite3. 178 | "PORT": "", 179 | } 180 | } 181 | 182 | 183 | ######### 184 | # PATHS # 185 | ######### 186 | 187 | import os 188 | 189 | # Full filesystem path to the project. 190 | PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) 191 | 192 | # Name of the directory for the project. 193 | PROJECT_DIRNAME = PROJECT_ROOT.split(os.sep)[-1] 194 | 195 | # Every cache key will get prefixed with this value - here we set it to 196 | # the name of the directory the project is in to try and use something 197 | # project specific. 198 | CACHE_MIDDLEWARE_KEY_PREFIX = PROJECT_DIRNAME 199 | 200 | # URL prefix for static files. 201 | # Example: "http://media.lawrence.com/static/" 202 | STATIC_URL = "/static/" 203 | 204 | # Absolute path to the directory static files should be collected to. 205 | # Don't put anything in this directory yourself; store your static files 206 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 207 | # Example: "/home/media/media.lawrence.com/static/" 208 | STATIC_ROOT = os.path.join(PROJECT_ROOT, STATIC_URL.strip("/")) 209 | 210 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 211 | # trailing slash. 212 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 213 | MEDIA_URL = STATIC_URL + "media/" 214 | 215 | # Absolute filesystem path to the directory that will hold user-uploaded files. 216 | # Example: "/home/media/media.lawrence.com/media/" 217 | MEDIA_ROOT = os.path.join(PROJECT_ROOT, *MEDIA_URL.strip("/").split("/")) 218 | 219 | # Package/module name to import the root urlpatterns from for the project. 220 | ROOT_URLCONF = "%s.urls" % PROJECT_DIRNAME 221 | 222 | # Put strings here, like "/home/html/django_templates" 223 | # or "C:/www/django/templates". 224 | # Always use forward slashes, even on Windows. 225 | # Don't forget to use absolute paths, not relative paths. 226 | TEMPLATE_DIRS = (os.path.join(PROJECT_ROOT, "templates"),) 227 | 228 | 229 | ################ 230 | # APPLICATIONS # 231 | ################ 232 | 233 | INSTALLED_APPS = ( 234 | "django.contrib.admin", 235 | "django.contrib.auth", 236 | "django.contrib.contenttypes", 237 | "django.contrib.redirects", 238 | "django.contrib.sessions", 239 | "django.contrib.sites", 240 | "django.contrib.sitemaps", 241 | "django.contrib.staticfiles", 242 | "mezzanine.boot", 243 | "mezzanine.conf", 244 | "mezzanine.core", 245 | "mezzanine.generic", 246 | "mezzanine.blog", 247 | "mezzanine.forms", 248 | "mezzanine.pages", 249 | "mezzanine.galleries", 250 | "mezzanine.twitter", 251 | #"mezzanine.accounts", 252 | #"mezzanine.mobile", 253 | "mezzanine_agenda", 254 | ) 255 | 256 | # List of processors used by RequestContext to populate the context. 257 | # Each one should be a callable that takes the request object as its 258 | # only parameter and returns a dictionary to add to the context. 259 | TEMPLATE_CONTEXT_PROCESSORS = ( 260 | "django.contrib.auth.context_processors.auth", 261 | "django.contrib.messages.context_processors.messages", 262 | "django.core.context_processors.debug", 263 | "django.core.context_processors.i18n", 264 | "django.core.context_processors.static", 265 | "django.core.context_processors.media", 266 | "django.core.context_processors.request", 267 | "django.core.context_processors.tz", 268 | "mezzanine.conf.context_processors.settings", 269 | ) 270 | 271 | # List of middleware classes to use. Order is important; in the request phase, 272 | # these middleware classes will be applied in the order given, and in the 273 | # response phase the middleware will be applied in reverse order. 274 | MIDDLEWARE_CLASSES = ( 275 | "mezzanine.core.middleware.UpdateCacheMiddleware", 276 | "django.contrib.sessions.middleware.SessionMiddleware", 277 | "django.middleware.locale.LocaleMiddleware", 278 | "django.contrib.auth.middleware.AuthenticationMiddleware", 279 | "django.middleware.common.CommonMiddleware", 280 | "django.middleware.csrf.CsrfViewMiddleware", 281 | "django.contrib.messages.middleware.MessageMiddleware", 282 | "mezzanine.core.request.CurrentRequestMiddleware", 283 | "mezzanine.core.middleware.RedirectFallbackMiddleware", 284 | "mezzanine.core.middleware.TemplateForDeviceMiddleware", 285 | "mezzanine.core.middleware.TemplateForHostMiddleware", 286 | "mezzanine.core.middleware.AdminLoginInterfaceSelectorMiddleware", 287 | "mezzanine.core.middleware.SitePermissionMiddleware", 288 | # Uncomment the following if using any of the SSL settings: 289 | # "mezzanine.core.middleware.SSLRedirectMiddleware", 290 | "mezzanine.pages.middleware.PageMiddleware", 291 | "mezzanine.core.middleware.FetchFromCacheMiddleware", 292 | ) 293 | 294 | # Store these package names here as they may change in the future since 295 | # at the moment we are using custom forks of them. 296 | PACKAGE_NAME_FILEBROWSER = "filebrowser_safe" 297 | PACKAGE_NAME_GRAPPELLI = "grappelli_safe" 298 | 299 | ######################### 300 | # OPTIONAL APPLICATIONS # 301 | ######################### 302 | 303 | # These will be added to ``INSTALLED_APPS``, only if available. 304 | OPTIONAL_APPS = ( 305 | "debug_toolbar", 306 | "django_extensions", 307 | "compressor", 308 | PACKAGE_NAME_FILEBROWSER, 309 | PACKAGE_NAME_GRAPPELLI, 310 | ) 311 | 312 | DEBUG_TOOLBAR_CONFIG = {"INTERCEPT_REDIRECTS": False} 313 | 314 | ################### 315 | # DEPLOY SETTINGS # 316 | ################### 317 | 318 | # These settings are used by the default fabfile.py provided. 319 | # Check fabfile.py for defaults. 320 | 321 | # FABRIC = { 322 | # "SSH_USER": "", # SSH username 323 | # "SSH_PASS": "", # SSH password (consider key-based authentication) 324 | # "SSH_KEY_PATH": "", # Local path to SSH key file, for key-based auth 325 | # "HOSTS": [], # List of hosts to deploy to 326 | # "VIRTUALENV_HOME": "", # Absolute remote path for virtualenvs 327 | # "PROJECT_NAME": "", # Unique identifier for project 328 | # "REQUIREMENTS_PATH": "", # Path to pip requirements, relative to project 329 | # "GUNICORN_PORT": 8000, # Port gunicorn will listen on 330 | # "LOCALE": "en_US.UTF-8", # Should end with ".UTF-8" 331 | # "LIVE_HOSTNAME": "www.example.com", # Host for public site. 332 | # "REPO_URL": "", # Git or Mercurial remote repo URL for the project 333 | # "DB_PASS": "", # Live database password 334 | # "ADMIN_PASS": "", # Live admin user password 335 | # "SECRET_KEY": SECRET_KEY, 336 | # "NEVERCACHE_KEY": NEVERCACHE_KEY, 337 | # } 338 | 339 | 340 | ################## 341 | # LOCAL SETTINGS # 342 | ################## 343 | 344 | # Allow any settings to be defined in local_settings.py which should be 345 | # ignored in your version control system allowing for settings to be 346 | # defined per machine. 347 | try: 348 | from local_settings import * 349 | except ImportError: 350 | pass 351 | 352 | 353 | #################### 354 | # DYNAMIC SETTINGS # 355 | #################### 356 | 357 | # set_dynamic_settings() will rewrite globals based on what has been 358 | # defined so far, in order to provide some better defaults where 359 | # applicable. We also allow this settings module to be imported 360 | # without Mezzanine installed, as the case may be when using the 361 | # fabfile, where setting the dynamic settings below isn't strictly 362 | # required. 363 | try: 364 | from mezzanine.utils.conf import set_dynamic_settings 365 | except ImportError: 366 | pass 367 | else: 368 | set_dynamic_settings(globals()) 369 | --------------------------------------------------------------------------------