Happening¶
Contents:
Introduction¶
Happening is an open source event/community management tool.
Think Eventbrite meets Meetup, running on your own domain with your own branding.
The project is still in heavy development and still has traces of its past as the website of the Southampton Code Dojo. It is not currently recommended for production use.
This documentation is also very incomplete, it will become more complete as the project progresses.
Usage¶
If you’d like to use Happening to host your own event or community, read these topics for instructions and advice.
Configuration¶
Once any wanted Plugins have been enabled. The Configuration
page of the Admin panel allows you to configure general site-wide settings. The default variables which are available for configuration are:
Other configuration options will be added by enabled Plugins.
Authentication¶
Using the Authentication
page of the Admin panel, you can configure third party services to allow for use of these services to log in to your community.
The available third party providers and configuration instructions are listed below.
GitHub
See `GitHub App Registration<https://github.com/settings/applications/new>`_.
See `Twitter App Registration<https://apps.twitter.com/app/new>`_.
See `LinkedIn App Registration<https://www.linkedin.com/secure/developer?newapp=>`_.
See `Google App Registration<https://console.developers.google.com/>`_.
See `Facebook Documentation<https://developers.facebook.com/docs/apps/register>`_.
Stack Exchange
See ``Stack Exchange App Registration<http://stackapps.com/apps/oauth/register>`_.
Members¶
Visitors will be required to register with the website (become a “member”) in order to participate in discussions, receive notifications, and register for events. They can do this using an email address and password, or by using one of the support social accounts.
Once they have registered, they will appear in the Members
page of the Staff panel.
Member Profiles
Member profile will consist of their name, a biography, and any number of custom profile fields as configured in Configuration.
From the Members
page of the Staff panel, you may click on a member’s name to view their profile, or click on Edit Profile
alongside their name to edit their profile. You may also click on True/False below Staff
to toggle their staff status (click it when it is True will remove their staff status, clicking when it is False will make them staff).
Member Settings¶
When signed in, by clicking on their username in the top right corner, and then clicking on Settings
, a member may modify their settings.
Username
A member’s username defines how other users will see them on their profile, on event pages, and during discussions.
Email Addresses
A member may have multiple email addresses attached to an account. The primary email address will be the one which recieves emails (for example notification emails) but all email addresses can be used to sign in to the account.
Social Accounts
A member may have multiple social accounts attached to an account. These accounts are used to sign in to the community, and if configured in the member’s profile (see Members), will be linked to from their profile page.
Social Accounts can be added for any of the supported external sites. To configure the allowed external sites see Configuration.
Password
Member’s may change their password by clicking Edit
alongside Password
.
Notifications
Member’s may click Notification Settings
to modify their notification settings. For details see Notifications.
Notifications¶
Notifications are used to inform members when an action occurs which they may wish to be informed about. Some notifications are built into the core of Happening and others are added by plugins.
Notifications can appear in a member’s notification feed, be sent to them by email, both, or neither. This is configurable on a notification-by-notification and member-by-member basis.
Viewing Notifications
If there are unread notifications, a number will appear in the top bar indicating the number of unread notifications. Clicking on this number will show the latest five notifications, with unread notifications highlighted. All notifications will then be marked as read.
To see all notifications, member’s should click on See More
at the bottom of this truncated notification feed.
Notification Settings
To access notification settings, members should click Notification Settings
from the Member Settings page. On this page, each notification is listed with a title and a description of what the notification does.
Most notifications allow you to configure whether it appears in your notification feed and whether it is sent by email. This can be changed by ticking the appriate checkboxes and clicking Save
.
Some notifications only allow you to configure either the notification feed setting or the email setting.
Events¶
Creating Events
To create an event, go to the Events
page of the Staff panel and click New Event
. Events must have a title, a date and time, and an initial number of tickets. Other properties are optional. Once the event has been configured press Save
.
Presets
If you are regularly creating events with similar properties, it might be worth creating a preset which allows you to load these properties in the future.
To create a preset, go to the Events
page of the Staff panel and click on Presets
. Then click New Preset
. You will be presented with the same form used for creating an Event, with an additional Preset Name
field. Give the preset a memorable name and enter the common properties. No properties are required when creating a preset. Once you are ready, press Save
to create the preset. To edit or delete the preset use the buttons alongside the preset name on the Presets page.
After a preset has been created, when creating a new Event, a button Load Preset
will appear at the top of the page. Click this and choose a preset to load the settings into the form.
Editing Events
To edit an event, go to the Events
page of the Staff panel and click on the event title. Then click on Edit
at the top of the page. Once you have changed the appropriate properties, click Save
.
Viewing Events
The most recent five events (including future events) are listed under the Events
menu in the top navigation bar. Clicking on the event name will show the event page. Older events can be accessed by clicking on View All
in this menu, and the next event is always shown on the index page.
The view event page shows a list of members who are attending the event, allows members to purchase Tickets, and provides space for Plugins to provide functionality.
Tickets¶
Purchasing Tickets
To purchase a ticket, go to the page of the Event (see Events), select the number of tickets to purchase and click Purchase
.
Viewing Purchased Tickets
To view a list of purchased tickets, sign in and then click on your username on the top menu, then click on My Tickets
.
Cancelling Tickets
To cancel a ticket, view the list of purchased tickets (as above) and click Cancel
next to the ticket you wish to cancel.
Checking In Tickets
To indicate that a ticket has been used and the member did attend the event, Happening supports the concept of “checking in” a ticket. To do this, visit the Event’s page on the staff panel (see Events), and click Manage Check Ins
.
This will present a list of purchased tickets with a search bar for filtering the tickets, and a Check In
button for each ticket.
Pages¶
Pages allow for the presentation of static content on your Happening website. This is particularly useful for About pages, Contact pages, and so on.
Creating A Page
To create a page, click the Pages
button in the Staff panel, and then click New Page
.
The properties to be provided are:
- URL - Which is the unique relative URL of this page, the full URL will be http://yourwebsite.com/pages/URL - so if URL was “about” the full URL would be http://youwebsite.com/pages/about
- Title - This is used in the title bar of the website
- Path - Explained below
- Content - Markdown representing the content of the page.
Paths
Paths define where the page appears on the navigation bar. Levels are separated by forward slashes, and each level is a sub-menu. The final level of the path will be the link which leads to the page.
For example, the path About/Sponsorship will create an “About” menu at the first level, with a “Sponsorship” link showing when the “About” menu is opened. Adding another page with a path of About/Contact/Leadership will create a Contact sub-menu below the About menu, which contains within it a Leadership link.
Editing A Page
To edit a page, visit the Pages
page of the Staff panel and click Edit
alongside the page to be edited.
Deleting A Page
To delete a page, visit the Pages
page of the Staff panel and click Delete
alongside the page to be deleted.
Emails¶
Sending Emails
To send an email to all members, go to the Send Email
page of the Staff panel. Emails have a query to specify who should recieve the email (see User Filtering), and a start and end time. Any members who match the query between the start and end time will recieve the email. Each member will only recieve each email a maximum of 1 time.
This is different to how most email systems work - where they are sent once when the email is written. In an event management system this is most useful for emails with event information, where latecomers would otherwise not get the email.
The to
query is written using the Happening Query Language. More information is available at User Filtering.
Sending Emails Related to an Event
To send an email to all attendees of a given Event, visit the Event’s page on the Staff panel (see Events) and click Send Email
. This form is identical to the general Send Email
form, but the content
fields will have an event
variable in context, which can be used e.g. Our event {{event.title}} will be....
.
Automatic Emails
Events can configure emails which will be sent automatically. These are configured when creating the Event (or Preset) and will have their start sending/stop sending set relative to the event. This are typically used for reminder and information emails.
In the to
field, you may use {{event.id}}
to represent the event ID and it will be automatically converted into the actual event ID.
(e.g. to send an email to all attendees of the event who have not cancelled their tickets, the query should be tickets__has:(event__id:{{event.id}} cancelled:False)
). For more information about filtering see User Filtering.
User Filtering¶
The Happening Filtering Language will be used throughout happening to allow staff members to target members. Currently it is only used as a target for Emails, but in the future it will be used to specify ticket eligibility, voting eligibility, group membership, etc.
The Happening Filter Language maps directly to Django queries. Differences are that keys and values are separated by :
instead of =
, count
being annotated automatically, and the addition of the has
keyword. To access attributes of related models you still use __.
Count
To filter users who have at least one ticket, use tickets__count__gt:0
. There is currently no way of counting the result of a previous query.
Has
Has is used to confirm that a relationship exists matching a subquery. For example tickets__has:(event__id:1 cancelled:True)
will find every User who has a cancelled ticket for event 1.
Select All
To target every member of the system, a blank query will suffice.
Tickets to a particular event
As shown above, tickets__has:(event__id:1 cancelled:False)
will find all members who have active tickets to a Event 1.
A specific Member
Members can be targetted using simple key querying. E.g. id:1
Payment¶
Some functionality in Happening will require/allow payment from members. Currently this is only the Membership plugin but in future will include ticket purchases, etc.
To configure the payment settings go to Payment
on the Admin
Panel. Currently we only support Stripe payments but will support other providers in future.
Plugins¶
Happening is built on a sophisticated plugin architecture which allows you to enable and disable features of the software as needed. Happening ships with several useful plugins built in, and if functionality you need isn’t provided it’s easy to add your own.
To enable and disable plugins, log in as an administrator and click Admin
on the menu, followed by Plugins
on the sidebar. This will present you with a list of plugins which can be enabled and disabled by toggling the checkbox and pressing save
.
Groups¶
The groups plugin allows for events where attendees are separated into groups. These groups can be assigned by organisers, self-assigned by attendees, or generated randomly. If the groups are generated randomly they can take into account attributes of the attendees to balance groups (for example, to ensure that groups are mixed-skillset or mixed-ability).
Group Permissions
When creating an event, you are asked to assign the permissions of attendees to create, move, and modify groups.
Creation refers to creating and joining a new group, moving involves joining or leaving existing groups, and modifying groups allows attendees to edit the named and properties of the group they are in.
The options for permission levels are:
- Attendees cannot create/move/edit groups
- Attendees can create/move/edit groups after the event starts
- Attendees can create/move/edit groups at any time
- Attendees can create/move/edit groups after they are checked in
These rules do not apply to staff members who are always able to create, move, and edit groups.
Group Properties
When creating an event, you are able to specify a number of properties which attendees will be attached to groups. These properties appear one the create group form and the edit group form and can be set by staff, or by attendees who are members of the group.
The properties can be:
- Text, which is a single line text input
- Email, which is a valid email address
- Number, which is an integer
- URL, which is a valid URL
- Boolean, which is a checkbox indicating True or False
Creating Groups
Groups can be created by viewing the event and clicking Add a group
. You will be asked for the Team name
, Description
, and any custom group properties configured for the event. After clicking Save
the group will be created and you will join it by default.
Joining Groups
If you are not a member of a group, and have permission to move groups, visiting the event page will show join group
alongside each group. Clicking this will add you to the group.
Leaving Groups
If you are a member of a group, and have permission to move groups, visiting the event page will show leave group
alongside the group you are part of. Clicking this will remove you from the group.
Staff Moving Groups
Staff can move attendees around groups by visiting the event page in the Staff panel, and clicking the edit icon (a pencil) alongside an attendee’s name.
Generating Groups
To generate a group, visit the event page in the Staff panel and click Generate Groups
. You then decide if you wish to clear existing groups, if only checked-in attendees should be grouped, and how many groups you want to create. Attendees will be split evenly between the groups.
Viewing Groups
Clicking View Groups
on the event page in the Staff panel will show all groups with avatars and names for members. This can be used when showing attendees which groups they are in.
Event Configuration
Sponsorship¶
The sponsorship allows for displaying of organisations or individuals who support your event/community. It supports two types of sponsor: event sponsors and tiered sponsors, you may use either or both in your commuity.
Creating/Modifying Sponsors
To create a sponsor, visit the Sponsors page of the Staff panel. You will be asked to provide:
- Sponsor’s name
- URL
- Description, which allows markdown formatting
- Logo
To edit an already created sponsor, visit the Sponsors page of the Staff panel and click on the sponsor’s name, then click Edit Sponsor
.
Event Sponsorship
Event sponsorship is where a sponsor contributes to the running of a single event, but may not be otherwise associated with the wider community. The event sponsor is shown on the event page, the index page when the event is advertised, and in all emails relating to the events they sponsor.
To add an event sponsor, visit the Events page of the Staff panel, then click on the name of the event you wish to add a sponsor for. Then click Add Sponsor
.
Tiered Sponsorship
Tiered sponsorship is used to associated a sponsor with an entire community rather than a single event. In it, sponsors are assigned to “tiers” (typically Gold, Silver, Bronze) and then are shown in the footer of the website in order of tier.
To use tiered sponsorship, you must first set up tiers by going to the Sponsorship Tiers
page on the Admin panel and clicking New Sponsorship Tier
.
Then, visit a sponsor’s page on the staff panel, and under Tiers
click Add Tier
. Select the tier their sponsorship falls under and click Save
.
Comments¶
The comments plugin adds a “discussion” section on all event pages. Signed in members are able to comment on the event.
Membership¶
The membership plugin currently only allows for pay-what-you-want for one year membership.
This will add a “+” to the profile picture of any members, and a “Paid Member” tag on their profile.
This functionality will be expanded to allow for different membership options soon.
Development¶
To contribute to development of Happening, or to develop a plugin for use with Happening, read the following pages.
Getting Started¶
Requirements
The following must be available and configured:
- python
- virtualenv
Development Requirements
- jshint (Available via:
npm install -g jshint
)
Getting Started
Clone the repository to your disk, and then run setup
- this will download all requirements and set up the database.
Standards¶
For Python, follow PEP8 and PEP257. With SCSS we follow a loose form of CSSGuidelin.es and OOCSS with BEM naming convention. Existing styles can be viewed in styleguide.html.
Check that the code passes using check-standards
.
check-standards
also looks for common mistakes such as unused or double imports - these issues should be fixed before they are committed. If any method has a higher cyclomatic complexity than 10 check-standards will flag it and it should be changed (split up into multiple methods).
Requirements files should be separated into logical groups, with each individual requirement commented. All requirements should specify a version.
All functionality implemented should have tests, and all code should follow the coding conventions mentioned above.
Code coverage should not fall below 90%.
Flash Messages¶
Flash messages are used to indicate the success/failure of an action (for example, posting a comment, purchasing a ticket, etc.). To manage flash messages we use the django messages framework:
from django.contrib import messages
messages.debug(request, '%s SQL statements were executed.' % count)
messages.info(request, 'Three credits remain in your account.')
messages.success(request, 'Profile details updated.')
messages.warning(request, 'Your account expires in three days.')
messages.error(request, 'Document deleted.')
Blocks¶
Blocks provide spaces where plugins can add additional content to a page. Happening provides a number of blocks which can be exploited by plugins and several plugins offer blocks of their own which can be exploited by other plugins.
Creating A Block
To create a block, import the plugins
library in a template and then use the plugin_block
templatetag, providing the name of the block and any parameters to be passed to the block.
For example:
{% load plugins %}
{% plugin_block "events.event_long" event %}
This creates a block named “events.event_long” which passes an event as a parameter.
Using A Block
To use a block, create a file named blocks.py
inside any app/plugin. Inside this file, import the plugin_block
decorator from happening.plugins
and use it as so:
from happening.plugins import plugin_block
from django.template.loader import render_to_string
from django.template import RequestContext
@plugin_block("events.event_long")
def event_long(request, event):
"""Add groups to long event information."""
return render_to_string("groups/blocks/events/event_long.html",
{"event": event},
context_instance=RequestContext(request))
This will add the rendered template to the events.event_long
block. Using render_to_string to use templates is a useful technique with blocks but is not required.
Built-in Blocks
-
events.
event_long
(request, event)¶ Shown on the events page.
Parameters: - request – Django request
- event – Event
-
events.
event_short
(request, event)¶ Shown on the index page next to a future event.
Parameters: - request – Django request
- event – Event
Shown at the top of the staff event page, alongside “Send Email”, “Manage Check Ins”, etc.
Parameters: - request – Django request
- event – Event
-
staff.event.tickets.
headers
(request, event, ticket)¶ Shown in the <thead><tr> of the ticket list on the staff event page
Parameters: - request – Django request
- event – Event
- ticket – Ticket
-
staff.event.tickets.
info
(request, event, ticket)¶ Shown in the <tbody><tr> of the ticket list on the staff event page
Parameters: - request – Django request
- event – Event
- ticket – Ticket
-
staff.event.tickets.
options
(request, event, ticket)¶ Shown in a button group to the right of each ticket on the staff event page. To fit the style this should return a <li> containing an <a> with a class of “button”
Parameters: - request – Django request
- event – Event
- ticket – Ticket
-
staff.
event
(request, event)¶ Shown at the bottom of the staff event page
Parameters: - request – Django request
- event – Event
Shown at the footer of every page
Parameters: request – Django request
Actions¶
Actions allow plugins to respond to actions occuring within Happening. They are similar to Django signals.
Triggering Actions
To trigger an action (creating a point for plugins to respond), import happening.plugins.trigger_action
and call it, passing the name of the action first, and keyword arguments which will be passed along to responders.
For example:
from happening.plugins import trigger_action
trigger_action("events.ticket_cancelled", ticket=self)
Responding To Actions
To respond to an action, create a file named actions.py
in any app/plugin. In it, import the happening.plugins.action
decorator and apply it like so:
from happening.plugins import action
@action("events.ticket_cancelled")
def ticket_cancelled(ticket):
"""If a ticket is cancelled, ensure that it is not in any groups."""
for g in ticket.groups.all():
g.delete()
In this case, we will respond to the ticket cancelled action by deleting any groups attached to the ticket.
Built-in Actions
-
events.
ticket_cancelled
(ticket)¶ A ticket has been cancelled
Parameters: ticket – Ticket
Notifications¶
Notifications are used for communicating events to users. To create a new notification type create a file named notifications.py
in any app, and add a new subclass of happening.notifications.Notification
. You must also create a template in templates/notifications/
which will provide the layout for the notifications pages. Check existing notifications for examples.
When the data passed to notifications is serialized a shallow copy will be made. This means that in your notification templates, functions and properties will not be available, and references to other models will be flattened into an ID. If, for example, you require the user and user.profile, BOTH of these must be required by your notification.
To send a notification create an instance of your class (ensuring you provide the required parameters and no unexpected parameters) and then call .send()
:
n = CancelledTicketNotification(
self.user,
ticket=self,
event=self.event,
event_name=str(self.event))
n.send()
Following¶
“Following” is used to allow users to optionally recieve updates about a particular thing. Currently this is only used for Comments, but it is abstract and can be used with anything.
A “role” allows for different users to follow different aspects of the same object. For example, many users may want to follow a discussion on an event, but relatively few users want to be alerted when new groups are created.
Automatically Following
To make a user follow a given object/role, use the follow
method on the User instance:
user.follow(event, "discuss")
Manual Following
Automatic following will not force the user to follow an object/role pair if they have previously chosen to unfollow. If you wish to force this follow (e.g. if the user has explicitly said they wish to follow it) then pass force=True:
user.follow(event, "discuss", force=True)
alternatively, a view has been set up which can be pushed to and will deal with these manual follows. To use this first call follow_object_code
on the User instance to generate a signed code (this ensures that users can only set up follows which are authorised):
code = user.follow_object_code(event, "discuss")
Then allow the user to POST this code to the “follow” view (as the “object” parameter). You should also pass a “next” GET parameter which tells the view where to redirect to, and the “message” parameter which will be sent as success Flash Messages..:
<form action="{% url "follow" %}?next={{request.path}}" method="POST">
<input type="hidden" name="object" value="{{follow_code}}">
<input type="hidden" name="message" value="You are now following {{event.name}} discussion">
<button type="submit">Follow</button>
</form>
Unfollowing
To unfollow an object, use the unfollow
method of the User instance:
user.unfollow(event, "discuss",)
alternatively, a view has been set up which can be pushed to and will deal with these unfollows. To use this first call follow_object_code
on the User instance to generate a signed code (this ensures that users can only set up follows which are authorised):
code = user.follow_object_code(event, "discuss")
Then allow the user to POST this code to the “unfollow” view (as the “object” parameter). You should also pass a “next” GET parameter which tells the view where to redirect to, and the “message” parameter which will be sent as success Flash Messages..:
<form action="{% url "unfollow" %}?next={{request.path}}" method="POST">
<input type="hidden" name="object" value="{{follow_code}}">
<input type="hidden" name="message" value="You are no longer following {{event.name}} discussion">
<button type="submit">Unfollow</button>
</form>
Sending notifications
To send notifications to followers use the happening.notifications.notify_following
method:
notify_following(
event, "discuss", CommentNotification,
{"comment": comment,
"author_photo_url": comment.author.profile.photo_url(),
"author_name": str(comment.author),
"object_name": str(event),
"object_url": request.POST['next']},
ignore=[request.user])
The ignore
parameter indicates a list of users who should not recieve the notification, even if they are following. In this example it includes the user who is making the comment.
Filtering¶
Filtering is used frequently in Happening to allow viewing subsets of large amounts of data. It involves the creation of a filter-form
and associating it with either a searchable-list
or a Data Tables:
<form class="filter-form input-grid" data-filter="#members-table">
<div class="input-grid__container">
<label class="input-grid__container__item" for="search">Search</label>
<input class="filter-form__search-filter" name="search" type="search">
</div>
<span class="input-grid__header">Status</span>
<div class="input-grid__container">
<label class="input-grid__container__item radio"><input name="member-status" type="checkbox" class="checkbox filter-form__option-filter" value="non-member">Non-member</label>
<label class="input-grid__container__item radio"><input name="member-status" type="checkbox" class="checkbox filter-form__option-filter" value="member">Member</label>
<label class="input-grid__container__item radio"><input name="member-status" type="checkbox" class="checkbox filter-form__option-filter" value="staff">Staff</label>
</div>
</form>
In this example, the filter-form applies to a datatable identified by the id members-table
. This could equally apply to a searchable-list
as the API is identical. It has a search box used to search every field (using the class filter-form__search-filter
), and some checkboxes for filtering based on the member-status
attribute (which would allow for filtering non-members, members, and staff members).
The available filter types are listed below.
search-filter
This allows for a text based search either on the whole table.
option-filter
This performs a search which checks that the specified column (as decided by the “name” field) matches one of the selected values. If no values are selected, no filtering is performed. To match against multiple values, separate valid values by a |. e.g:
<label class="input-grid__container__item radio"><input name="age" type="checkbox" class="checkbox filter-form__option-filter" value="20|21|22|23|24|25|26|27|28|29">20-29</label>
Searchable Lists
Searchable Lists are collections of items which can be search/filtered by a filter-form. As an example:
<div class="searchable-list" id="events-list">
{% for event in all_events %}
<div class="block block-list__item event-block searchable-list__item" data-searchable-title="{{event.title}}" data-searchable-description="{{event.description}}">
<!-- ... -->
</div>
{% endfor %}
</div>
The container must have the class searchable-list
and each item must have the class searchable-list__item
. In this case, we are adding a “title”, and “description” to the item, which can be filtered as mentioned above. All data added using these data-searchable-* attributes will be searched using the overal search functionality.
Data Tables¶
Data Tables are used through Happening to provide sorting, filtering, and searching on data. This uses the open source DataTables jQuery plugin.
To apply it, add the “data-tables” class to a table. This will add pagination and sorting:
<table class="data-table" id="members-table">
<thead>
<tr>
<th>Username</th>
</tr>
</thead>
<tbody>
<tr><td><a href="/member/2">Member 2</a></td></tr>
<tr><td><a href="/member/3">Member 3</a></td></tr>
</tbody>
</table>
Filtering
The filter form should be built in the same way as detailed in Filtering. However, instead of providing data using data-
attributes, the queryable data will be provided in table columns:
<table class="data-table" id="members-table">
<thead>
<tr>
<th>Username</th>
<th data-name="age">20</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/member/2">Member 2</a>
<td>20</td>
</td>
</tr>
<!-- ... -->
</tbody>
</table>
In this case it would add a “name” attribute which can be filtered/searched. By adding the appropriate filter type this column could be filtered:
<div class="input-grid__container">
<label class="input-grid__container__item radio"><input name="age" type="checkbox" class="checkbox filter-form__option-filter" value="20">20</label>
<label class="input-grid__container__item radio"><input name="age" type="checkbox" class="checkbox filter-form__option-filter" value="21">21</label>
<label class="input-grid__container__item radio"><input name="age" type="checkbox" class="checkbox filter-form__option-filter" value="22">22</label>
</div>
If you’d rather this field still be searchable but not be visible, you can add data-visible="0"
to the column, e.g:
<table class="data-table" id="members-table">
<thead>
<tr>
<th>Username</th>
<th data-name="age" data-visible="0">20</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="/member/2">Member 2</a>
<td>20</td>
</td>
</tr>
<!-- ... -->
</tbody>
</table>
Creating Plugins¶
To create a plugin, create a directory in the plugins
directory and inside it create a __init__.py
file. Inside this file create a class Plugin
:
class Plugin(object):
"""Plugin which adds comments."""
name = "Comments"
At a minimum, you must give a name attribute which will show in the “Plugins” page of the Admin panel. If you provide a docstring that will also be shown.
URLS
If you want to have views, add a url_root
attribute to your Plugin
class:
url_root = "comments/"
In this case, it will prefix all URLs for this plugin with “comments/”. Then create a urls.py as you would normally in a django project:
from django.conf.urls import patterns, include
urlpatterns = patterns('plugins.comments.views',
(r'^comments/posted/$', 'comment_posted'),
(r'^comments/', include('django_comments.urls')),
)
Staff
If you want to add views accessible only to staff, add a staff_url_root
attribute to the Plugin
class:
staff_url_root = "groups/"
and then create a staff.py
which contains the django urls:
from django.conf.urls import patterns, url
urlpatterns = patterns('plugins.groups.views',
url(r'^groups$',
'groups',
name='groups'),
)
These urls will only be accessible by staff members. If you want to add a link to the Staff panel navigation, inside staff.py
add a staff_links
variable:
staff_links = (
("Groups", "groups"),
)
The first part of the tuple is the text, the second part is the url name to link to.
Admin
If you want to add views accessible only to admins, add an admin_url_root
attribute to the Plugin
class:
admin_url_root = "groups/"
and then create a admin.py
which contains the django urls:
from django.conf.urls import patterns, url
urlpatterns = patterns('plugins.groups.views',
url(r'^group_admin$',
'group_admin',
name='group_admin'),
)
These urls will only be accessible by admins. If you want to add a link to the Admin panel navigation, inside admin.py
add an admin_links
variable:
admin_links = (
("Groups", "group_admin"),
)
The first part of the tuple is the text, the second part is the url name to link to.
Configuration Variables¶
Configuration Variables are used to allow plugins to add properties to existing models. In the core of Happening they are used to add general Configuration, and Event Configuration. Plugins can add their own configuration variables.
Adding Configuration Variables
To add a configuration variable, you must create a file in your app/plugin which is specified by the model you’re attempting to add a property to. In the case of Configuration the filename is configuration.py
, in the case of Event Configuration it is event_configuration.py
, for Plugins see the appropriate documentation.
Inside this file, create classes which subclass happening.configuration.ConfigurationVariable
. There are a number of existing subclasses to provide useful data types, which are listed below.
Accessing Configuration Variables
In Python code, to access a configuration variable, simply instantiate the variable’s class, passing the object the variable should be bound to, and then call .get():
from plugins.groups.event_configuration import GroupCreation
event = # ...
group_creation_config = GroupCreation(event).get()
In a template, use the get_configuration filter in the plugins library to read configuration variables:
{% load plugins %}
{{"pages.configuration.NameOfEvents"|get_configuration}}
{{"groups.configuration.GroupCreation"|get_configuration:event}}
Allowing Other Plugins to Add Configuration Variables
If you wish to use configuration variables on your models, you will use the get_configuration_variables
, attach_to_form
, and save_variables
methods.
An example usage is shown below:
def edit_group(request, pk, group_number):
"""Edit a group."""
event = # ...
group = # ...
variables = get_configuration_variables("group_form", group, event=event)
form = GroupForm(instance=group)
attach_to_form(form, variables)
if request.method == "POST":
form = GroupForm(request.POST, instance=group)
attach_to_form(form, variables)
if form.is_valid():
form.save()
save_variables(form, variables)
return redirect("view_event", event.pk)
return render(request, "groups/edit_group.html", {"form": form})
In this case, we are editing a group - and allowing Configuration Variables to be added to the group. We first get the variables using get_configuration_variables
, passing the filename which will contain the variables (“group_form.py”), the object that the variables will attach to (group) and some extra context which can optionally be used by the Configuration Variables (the event the group is for). After creating the form (or re-creating the form to add POST data) we must use attach_to_form
to add the appropriate fields to the form. Once the form is validated we use save_variables
to commit the variables to the database.
The process is very similar when we’re creating a new object instead of modifying an existing one:
def add_group(request, pk):
"""Add a group."""
event = # ..
form = GroupForm()
variables = get_configuration_variables("group_form", event=event)
attach_to_form(form, variables)
if request.method == "POST":
form = GroupForm(request.POST)
attach_to_form(form, variables)
if form.is_valid():
group = form.save()
variables = get_configuration_variables("group_form", group, event=event)
save_variables(form, variables)
return redirect("view_event", event.pk)
return render(request, "groups/add_group.html", {"form": form, "event": event})
In here, we initially get the variables without specifying which group they will be attached to, then after we have validated the form and created the group, we get new variables which are bound to the group object, before saving them using save_variables
.
Configuration Variable Types
Custom Properties
Custom Properties are a set of two Configuration Variable types CustomProperties
and PropertiesField
which work together to allow users to configure their own properties attached to models.
To demonstrate how these work, we’ll take the example of adding custom properties to groups on an event-by-event basis. First, we add a CustomProperties
to event_configuration.py
:
from happening import configuration
class GroupProperties(configuration.PropertiesField):
"""What properties should be provided for groups."""
Then, we add these properties (via a PropertiesField
) to the groups. To do this, we create a group_form.py
:
from happening import configuration
class CustomProperties(configuration.CustomProperties):
"""The custom properties added on event creation."""
configuration_variable = "plugins.groups.event_configuration.GroupProperties"
configuration_variable_instance = "event"
Here, the properties come from the configuration variable “plugins.groups.event_configuration.GroupProperties”, and are bound to the “event” object (which will be passed in when calling get_configuration_variables
).
This is all that is needed to allow users to add their own properties to models.
Custom Variable Types
Creating a custom variable type typically requires the creation of three classes. To demonstrate we’ll create a “MarkdownField”, which is simply a TextArea with a particular class added (that Javascript can hook onto).
First, we must create a subclass of django.forms.Widget
, and implement the render
method:
class MarkdownWidget(forms.Textarea):
"""A widget for editing markdown."""
def render(self, name, value, attrs):
"""Render the widget."""
attrs['class'] = 'markdown-widget ' + attrs.get('class', '')
return super(MarkdownWidget, self).render(name, value, attrs)
In this case we have overridded forms.Textarea which is a subclass of forms.Widget and takes care of rendering a TextArea. We could have used render_to_string here to render completely custom html.
We then create a subclass of django.forms.Field
, which references the widget to be rendered. It’s often best here to subclass an existing Field
subclass (see ``Django Form Fields Documentation <https://docs.djangoproject.com/en/1.8/ref/forms/fields/>``_, of which there are many:
class MarkdownField(forms.CharField):
"""A field for editing markdown."""
widget = MarkdownWidget
In this case we subclass CharField as the widget will return a text value.
Finally, you need to create a subclass of happening.configuration.ConfigurationVariable
. At a minimum this should point to the Field just created:
class MarkdownField(ConfigurationVariable):
field = forms.MarkdownField
This could also be achieved without the ConfigurationVariable
subclass. However, in this case you would be required to reference the Field
subclass each time you create a new configuration variable of this type. For example:
class Description(configuration.CharField):
"""Event Description."""
field = forms.MarkdownField
In this case, we simply override the field for the Description variable.
Custom “Custom Properties” Types
Currently Custom Properties can only have variables of type CharField, EmailField, IntegerField, URLField, and BooleanField. We hope to allow for custom types in future.
Configuration¶
Configuration is used to allow site-by-site Configuration Variables. To create a configuration variable create a file named configuration.py
in any app. In this file, add a subclass of happening.configuration.ConfigurationVariable
representing the variable you are adding.
For example:
class NameOfEvents(configuration.CharField):
"""The term used to refer to an event, e.g. "match", "rally"."""
default = "event"
This creates a “name of events” variable which is a string (CharField), and defaults to “event”
You can set this variable to be required, by setting required = True.
To access the content of the variable, create an instance of the class and call .get():
event_name = NameOfEvents().get()
In a template, use the get_configuration filter in the plugins library to read configuration variables:
{% load plugins %}
{{"pages.configuration.NameOfEvents"|get_configuration}}
Event Configuration¶
Event Configuration is used to allow event-by-event Configuration Variables. To create a configuration variable create a file named event_configuration.py
in any app. In this file, add a subclass of happening.configuration.ConfigurationVariable
representing the variable you are adding.
For example:
class GroupCreation(configuration.ChoiceField):
"""Who is able to create groups."""
default = 0
choices = [
(0, "Members cannot create groups"),
(1, "Members can create groups after the event starts"),
(2, "Members can create groups at any time"),
]
This creates a “group creation” variable which is one of three options, and defaults to 0
To access the content of the variable, create an instance of the class and call .get()
:
can_create_groups = GroupCreation(event).get()
In a template, use the get_configuration filter in the plugins library to read configuration variables:
{% load plugins %}
{{"groups.configuration.GroupCreation"|get_configuration:event}}
Payment¶
Some functionality in Happening will require/allow payment from members. Currently this is only the Membership plugin but in future will include ticket purchases, etc.
The payment details is abstracted away so that Happening functionality which requires payment can make a request and be informed that payment has been received. There is no need for more involved interaction with payments.
To take a payment, first create a Payment object, and redirect to make_payment:
from payments.models import Payment
payment = Payment(
user=request.user,
description="Membership",
amount=1000,
extra={"member": member.pk},
success_url_name="membership_payment_success",
failure_url_name="membership_payment_failure"
)
payment.save()
return redirect("make_payment", payment.pk)
The description will be shown on the member’s bank statement. The amount is in pennies. Extra can take any information you want to refer back to later. It will be shown in the payment log and will be available in your success/failure callbacks.
The success_url_name and failure_url_name configure the view which will be redirected to once payment is complete. These should be decorated with the payment_successful
and payment_failed
decorators.
An example of callbacks are:
@login_required
@payment_successful
def membership_payment_success(request, payment):
"""Membership payment successful."""
member = get_object_or_404(get_user_model(), pk=payment.extra["member"])
# Do something with the member since they've paid
messages.success(request, "Your payment has been made " +
"successfully. Thank you very much!")
n = MembershipPaymentSuccessfulNotification(
request.user, amount=payment.amount / 100)
n.send()
return redirect("membership", member.pk)
@login_required
@payment_failed
def membership_payment_failure(request, payment):
"""Membership payment failed."""
messages.error(request, payment.error)
return redirect("membership", payment.extra["member"])
Plugins¶
Groups¶
Group Properties
To add properties to groups, create a file named group_form.py
in any app/plugin, and inside it put any needed :ref:configuration_variables
. These will be shown on the create/edit group pages and on the view group page.
Sponsorship¶
Sponsorship currently provides no blocks, actions, or other integration with other plugins.