Basic documentation for confreg system
authorMagnus Hagander <magnus@hagander.net>
Mon, 9 Jul 2018 08:37:56 +0000 (10:37 +0200)
committerMagnus Hagander <magnus@hagander.net>
Mon, 9 Jul 2018 08:37:56 +0000 (10:37 +0200)
This adds basic contextual aware documentation to the confreg system
(sponsorship system not yet included, but will be at some point in the
future). A help link is added to the top right part of the navigation
bar, which links to (potentially context aware) help pages.

Docs live in markdown format in the repo and are rendered as django
views, inside the framework. Graphs that are used are generated using
graphviz, but checked into the repo both as .dot and as .svg to not make
graphviz a dependency for anything other than actually changing the
graphs. The docs view in django basically does the markdown rendering
straight up but also knows how to inline the svg graphs specifically.

38 files changed:
docs/confreg/badges.md [new file with mode: 0644]
docs/confreg/callforpapers.md [new file with mode: 0644]
docs/confreg/configuring.md [new file with mode: 0644]
docs/confreg/copyfromother.md [new file with mode: 0644]
docs/confreg/emails.md [new file with mode: 0644]
docs/confreg/feedback.md [new file with mode: 0644]
docs/confreg/graphs/Makefile
docs/confreg/graphs/regular_reg.dot [new file with mode: 0644]
docs/confreg/graphs/regular_reg.svg [new file with mode: 0644]
docs/confreg/graphs/waitlist.dot [new file with mode: 0644]
docs/confreg/graphs/waitlist.svg [new file with mode: 0644]
docs/confreg/index.md [new file with mode: 0644]
docs/confreg/personaldata.md [new file with mode: 0644]
docs/confreg/registrations.md [new file with mode: 0644]
docs/confreg/reports.md [new file with mode: 0644]
docs/confreg/schedule.md [new file with mode: 0644]
docs/confreg/series.md [new file with mode: 0644]
docs/confreg/signups.md [new file with mode: 0644]
docs/confreg/stepbystep.md [new file with mode: 0644]
docs/confreg/super_conference.md [new file with mode: 0644]
docs/confreg/tokens.md [new file with mode: 0644]
docs/confreg/volunteers.md [new file with mode: 0644]
docs/confreg/vouchers.md [new file with mode: 0644]
docs/confreg/waitlist.md [new file with mode: 0644]
docs/confreg/wiki.md [new file with mode: 0644]
media/css/confadmin.css
postgresqleu/confreg/backendforms.py
postgresqleu/confreg/backendviews.py
postgresqleu/confreg/docsviews.py [new file with mode: 0644]
postgresqleu/confreg/feedback.py
postgresqleu/confreg/pdfschedule.py
postgresqleu/confreg/reporting.py
postgresqleu/confreg/templatetags/docslink.py [new file with mode: 0644]
postgresqleu/confreg/views.py
postgresqleu/confwiki/views.py
postgresqleu/urls.py
template/confreg/admin_backend_docpage.html [new file with mode: 0644]
template/confreg/confadmin_base.html

diff --git a/docs/confreg/badges.md b/docs/confreg/badges.md
new file mode 100644 (file)
index 0000000..8a330b9
--- /dev/null
@@ -0,0 +1,31 @@
+# Badges
+
+Other than skinning the website itself, creation of "skinned badges"
+is also possible. Unlike the website, if there is no skin created, no
+badges can be created (it is of course still possible to download a
+CSV list of all registrations and create them separately).
+
+To create badges, use the [Attendee reports](reports#attendee) page.
+
+**Always** create a filter that includes "Payment confirmed". Without
+this, badges would be created for attendees who have not paid. The
+"Payment confirmed" field will be set automatically even for
+registrations that don't need payment, as soon as they are confirmed.
+
+Select output format "badge", and decide if you want borders and/or
+page breaks included, depending on printing requirements and the badge
+design itself.
+
+It doesn't matter which fields are included, as the badge template
+will always have access to all badges.
+
+### Incremental printing
+
+If badges need to be printed incrementally (for example if most badges
+have been printed but late registrations have to be printed as a
+separate batch), use the date filter functionality on *payment
+confirmed*. If the last batch was printed on 2017-10-20 for example,
+enter ">2017-10-20" in the filter field.
+
+For easier validation, generate a normal report with this filter
+first, and change the output format to *Badges* when ready.
diff --git a/docs/confreg/callforpapers.md b/docs/confreg/callforpapers.md
new file mode 100644 (file)
index 0000000..f8091ef
--- /dev/null
@@ -0,0 +1,113 @@
+# Call for Papers
+
+## Running a call for papers
+
+Running a call for papers consists of a few clearly separated steps:
+
+### 1. Collecting proposals
+
+To start collecting proposals, enable the call for papers on the
+[conference](configuring). As soon as this is done, speakers can start
+submitting entries. Before doing that, make sure you configure the
+introductory text (if you want one), and decide if you want to ask for
+skill levels.
+
+If you want to ask for Tracks in the call for papers, make sure you
+[create them](schedule#tracks) create them first, and flag them as "in
+cfp". If no tracks with "in cfp" set exists, the track field will
+automatically be excluded from the call for papers form.
+
+Each speaker has to set up a speaker profile. This profile is shared
+between all the conferences on the system, so if they have already set
+one up, then they can just re-use the one they already have.
+
+### 2. Voting
+
+Voting is done by everybody who is listed as a talk voter in the
+conference configuration, by using the "view sessions and vote" page.
+
+Each person can give a score, 1-9 with 9 being the best, to a talk to
+rate it. If the score is left at 0 that means "no vote", it does *not*
+mean "really bad".
+
+Normally each person is not allowed to vote for any talks they
+submitted themselves, and need to exercise restraint when voting for
+colleagues. However, those rules are not in any way enforced in the
+system, but are handled manually by the people on the committee.
+
+Each voter can leave a comment for each talk in the text field, and
+view each others comment. All voting is open to all members of the
+committee.
+
+Click the talk title to view full details about the talk.
+
+Click the status to bring up a dialog allowing the change of status.
+
+Sessions can be sorted by session name (default), speakers or average
+score by clicking the appropriate headlines.
+
+### 3. Deciding and notifying speakers
+
+Once the voting is done, the decisions can be made and the speakers be
+notified. As this process starts, each session pass through a number
+of [states](#states), where each level can generate an email. These
+emails are always just flagged and not sent until explicitly
+requested. That way it's possible to perform extra review.
+
+A state of a session can be changed either from the voting page (by
+clicking the current state and picking a new one) or from the Edit
+Session page (by picking a new state in the drop down). In either case
+the new state is limited by valid state transitions. By repeating this
+process it is possible to "break the rules" and bypass states, but
+this should of course never be done. After having edited a session
+state, be sure not to forget to trigger the emails!
+
+Once a session has entered the *approved* state, it will show up in
+the list of sessions, if that is enabled for the
+[conference](configuring).
+
+See below for a diagram of how a session moves through the different
+states. Most of these steps are fully automatic by changing the
+state. The *exception* to consider here is the **reserve list**!
+
+To put a session on the reserve list, the process is:
+
+1. Contact the speaker manually via email to confirm that they are
+willing to be on the reserve list, knowing what it means.
+1. Change the state to Reserve
+1. Trigger the email
+
+## Session states <a name="states"></a>
+
+During call for paper handling, a session moves between different
+states. Some of these movements are initiated by the user (such as
+submitting or confirming a talk), some are initiated by the admin
+using the backend interfaces.
+
+Whenever a session state is changed, an email may be triggered. These
+emails are *not* sent by default. Instead, they are placed in a queue,
+and this queue can be reviewed and emptied using the button that
+appears on the main administration page when the queue has some
+contents. This is so that it can be verified that incorrect state
+change emails are not sent out in the middle of processing multiple
+talks as part of a call for papers.
+
+### Session state flow
+
+![Session states](graphs/talk_states.svg)
+
+
+## Slides <a name="slides"></a>
+
+The call for papers system has a built-in functionality to handle
+uploading of slides from speakers, or linking to them.
+
+Once a talk is in the [state](#states) *Accepted* it will be possible
+for the speaker to return to the page for it and either upload slides
+or add links to slides that are hosted elsewhere (such as on their
+personal website or on a slide-hosting service). Once uploaded these
+slides will be available from the public page of the session, and
+possibly also indicated in the session list and on the schedule
+(depending on the templates used).
+
+Uploaded slides are restricted to PDF format.
diff --git a/docs/confreg/configuring.md b/docs/confreg/configuring.md
new file mode 100644 (file)
index 0000000..73f7135
--- /dev/null
@@ -0,0 +1,218 @@
+# Configuring a conference
+
+This documentation page covers how to configure an existing
+conference. For information about how a superuser can create a
+completely new conference, see the [separate page](super_conference#new).
+
+### Base configuring
+
+There are a few base configuration steps to always perform.
+
+Set the maximum number of attendees before the [waitlist](waitlist)
+activates. This should always be set to a few below venue capacity, to
+make sure a multireg doesn't "break the bank".
+
+Configure the automatic cancellation of invoices that are old past a
+certain time. As the conference approaches, this number should be
+decreased. The risk of keeping it too high is that people create an
+invoice, which then "blocks" a seat at the conference, without paying
+it. During that time, it's unknown if they will actually attend, so
+seats cannot be given to others.
+
+Once the autocancel time is decreased below a certain threshold, some
+payment methods that don't complete immediately (notably things like
+IBAN bank transfers) will automatically become unavailable to the attendees.
+
+
+Decide if you want a welcome email to be sent to all attendees when
+their registration is fully completed (payment confirmed), which is
+normally recommended. If you do, of course also set the text of that
+email. The email will be sent from the address configured as the
+contact address by the [superuser](super_conference).
+
+If needed, add a text added as a prefix before the
+[additional options](registrations) listing on the registration form.
+
+### Registration fields
+
+Configure which additional fields should be included on the
+registration form, such as Twitter name or Dietary needs (see
+[below](#conferenceform) for reference). This should always be done
+before the first registration is allowed, to make sure the same data
+exists on all attendees.
+
+### Workflow steps
+
+You can separately enable each of the different user workflow steps
+for [registration](registration), [call for papers](callforpapers),
+[call for sponsors](sponsors), [schedule](schedule) and
+[feedback](feedback). Make sure you don't enable any steps until the
+organisation is ready for it.
+
+### Call for papers
+
+Decide if you want skill level prompts in the
+[call for papers](callforpapers) and visible in the
+[schedule](schedule), and set the intro text used on the
+[call for papers](callforpapers).
+
+### Roles
+
+There are four types of roles that can be configured at the level of
+conference.
+
+Testers are users that can bypass the restrictions on which parts of
+the workflow are open, e.g. they can perform a registration even if
+registrations are closed. The idea is that this can be used for
+example to test skinning functions, and to validate the setup of
+things like [registration types](registrations).
+
+Talkvoters are users that can vote on talks in the
+[call for papers](callforpapers).
+
+Staff are users who are allowed to [register](registrations) with a
+registration that requires staff (basically intended for free or
+discounted registrations). This is assigned to users *before* they
+register, to gain access to this registration type.
+
+Volunteers are *registered* users who can participate in the
+[volunteer schedule](volunteers). Note that this requires the users to
+actually be registered for the conference in order to be
+selected. This is a separate flag instead of a registration type so
+that it's easy to for example have attendees who are both speakers and
+on the volunteer schedule. If volunteers should get free entry as
+well, that is typically handled either with a
+[registration type](registrations) that requires manual validation, or
+with a custom [voucher or discount code](vouchers).
+
+The final role that exists in a conference is an administrator. This
+can only be assigned by a [superuser](super_conference).
+
+## Reference
+
+### Conference details <a name="conferenceform"></a>
+
+The form to edit a conference has the following fields:
+
+Attendees before waitlist
+: Number of confirmed attendees before the [waitlist](waitlist) is
+activated. This should be below the venue maximum *with some margin*,
+as the number of attendees can "jump" with either bulk registrations
+or parallel registration processes by multiple users.
+
+Autocancel invoices
+: Invoices are automatically canceled if not paid after this many
+hours. Should always be set to ensure there are no "dangling invoices"
+from people who are not completing their registration, making it
+impossible to know if they will use their seat or not. Typically
+starts out as a high value that is decreased as either the conference
+draws closer or it starts approaching sold out.
+
+Send welcome mail
+: Should an email be sent to the attendee confirming that they have
+completed their registration.
+
+Welcome email contents
+: Contents of said welcome email
+
+Additionalintro
+: Text shown on the registration page just above the list of
+additional options. Typically introduces what the additional options
+are. Can contain markdown.
+
+Field t-shirt
+: Should the field asking for t-shirt size be displayed on
+registration form. Only used if t-shirts are given out.
+
+Field dietary
+: Should the field asking for dietary needs be displayed on the
+registration form. Only used if catering is provided.
+
+Field nick
+: Should the field asking for nickname be displayed on the
+registration form.
+
+Field twitter name
+: Should the field asking for twitter name be displayed on the
+registration form.
+
+Field share email
+: Should the field asking to share email address with sponsors be
+displayed on the registration form.
+
+Field photo consent
+: Should the field asking the attendee to give (or not) consent to
+have their photograph taken at the event.
+
+
+Registration open
+: If regular registration is open.
+
+Allow editing registrations
+: If a user is allowed to edit an existing registration. Only some
+limited fields can be edited, things like t-shirt size and dietary
+needs. This is typically turned off once the final list of such
+information has to be locked in with a venue, or when badges are
+printed (in case some of this information is used on the badges).
+
+Call for papers open
+: If the call for papers is open
+
+Call for sponsors open
+: If the call for sponsors is open
+
+Schedule publishing active
+: If the schedule is published, including times, and rooms.
+
+Session publishing active
+: If the session publishing is active, which just lists the sessions
+and their details, typically used before the schedule is done but
+talks are being approved.
+
+Conference feedback open
+: If registered attendees of the conference can leave
+[feedback](feedback) on the full conference.
+
+Session feedback open
+: If registered attendees of the conference can leave
+[feedback](feedback) on individual sessions.
+
+Skill levels
+: Should the [call for papers](callforpapers) ask for skill levels on
+all sessions, and should they be displayed on the schedule and session
+lists.
+
+Callforpapersintro
+: Text shown on the [call for papers](callforpapers) page, above the
+actual call for papers. Can contain HTML.
+
+Testers
+: List of users who are assigned as testers of the conference. These
+users can bypass the restrictions above and make registrations and
+submissions even when they are closed, etc. This can be both
+registered users and not registered users.
+
+Talkvoters
+: List of users who can vote in the
+[call for papers](callforpapers). This can be both registered users
+and not registered users.
+
+Staff
+: List of users who can register as staff using the special
+[registration type](registrations). This is typically users who have
+not registered when they are added, since they will later use the
+staff registration to become registered.
+
+Volunteers
+: List of registered users who participate in the
+[volunteer schedule](volunteers). This must be registered users.
+
+Width of HTML schedule
+: Width in pixels of the built-in HTML schedule. This only controls
+the "old style" HTML schedule, which is normally overridden by the
+conference templates in which case this has no effect.
+
+Vertical pixels per minute
+: Number of pixels to assign to each minute on the Y axis when
+generating the "old style" HTML schedule.
+
diff --git a/docs/confreg/copyfromother.md b/docs/confreg/copyfromother.md
new file mode 100644 (file)
index 0000000..b3ee02c
--- /dev/null
@@ -0,0 +1,32 @@
+# Copying data from another conference
+
+Most of the conference metadata can be copied over from another
+conference. Normally this would be from another conference in the same
+series, but it can be from any conference at all.
+
+When copying from another conference, the process is:
+
+1. Enter the administration dashboard for the *receiving* conference.
+1. Click the button for the type of object being copied, such as
+   "tracks".
+1. Click "Copy *object* from other events"
+1. Select the conference to copy from. The drop down will allow for any
+   conference the user is an administrator of, or for superuser any
+   conference at all.
+1. In the list of objects, mark those objects that should be
+   copied. Use the checkbox in the top right corner of the table to
+   select *all* objects.
+1. Fill out any transforms if there are any
+1. Click "Copy *objects*"
+1. If a transform was available and used, check the box to confirm
+   that the transform example looks correct, and click "Copy
+   *objects*" again.
+
+## Transforms
+
+Some objects allow for transformation during copy. In particular,
+objects that include timestamps (*sessions*, *schedule slots* and
+*volunteer slots*) allows a transform that adds a fixed number of
+"days hours:minutes:seconds" to be added to all times, both start and
+end times.
+
diff --git a/docs/confreg/emails.md b/docs/confreg/emails.md
new file mode 100644 (file)
index 0000000..3f55c93
--- /dev/null
@@ -0,0 +1,87 @@
+# Emails
+
+## Attendee emails
+
+Using the *Attendee emails* functionality it is possible to send email
+to all attendees, or those of a specific
+[registration class](registrations#typesandclasses). Emails sent using
+this function will both be sent as an email to the attendee *and*
+listed on the registration dashboard for those that have
+registered. This means that attendees who sign up *after* the email is
+sent will **also** be able to view the emails.
+
+From
+:  The *from* address of the email will always be the
+[main contact address](super_conference) for the conference.
+
+Regclasses
+:  Select the registration classes that the email should be delivered to,
+and visible for on the registration dashboard.
+
+Subject
+:  The subject of the email
+
+Message
+:  The message body. No formatting is done, so make sure you put
+reasonable linebreaks in, and don't use markdown.
+
+Emails can (obviously) not be edited after they've been sent.
+
+## Cross conference email <a name="crossconference"></a>
+
+Cross conference emails are currently only available to superusers,m
+due to the inability to limit sender address.
+
+Cross conference emails are different from attendee emails in that
+they are a one-off sending of email. They are not stored server-side
+anywhere, and are not viewable in the system after they've been sent
+(even for administrators).
+
+Multiple criteria can be set for each email, both including and
+excluding. Include criteria are applied first, and then exclude
+criteria, so exclude ones take precedence.
+
+First pick the conference, and then either the
+[registration class](registrations#typesandclasses) or
+[speaker state](callforpaper#states). This can be done for both
+include and exclude.
+
+To add multiple either include or exclude filters, click the *+*
+button. To remove an existing filter, click the *-* button.
+
+Then fill out the actual email fields:
+
+Senderaddr
+:  Email-address used to send the email. Be careful about what is used
+here, should always be one where the sending server sets proper DKIM.
+
+Sendername
+:  Name of the sender (typically the conference series name, if sent
+to a complete conference series).
+
+Subject
+:  The subject of the email (!)
+
+Text
+:  The body of the email. No formatting is done, so take care with
+linebreaks!
+
+Before the email is actually sent a list of recipients will be shown
+at the bottom of the form and a confirm box will appear to confirm
+sending to all attendees.
+
+All emails will automatically get a footer that says where it was sent
+from and that also includes an opt-out link.
+
+
+### Opt-out <a name="optout"></a>
+
+Each email sent using this functionality will include a link to
+opt-out. Opt-out is tracked both at the global (instance) level, and
+at the *conference series* level. It is not possible to opt out from a
+single conference, but it is possible to opt out either on the full
+series or for *all* series.
+
+Note that even if users have opted out from the series, they will
+still be sent attendee emails for the conference and any emails
+concerning payments.
diff --git a/docs/confreg/feedback.md b/docs/confreg/feedback.md
new file mode 100644 (file)
index 0000000..e4be063
--- /dev/null
@@ -0,0 +1,65 @@
+# Feedback
+
+The feedback systems consists of [session feedback](#session) and
+[conference feedback](#conference). They can be independently enabled
+in the [configuration](configuring), and some events will only use
+some of them.
+
+## Session feedback <a name="session"></a>
+
+When enabled, feedback is collected on each session. A fixed set of
+fields are collected for each session -- rankings from 1-5 on topic
+importance, content quality, speaker knowledge and speaker
+quality. Other than this it also collects a set of free text comments
+that are visible only to the organisers, and a set of free text
+comments that are visible only to the speaker.
+
+Feedback is enabled for an individual session once the start time for
+the session has passed. That way the list of sessions with feedback
+available becomes incrementally available as the conference goes on.
+
+Each speaker can view their own feedback through the call for papers
+page.
+
+Administrators can view the collected feedback across all sessions on
+the admin page.
+
+### Toplists
+
+On the administrators page to view feedback there are also toplists
+with the highest scoring talks and speakers in each category. Be
+careful with the information on these lists, particularly in sharing
+it, so that people understand what it means. In particular, if there
+are multiple speakers on a talk, they share the score for both talk
+*and* speaker, which can give very surprising results if the same
+speaker also has their own talk.
+
+## Conference feedback <a name="conference"></a>
+
+The conference feedback contains one set of questions that is answered
+once by each (potential) attendee. The set of questions is dynamic and
+are created by entering a number of *feedback questions*. For each
+question, the following fields are available:
+
+Question
+:  The actual question
+
+Isfreetext
+:  If the response to the question is textual. If this box is not
+checked, the question will be a rating from 1-5.
+
+Textchoices
+: If the response is freetext per above, this field can contain a set
+of options separated by semicolons. If it does, the form will contain
+a list of options. If the field is freetext and has nothing in the
+textchoices field, a regular textbox is used.
+
+Sortkey
+:  An integer representing the sort order of this field. Lower numbers
+sort earlier.
+
+Newfieldset
+:  If specified, this question will create a new fieldset (section) on
+the form, and the string in this field will be used as the title. If
+left empty, this field will belong to the same fieldset as the
+previous (per sortkey) field.
index 3102f3b65f65b0911737edf6cbe2574e031b4ca8..cc0d5a96fb63f2c6b568b4cc5f9896820b190b6c 100644 (file)
@@ -1,4 +1,4 @@
-all: talk_states.svg
+all: talk_states.svg regular_reg.svg waitlist.svg
 
-talk_states.svg: talk_states.dot
-       dot -Tsvg talk_states.dot > talk_states.svg
+%.svg: %.dot
+       dot -Tsvg $< > $@
diff --git a/docs/confreg/graphs/regular_reg.dot b/docs/confreg/graphs/regular_reg.dot
new file mode 100644 (file)
index 0000000..3d34e76
--- /dev/null
@@ -0,0 +1,19 @@
+digraph registration {
+       rankdir="LR";
+
+       start[shape=circle label=Start];
+
+       saved[shape=box label=Saved];
+       invoice[shape=box label="Invoice generated"];
+       paid[shape=box label="Invoice paid"];
+       done[shape=box label=Completed];
+
+       start -> saved [label = "Details saved"];
+       saved -> invoice [label = "Invoice generated"];
+       invoice -> paid [label = "Payment"];
+       paid -> done;
+
+       saved -> saved [label = "Edit" ];
+       invoice -> saved [label = "Invoice expired or canceled"];
+       saved -> done [label = "No payment required"];
+}
\ No newline at end of file
diff --git a/docs/confreg/graphs/regular_reg.svg b/docs/confreg/graphs/regular_reg.svg
new file mode 100644 (file)
index 0000000..77cd518
--- /dev/null
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.38.0 (20140413.2041)
+ -->
+<!-- Title: registration Pages: 1 -->
+<svg width="877pt" height="93pt"
+ viewBox="0.00 0.00 876.60 93.00" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 89)">
+<title>registration</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-89 872.595,-89 872.595,4 -4,4"/>
+<!-- start -->
+<g id="node1" class="node"><title>start</title>
+<ellipse fill="none" stroke="black" cx="27.2976" cy="-34" rx="27.0966" ry="27.0966"/>
+<text text-anchor="middle" x="27.2976" y="-30.3" font-family="Times,serif" font-size="14.00">Start</text>
+</g>
+<!-- saved -->
+<g id="node2" class="node"><title>saved</title>
+<polygon fill="none" stroke="black" points="217.595,-52 163.595,-52 163.595,-16 217.595,-16 217.595,-52"/>
+<text text-anchor="middle" x="190.595" y="-30.3" font-family="Times,serif" font-size="14.00">Saved</text>
+</g>
+<!-- start&#45;&gt;saved -->
+<g id="edge1" class="edge"><title>start&#45;&gt;saved</title>
+<path fill="none" stroke="black" d="M54.9326,-34C81.8265,-34 123.551,-34 153.426,-34"/>
+<polygon fill="black" stroke="black" points="153.456,-37.5001 163.456,-34 153.456,-30.5001 153.456,-37.5001"/>
+<text text-anchor="middle" x="109.095" y="-37.8" font-family="Times,serif" font-size="14.00">Details saved</text>
+</g>
+<!-- saved&#45;&gt;saved -->
+<g id="edge5" class="edge"><title>saved&#45;&gt;saved</title>
+<path fill="none" stroke="black" d="M176.122,-52.1527C173.896,-61.5391 178.72,-70 190.595,-70 197.831,-70 202.45,-66.8581 204.449,-62.2796"/>
+<polygon fill="black" stroke="black" points="207.952,-62.3475 205.068,-52.1527 200.965,-61.9208 207.952,-62.3475"/>
+<text text-anchor="middle" x="190.595" y="-73.8" font-family="Times,serif" font-size="14.00">Edit</text>
+</g>
+<!-- invoice -->
+<g id="node3" class="node"><title>invoice</title>
+<polygon fill="none" stroke="black" points="517.595,-65 404.595,-65 404.595,-29 517.595,-29 517.595,-65"/>
+<text text-anchor="middle" x="461.095" y="-43.3" font-family="Times,serif" font-size="14.00">Invoice generated</text>
+</g>
+<!-- saved&#45;&gt;invoice -->
+<g id="edge2" class="edge"><title>saved&#45;&gt;invoice</title>
+<path fill="none" stroke="black" d="M217.709,-45.1797C223.496,-47.1792 229.671,-48.9564 235.595,-50 288.304,-59.2859 349.053,-57.7479 394.056,-54.3254"/>
+<polygon fill="black" stroke="black" points="394.591,-57.7936 404.275,-53.4956 394.024,-50.8166 394.591,-57.7936"/>
+<text text-anchor="middle" x="311.095" y="-59.8" font-family="Times,serif" font-size="14.00">Invoice generated</text>
+</g>
+<!-- done -->
+<g id="node5" class="node"><title>done</title>
+<polygon fill="none" stroke="black" points="868.595,-36 791.595,-36 791.595,-0 868.595,-0 868.595,-36"/>
+<text text-anchor="middle" x="830.095" y="-14.3" font-family="Times,serif" font-size="14.00">Completed</text>
+</g>
+<!-- saved&#45;&gt;done -->
+<g id="edge7" class="edge"><title>saved&#45;&gt;done</title>
+<path fill="none" stroke="black" d="M217.932,-26.3833C223.705,-25.0043 229.819,-23.7632 235.595,-23 310.071,-13.1599 329.477,-20.8437 404.595,-20 541.242,-18.4653 702.645,-18.1046 781.252,-18.0223"/>
+<polygon fill="black" stroke="black" points="781.333,-21.5223 791.329,-18.0128 781.326,-14.5223 781.333,-21.5223"/>
+<text text-anchor="middle" x="594.095" y="-21.8" font-family="Times,serif" font-size="14.00">No payment required</text>
+</g>
+<!-- invoice&#45;&gt;saved -->
+<g id="edge6" class="edge"><title>invoice&#45;&gt;saved</title>
+<path fill="none" stroke="black" d="M404.518,-33.5905C398.498,-32.5306 392.438,-31.6286 386.595,-31 331.552,-25.0782 267.272,-27.9448 228.009,-30.7892"/>
+<polygon fill="black" stroke="black" points="227.572,-27.3124 217.867,-31.5642 228.105,-34.292 227.572,-27.3124"/>
+<text text-anchor="middle" x="311.095" y="-34.8" font-family="Times,serif" font-size="14.00">Invoice expired or canceled</text>
+</g>
+<!-- paid -->
+<g id="node4" class="node"><title>paid</title>
+<polygon fill="none" stroke="black" points="754.595,-65 670.595,-65 670.595,-29 754.595,-29 754.595,-65"/>
+<text text-anchor="middle" x="712.595" y="-43.3" font-family="Times,serif" font-size="14.00">Invoice paid</text>
+</g>
+<!-- invoice&#45;&gt;paid -->
+<g id="edge3" class="edge"><title>invoice&#45;&gt;paid</title>
+<path fill="none" stroke="black" d="M517.777,-47C560.41,-47 618.757,-47 660.267,-47"/>
+<polygon fill="black" stroke="black" points="660.438,-50.5001 670.438,-47 660.438,-43.5001 660.438,-50.5001"/>
+<text text-anchor="middle" x="594.095" y="-50.8" font-family="Times,serif" font-size="14.00">Payment</text>
+</g>
+<!-- paid&#45;&gt;done -->
+<g id="edge4" class="edge"><title>paid&#45;&gt;done</title>
+<path fill="none" stroke="black" d="M754.641,-36.694C763.31,-34.5176 772.527,-32.2032 781.416,-29.9713"/>
+<polygon fill="black" stroke="black" points="782.42,-33.3281 791.266,-27.4982 780.715,-26.5389 782.42,-33.3281"/>
+</g>
+</g>
+</svg>
diff --git a/docs/confreg/graphs/waitlist.dot b/docs/confreg/graphs/waitlist.dot
new file mode 100644 (file)
index 0000000..b1adacf
--- /dev/null
@@ -0,0 +1,30 @@
+digraph waitlist {
+       subgraph legend {
+               key[shape=box, label=<
+                   <font color="blue">Actions by admin</font><br/>
+                   <font color="green">Actions by attendee</font><br/>
+               >, labeljust="l"]
+       }
+
+       start[shape=circle label="Registration"];
+
+       waitlist[shape=box label="On waitlist"];
+       offer[shape=box label="Offer made"];
+       invoice[shape=box label="Invoice generated"];
+       completed[shape=box label="Registration completed"];
+       deleted[shape=triangle label="Waitlist canceled"];
+
+       start -> waitlist [label = "&#9993; Sign up on WL", color=green];
+       start -> deleted [label = "No interest in WL", color=green];
+
+       waitlist -> offer [label = "&#9993; Offer extended", color=blue];
+
+       offer -> waitlist [label = "&#9993; Offer expired"];
+       offer -> waitlist [label = "&#9993; Offer canceled", color=blue];
+       offer -> deleted [label = "Declined spot", color=green];
+       offer -> invoice [label = "Accepted spot", color=green];
+
+       invoice -> completed [label = "&#9993; Paid invoice", color=green];
+       invoice -> waitlist [label = "&#9993; Invoice expired"];
+
+}
\ No newline at end of file
diff --git a/docs/confreg/graphs/waitlist.svg b/docs/confreg/graphs/waitlist.svg
new file mode 100644 (file)
index 0000000..084d4a4
--- /dev/null
@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+ "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Generated by graphviz version 2.38.0 (20140413.2041)
+ -->
+<!-- Title: waitlist Pages: 1 -->
+<svg width="512pt" height="474pt"
+ viewBox="0.00 0.00 511.79 473.89" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 469.89)">
+<title>waitlist</title>
+<polygon fill="white" stroke="none" points="-4,4 -4,-469.89 507.787,-469.89 507.787,4 -4,4"/>
+<!-- key -->
+<g id="node1" class="node"><title>key</title>
+<polygon fill="none" stroke="black" points="273,-429.945 132,-429.945 132,-393.945 273,-393.945 273,-429.945"/>
+<text text-anchor="start" x="145.5" y="-415.745" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;</text>
+<text text-anchor="start" x="161.5" y="-415.745" font-family="Times,serif" font-size="14.00" fill="blue">Actions by admin</text>
+<text text-anchor="start" x="140" y="-401.745" font-family="Times,serif" font-size="14.00"> &#160;&#160;&#160;</text>
+<text text-anchor="start" x="156" y="-401.745" font-family="Times,serif" font-size="14.00" fill="green">Actions by attendee</text>
+</g>
+<!-- start -->
+<g id="node2" class="node"><title>start</title>
+<ellipse fill="none" stroke="black" cx="344.5" cy="-411.945" rx="53.8905" ry="53.8905"/>
+<text text-anchor="middle" x="344.5" y="-408.245" font-family="Times,serif" font-size="14.00">Registration</text>
+</g>
+<!-- waitlist -->
+<g id="node3" class="node"><title>waitlist</title>
+<polygon fill="none" stroke="black" points="240,-307 163,-307 163,-271 240,-271 240,-307"/>
+<text text-anchor="middle" x="201.5" y="-285.3" font-family="Times,serif" font-size="14.00">On waitlist</text>
+</g>
+<!-- start&#45;&gt;waitlist -->
+<g id="edge1" class="edge"><title>start&#45;&gt;waitlist</title>
+<path fill="none" stroke="green" d="M303.854,-376.151C296.745,-370.09 289.414,-363.854 282.5,-358 265.078,-343.249 245.474,-326.787 229.916,-313.755"/>
+<polygon fill="green" stroke="green" points="232.096,-311.016 222.181,-307.281 227.602,-316.384 232.096,-311.016"/>
+<text text-anchor="middle" x="309" y="-328.8" font-family="Times,serif" font-size="14.00">✉ Sign up on WL</text>
+</g>
+<!-- deleted -->
+<g id="node7" class="node"><title>deleted</title>
+<polygon fill="none" stroke="black" points="359.5,-133 215.425,-98.5 503.575,-98.5 359.5,-133"/>
+<text text-anchor="middle" x="359.5" y="-106.3" font-family="Times,serif" font-size="14.00">Waitlist canceled</text>
+</g>
+<!-- start&#45;&gt;deleted -->
+<g id="edge2" class="edge"><title>start&#45;&gt;deleted</title>
+<path fill="none" stroke="green" d="M368.008,-363.225C382.303,-328.899 396.862,-281.229 391.5,-238 387.318,-204.285 377.117,-166.663 369.347,-141.18"/>
+<polygon fill="green" stroke="green" points="372.611,-139.888 366.297,-131.38 365.927,-141.969 372.611,-139.888"/>
+<text text-anchor="middle" x="442.5" y="-241.8" font-family="Times,serif" font-size="14.00">No interest in WL</text>
+</g>
+<!-- offer -->
+<g id="node4" class="node"><title>offer</title>
+<polygon fill="none" stroke="black" points="246.5,-220 166.5,-220 166.5,-184 246.5,-184 246.5,-220"/>
+<text text-anchor="middle" x="206.5" y="-198.3" font-family="Times,serif" font-size="14.00">Offer made</text>
+</g>
+<!-- waitlist&#45;&gt;offer -->
+<g id="edge3" class="edge"><title>waitlist&#45;&gt;offer</title>
+<path fill="none" stroke="blue" d="M162.668,-284.997C119.999,-280.159 60.3751,-267.931 83.5,-238 102.877,-212.92 122.042,-228.761 152.5,-220 153.917,-219.592 155.353,-219.174 156.801,-218.748"/>
+<polygon fill="blue" stroke="blue" points="157.891,-222.075 166.463,-215.848 155.879,-215.371 157.891,-222.075"/>
+<text text-anchor="middle" x="131.5" y="-241.8" font-family="Times,serif" font-size="14.00">✉ Offer extended</text>
+</g>
+<!-- offer&#45;&gt;waitlist -->
+<g id="edge4" class="edge"><title>offer&#45;&gt;waitlist</title>
+<path fill="none" stroke="black" d="M204.226,-220.061C203.553,-225.755 202.892,-232.142 202.5,-238 202.009,-245.334 201.733,-253.285 201.584,-260.643"/>
+<polygon fill="black" stroke="black" points="198.081,-260.812 201.446,-270.858 205.081,-260.907 198.081,-260.812"/>
+<text text-anchor="middle" x="246.5" y="-241.8" font-family="Times,serif" font-size="14.00">✉ Offer expired</text>
+</g>
+<!-- offer&#45;&gt;waitlist -->
+<g id="edge5" class="edge"><title>offer&#45;&gt;waitlist</title>
+<path fill="none" stroke="blue" d="M246.572,-211.183C274.561,-218.865 304.594,-232.312 290.5,-253 281.133,-266.749 265.463,-275.097 249.86,-280.166"/>
+<polygon fill="blue" stroke="blue" points="248.867,-276.81 240.21,-282.917 250.786,-283.541 248.867,-276.81"/>
+<text text-anchor="middle" x="340.5" y="-241.8" font-family="Times,serif" font-size="14.00">✉ Offer canceled</text>
+</g>
+<!-- invoice -->
+<g id="node5" class="node"><title>invoice</title>
+<polygon fill="none" stroke="black" points="128,-128 15,-128 15,-92 128,-92 128,-128"/>
+<text text-anchor="middle" x="71.5" y="-106.3" font-family="Times,serif" font-size="14.00">Invoice generated</text>
+</g>
+<!-- offer&#45;&gt;invoice -->
+<g id="edge7" class="edge"><title>offer&#45;&gt;invoice</title>
+<path fill="none" stroke="green" d="M180.79,-183.86C159.489,-169.66 129.036,-149.357 105.683,-133.789"/>
+<polygon fill="green" stroke="green" points="107.376,-130.711 97.1145,-128.076 103.494,-136.536 107.376,-130.711"/>
+<text text-anchor="middle" x="189.5" y="-154.8" font-family="Times,serif" font-size="14.00">Accepted spot</text>
+</g>
+<!-- offer&#45;&gt;deleted -->
+<g id="edge6" class="edge"><title>offer&#45;&gt;deleted</title>
+<path fill="none" stroke="green" d="M235.638,-183.86C260.799,-169.06 297.228,-147.63 324.07,-131.841"/>
+<polygon fill="green" stroke="green" points="325.93,-134.808 332.775,-126.721 322.381,-128.774 325.93,-134.808"/>
+<text text-anchor="middle" x="325.5" y="-154.8" font-family="Times,serif" font-size="14.00">Declined spot</text>
+</g>
+<!-- invoice&#45;&gt;waitlist -->
+<g id="edge9" class="edge"><title>invoice&#45;&gt;waitlist</title>
+<path fill="none" stroke="black" d="M64.7193,-128.391C54.0105,-159.118 37.8388,-222.624 73.5,-253 95.4854,-271.727 126.632,-280.506 152.696,-284.59"/>
+<polygon fill="black" stroke="black" points="152.389,-288.08 162.774,-285.98 153.346,-281.146 152.389,-288.08"/>
+<text text-anchor="middle" x="103" y="-198.3" font-family="Times,serif" font-size="14.00">✉ Invoice expired</text>
+</g>
+<!-- completed -->
+<g id="node6" class="node"><title>completed</title>
+<polygon fill="none" stroke="black" points="143,-36 0,-36 0,-0 143,-0 143,-36"/>
+<text text-anchor="middle" x="71.5" y="-14.3" font-family="Times,serif" font-size="14.00">Registration completed</text>
+</g>
+<!-- invoice&#45;&gt;completed -->
+<g id="edge8" class="edge"><title>invoice&#45;&gt;completed</title>
+<path fill="none" stroke="green" d="M71.5,-91.6471C71.5,-78.8228 71.5,-61.1075 71.5,-46.3815"/>
+<polygon fill="green" stroke="green" points="75.0001,-46.2996 71.5,-36.2996 68.0001,-46.2997 75.0001,-46.2996"/>
+<text text-anchor="middle" x="112.5" y="-57.8" font-family="Times,serif" font-size="14.00">✉ Paid invoice</text>
+</g>
+</g>
+</svg>
diff --git a/docs/confreg/index.md b/docs/confreg/index.md
new file mode 100644 (file)
index 0000000..6ba431b
--- /dev/null
@@ -0,0 +1,41 @@
+# PostgreSQL Europe Conference Management System
+
+This is the administrative help for the PostgreSQL Europe Conference
+Management System. It is aimed at the persons administering
+conferences -- it is not for the developers of the system, or for the
+end users.
+
+At any point in the system, you can click the help link in the top
+right corner. If there is specific documentation about the part that
+you are currently working on you will be brought directly to the page
+for that. If not, you will be brought here to the index page, and can
+work from here.
+
+## Table of contents
+* Quickstart
+    * [Step by step to create a conference](stepbystep)
+* Basics
+    * [Creating new conference](super_conference#new) (superusers only)
+    * [Configuring a conference](configuring)
+    * [Conference series](series)
+* Call for papers and schedule
+    * [Call for papers](callforpapers)
+    * [Schedule](schedule)
+    * [Slides](callforpapers#slides)
+* Registrations
+    * [Registrations and registration types](registrations)
+    * [Vouchers and discount codes](vouchers)
+    * [Waitlist](waitlist)
+    * [Emails](emails)
+    * [Wiki pages](wiki)
+    * [Signups](signups)
+    * [Volunteers](volunteers)
+    * [Reports](reports)
+* Feedback
+    * [Feedback](feedback)
+* Sponsors
+    * Probably several links
+* Advanced
+    * [Access tokens](tokens)
+    * [Badges](badges)
+    * [Copying from another conference](copyfromother)
diff --git a/docs/confreg/personaldata.md b/docs/confreg/personaldata.md
new file mode 100644 (file)
index 0000000..b68ae69
--- /dev/null
@@ -0,0 +1,38 @@
+# Personal data
+
+Personal data must, of course, be collected by the registration system
+in order to work. However, as much of that personal data is not needed
+past the end of the conference, there is functionality to purge that
+data.
+
+The data in that case is typically aggregated and anonymized in order
+to still be able to access statistical information, but without any
+chance to connect it to a person.
+
+The set of personal data being purged is:
+
+T-shirt size registrations
+: This data is aggregated so that it can be used to determine how many
+t-shirts in total of each size were selected, to use for future
+events.
+
+Phone numbers
+:  This data is removed.
+
+Addresses
+:  This data is removed.
+
+Dietary needs
+:  This data is aggregated so that it can be used as examples for use
+with future events (for example, to provide venues with examples and
+common responses).
+
+This data is never purged automatically. Instead there is a button at
+the top of the conference administration dashboard that gets enabled
+when the conference has finished, and remains enabled until the data
+has been purged. At regular intervals an email is also sent to the
+administrators of the conference to remember to do the purging.
+
+Typically the personal data is kept around a couple of weeks after the
+conference, in case it is needed to solve any questions or discussions.
+
diff --git a/docs/confreg/registrations.md b/docs/confreg/registrations.md
new file mode 100644 (file)
index 0000000..1d0ef18
--- /dev/null
@@ -0,0 +1,310 @@
+# Registrations and registration types
+
+## Registrations
+
+### Regular registrations
+
+Registrations are at the core of the system. The standard workflow is
+intended to be as simple as possible for the attendee (whether it
+succeeds can always be discussed..):
+
+1. The attendee signs in to the registration form. This step requires
+   a community account, or one can be created as part of the signup
+   process.
+1. The exact fields to be filled out can be
+   [configured](configuring) configured at the conference level, for
+   example if there should be a field about t-shirt size or not.
+1. The attendee can incrementally fill out and save in between if
+   necessary. If they do that, they will at regular intervals receive
+   a reminder email from the system telling them they have a stalled
+   registration.
+1. Once ready, they click "Save and Finish". At this point they get a
+   chance to review their charges, and then proceed to actually
+   generate an invoice.
+1. Once the invoice is generated, the registration data is
+   locked. This invoice can then be paid by any of the configured
+   payment methods, normally by credit card or PayPal. If the invoice
+   is not paid within the defined [autocancel](configuring) period,
+   the invoice is automatically canceled, and the registration is
+   returned to unlocked state, and will again start receiving
+   reminders to complete it.
+1. Once the invoice is paid, a receipt is sent to the user. If a
+   welcome email is [configured](configuring) for the conference, this
+   email will be sent to the user.
+1. Certain fields are unlocked for editing again, until the *allow
+   editing* checkbox is [turned off](configuring). The registration is
+   now fully complete.
+
+![Regular registration workflow](graphs/regular_reg.svg)
+
+### Multiple registrations
+
+The system also supports a method for registering multiple attendees
+from the same account. This can also be used for one person to make a
+registration for somebody else, the process is the same.
+
+1. The person making the registrations proceeds to the form for
+   registering for somebody else or advanced registration.
+1. For each of the attendees that should be registered, the email
+   address is added and then the registration details are filled out
+   for that attendee. Additional options can be added if necessary, as
+   well as [voucher and discount codes](vouchers). The same fields and
+   restrictions as regular registrations are used.
+1. Once all registrations have been made, the user picks *Create
+   invoice*. At this point an invoice is created for all registrations
+   that are pending under this user. This invoice is paid using the
+   same rules as all other invoices.
+1. Once this invoice is paid, all registrations are confirmed at the
+   same time. Each attendee receives the confirmation email.
+    * If a community account exists for a user registered this way,
+      this account is automatically connected by matching the email
+      address, and this user can access their registration page as
+      normal.
+    * If no account exists, the user is sent an email with a
+      token-link that allows them to connect it to a different
+      community account, regardless of email address matching. The
+      attendee can also create a new account at this point.
+
+The system for multiple payments is automatically disabled once the
+[waitlist](waitlist) is in force.
+
+### Bulk payments
+
+Bulk payments is mostly the legacy way for handling multiple
+registrations, and is not used much anymore. The reason for this is
+that it requires a somewhat awkward interaction between the users
+registering and the person paying. The steps are:
+
+1. Each user makes their own registration, completing step 1-3 in the
+   list above. It is, however, very important that they do not proceed
+   to step 4 (this is the first part that usually fails).
+1. The person paying the bill then goes into the bulk payment
+   interface, and adds each individual registration using the email
+   address (this is the second part that usually fails), and enters
+   the invoicing details.
+1. The invoice is paid.
+1. The person who set up the invoice receives the receipt, and each
+   individual attendee receives the registration confirmation.
+
+The system for bulk payments is automatically disabled once the
+[waitlist](waitlist) is in force.
+
+## Registration types and classes <a name="typesandclasses"></a>
+
+When making a registration, the attendee picks the appropriate
+[registration type}(#regtypes). There always has to be at least one registration
+type, even for the most simple conference.
+
+A registration type contains the different kinds of parameters that are per-type
+configuration and maps those to what the user picks. This include things like
+cost, ability to pick [options](#options) and so on. Typical examples
+of this is "Normal attendee", "Speaker", "Staff" etc.
+
+An *optional* object higher up in the hierarchy is a registration
+class. A registration class groups a number of registration types
+together as one group. A typical example of this is there may be both
+"Attendee" and "Training attendee" as registration type, which both
+map to "Attendee" as registration class. In this scenario, the badge
+rendering (primarily - but other templates as well) can reference the
+registration class to for example always use the same color on the
+badge and similar things.
+
+### Special registration types
+
+A registration type can be mapped to a *special registration
+type*. This sets a number of restrictions on how a registration can be
+used. The following types are currently available:
+
+Manually confirmed
+: If this is picked, the registration cannot be completed by the
+attendee. It requires the administrator to confirm it manually.
+
+Confirmed speaker
+: The attendee registering must, with the same community account, have
+a speaker profile, and have at least one talk by this speaker profile
+be in a [state](callforpapers#states) of *Accepted*. Most of the time,
+the "Confirmed or reserve speaker" should probably be used instead.
+
+Confirmed or reserve speaker
+: The attendee registering must, with the same community account, have
+a speaker profile, and have at least one talk by this speaker profile
+be in a [state](callforpapers#states) of *Accepted* or the state
+*Reserve*.. This means that speakers on the reserve list can also use
+this registration type, which is usually the choice wanted since they
+should be able to be substituted at the last moment.
+
+Confirmed staff
+: The attendee registering must use one of the community accounts that
+are listed as staff in the [conference configuration](configuring).
+
+### Registration days <a name="days"></a>
+
+Registration days are not a mandatory part of the system, but if they are used
+they are used to indicate which different days the conference runs, and can be
+used to limit certain registration types to certain days, typically used to
+render information on the badges.
+
+IN the system, registration days are simply a list of dates, that must fall
+between the [conference](configuring) start date and end date.
+
+## Additional options <a name="options"></a>
+
+Additional options represent things that can be added to a
+registration, such as training sessions, attendance to a separate
+event etc. An additional option can either be paid for, or it can be a
+free option. It is something that is normally done at registration
+time, and should not be confused with [signups](signups) which are
+normally used to handle things like signing up for a social event or a
+dinner or similar things.
+
+An additional option can be restricted so it can only be used by
+a set of specific registration types. This is typically used to handle
+things like certain discounted registration types should not get
+access to a specific additional option.
+
+The reverse is also possible, a registration type can be configured so
+that it *requires* a specific additional option. This way is typically
+used to ensure things like training discount registration only being
+available to attendees who actually purchase training.
+
+Each additional option can be associated with a
+[cost](#additionaloptions), in which case the cost
+of it will be added to the initial invoice. It will also handle the
+case when the attendee later adds an option to a registration, and
+will generate an invoice before the transaction is completed.
+
+## Transferring a registration <a name="transfer"></a>
+
+A registration can be transferred to a different person, typically
+only allowed within the same company. If this transfer should be
+associated with a cost, the payment for it has to be processed
+manually.
+
+To perform a registration transfer:
+
+1. Ensure the *new* person attending makes a registration in the
+   system, but does *not* generate an invoice. If the
+   [waitlist](waitlist) is active, the registration should also *not*
+   be signed up on the waitlist (if it is, it has to be canceled
+   first).
+2. Go to the transfer page, and pick the registrations to transfer to
+   and from.
+3. Validate the steps shown, and confirm the transfer.
+
+When transferring a registration, both the "source" and "destination"
+attendees will receive an email telling them about the transfer once
+it has been completed. An email is also sent to the conference
+organiser address.
+
+Existing invoices are re-attached to the new registrations, so after a
+transfer there will be a mismatch between what's on the invoice and
+what's in the registration, but they are both traceable to each other.
+
+## Reference
+
+### Registration types <a name="regtypes"></a>
+
+Regtype
+: Name of the registration type as shown to the user
+
+Regclass
+: Registration class that this registration belongs to. This can be left
+unspecified, but that is likely to cause issues with things like badge generation.
+
+Cost
+: The cost for registering with this type, if any. 0 if the registration is free. If
+VAT is used, the cost is specified without VAT.
+
+Active
+: Whether this registration class can be used at this point.
+
+Activeuntil
+: The use-before-date for this registration type. If left empty, the registration type
+can be used as long as it's active and registration is open.
+
+Days
+: References which days this registration type gives access to, if registration days are
+used at this conference.
+
+Sortkey
+: An integer specifying how this registration type should be sorted. Lower numbers are
+sorted first.
+
+Specialtype
+: If this registration type has special rules on it
+
+Require phone
+: If attendees registering with this registration type should be required to enter
+their phone number (typically for speakers and/or staff)
+
+Alertmessage
+: This message is shown in a pop-up window when the user tries to complete the
+registration. This is typically used for things like informing people of special
+requirements such as a student ID to access student discounts.
+
+Autocancel invoices
+: If registrations with this registration type should override the value for
+autocancel from the [conference](configuring). The lowest value of
+autocancel is always used.
+
+Requires option
+: If a specific [additional option](#options) is required to use this
+registration type. If multiple options are chosen, *any one* of those
+will be enough to allow the registration type.
+
+Upsell target
+: If it should be possible t upgrade to this registration type in
+order to add additional options. If this type is more expensive than
+the previous one the attendee has, the difference will be invoiced.
+
+### Registration classes <a name="regclasses"></a>
+
+Registration class
+: Name of the registration class
+
+Badge color
+: Background color used on badge for this registration class (assuming
+the template makes use of this).
+
+Badge foreground
+: Foreground color used on the badge for this registration class
+(assuming the template makes use of this).
+
+### Additional Options <a name="additionaloptions"></a>
+
+Name
+: Name of the additional option
+
+Cost
+: Cost of this option, if any. If VAT is used, the cost is specified
+without VAT.
+
+Maxcount
+: Maximum number of instances of this additional option that can be
+purchased in total.
+
+Public
+: If this option is available on the public registration form. If this
+box is not checked ,the option can only be added by administrators.
+
+Upsellable
+: If this option can be added to a registration after it has been
+completed.
+
+Autocancel invoices
+: If registrations with this option should override the value for
+autocancel from the [conference](configuring). The lowest value of
+autocancel is always used.
+
+Requires regtype
+: In order to add one of these options, one of the specified
+registration types must be picked. If the attendee does not have one
+of these registration types, they may be offered an upsell to a
+different registration type if that is [enabled](#regtypes).
+
+Mutually exclusive
+: This option cannot be picked at the same time as the selected other
+options. This is typically used for things like training sessions
+running at the same time. If the exclusion is specified in one
+direction between the two options, it is automatically excluding both
+directions.
diff --git a/docs/confreg/reports.md b/docs/confreg/reports.md
new file mode 100644 (file)
index 0000000..7392c4b
--- /dev/null
@@ -0,0 +1,44 @@
+# Reports
+
+## Simple reports <a name="simple"></a>
+
+There are a number of simple reports available as direct buttons from
+the administration dashboard page.
+
+Some of them are "verification reports", such as finding people who
+have not registered or misconfigured sessions. Those reports will have
+their button highlighted if they have any data in them.
+
+## Attendee reports <a name="attendee"></a>
+
+Attendee reports allow for the creation of custom reports of almost
+anything related to attendees.
+
+For fields, pick the fields you want.
+
+For filters, check boxes for the fields that should be filtered. In
+each filter field, type what to filter for. This field can also take
+*>* and *<* to indicate that the value should be greater/smaller (for
+numbers) or after/before (for dates). For string filters, a *%filter%*
+is applied, so it matches partial filters. If a field is marked but
+nothing is entered in the search box, the filter will be checked for
+presence (meaning not null).
+
+Specify a title if you want one on the report, mainly for printing.
+
+In *Additional columns* specify extra column headers with comma
+between them. This is particularly useful if for example generating an
+attendee list to tick off people on -- make an empty column for the
+tick-mark!
+
+## Time reports <a name="time"></a>
+
+The time reports can be used to draw graphs relative to the time of
+conference for a number of things. In particular, they can be used to
+compare rates between different instances of conferences.
+
+The filter field will apply a client-side filter on the list, to limit
+the options. Note than when this is used, you probably have to select
+each conference manually (with ctrl-click) rather than select a range
+(with shift-click) as a range will usually include the values that
+have been filtered out.
diff --git a/docs/confreg/schedule.md b/docs/confreg/schedule.md
new file mode 100644 (file)
index 0000000..4ebd4de
--- /dev/null
@@ -0,0 +1,150 @@
+# Schedule
+
+## Scheduling
+
+Once talks have been accepted in the [call for papers](callforpapers),
+or sessions have been manually added, scheduling takes place.
+
+The following terms are in use for scheduling:
+
+Rooms
+:   This represents a physical room. Two sessions cannot be schedule
+in the same room at the same time. A room is not tied to a topic in
+any way. A room may or may not be available throughout the
+conference. Rooms are typically represented by different columns in
+the schedule.
+
+Track
+:    A track represents a topic of content. It is *not* tied to a
+specific room, so it's perfectly possible to have multiple sessions in
+the same track at the same time, as long as they are in different
+rooms. Tracks are typically represented by different colors on the schedule.
+
+Session slot
+:    A session slot represents a start and an end time on the
+schedule. Most sessions are scheduled in a schedule slot, and it's
+what the drag-and-drop interface supports doing. However, it is
+perfectly valid and possible to manually schedule a session that does
+not conform to a schedule slot.
+
+Cross schedule sessions
+:     A cross schedule session is one that is not tied to a particular
+room, but instead spans across the entire schedule. Typical examples
+of this is coffee breaks and lunch.
+
+## Building a schedule graphically
+
+Using the "create schedule" functionality a schedule can be built
+incrementally using the graphical tools. All approved sessions are
+available in a list on the right-hand side, and all rooms and schedule
+slots are available on the left.
+
+Cross schedule sessions are typically not scheduled using this tool.
+
+Drag and drop a session between locations to move it. Note that
+session length is *not* considered when doing this, so if you drag a
+session to a different length slot, it will change length!
+
+A draft can be saved at any time, and will only be visible to other
+administrators.
+
+Once done, click the link at the bottom to publish the schedule. This
+operation will show a list of exactly which changes will be
+made. Initially this will be a long list, and it's mainly useful when
+making changes to the schedule after it has been published. If the
+changes look OK, hit the confirm link to publish.
+
+The published schedule will immediately become available *if* schedule
+publishing has been activated on the [conference](configuring).
+
+## PDF Schedules
+
+Simple schedules can be built in PDF format, for printing, and usually
+gives a much nicer print-out experience than printing the one from the
+website.
+
+Schedules can be printed to include only specific [tracks](#tracks),
+[rooms](#rooms) or days. Printing can be color, in which case the
+tracks are printed in the same color as they would have on the
+website, or black and white.
+
+Printing can be done A3 or A4, portrait or landscape. It is also
+possible to stretch the schedule out over multiple pages, particularly
+useful if for example printing a full day schedule but only having
+access to an A4 printer. Print in landscape A4, and tape together to
+make for an "almost A3".
+
+## Reference
+
+### Tracks <a name="tracks"></a>
+
+Track name
+:      Name of the track
+
+Sort key
+:   An integer representing how to sort the track. Lower values sorts
+earlier.
+
+Color
+:   Color to use on schedule (and other places) for this track
+
+Incfp
+:   Whether this track should be available to choose in the
+[call for papers](callforpapers) submission form.
+
+
+### Rooms <a name="rooms"></a>
+
+Room name
+:   Name of the room
+
+Sortkey
+:   An integer representing how to sort the room. Lower values sorts
+earlier.
+
+### Schedule slots <a name="slots"></a>
+
+Start time
+:   When this slot starts
+
+End time
+:   When this slot ends
+
+### Sessions <a name="sessions"></a>
+
+The form to edit session has the following fields:
+
+Title
+:      The title of the session
+
+Speaker
+:      One or more speakers. A speaker must have created a speaker
+profile before it can be used here, but it the same speaker
+profile can be used across multiple conferences.
+
+Status
+:      The [state](callforpapers#states) state of this session.
+
+Starttime
+:      If the session is scheduled, the starting date and time.
+
+Endtime
+:      If the session is scheduled, the ending date and time.
+
+Cross schedule
+:      If this session should be displayed across the schedule, instead
+of in one room. Typically used for things like breaks and lunch.
+
+Track
+:      The track selected for this schedule, if any.
+
+Room
+:      The room selected for this schedule, if any.
+
+Abstract
+:   This is the abstract that is listed on the schedule and session
+description pages. Any text here is public.
+
+Submission notes
+:   These are notes given by the submitter in the call for papers
+form, that are only visible to the conference organisers.
diff --git a/docs/confreg/series.md b/docs/confreg/series.md
new file mode 100644 (file)
index 0000000..d5ccea2
--- /dev/null
@@ -0,0 +1,8 @@
+# Conference series
+
+A conference series represents a set of "the same" conference that
+runs across multiple instances. For example, "PGConf.EU" is a series
+and "PGConf.EU 2018" is an instance.
+
+Conference series are primarily used for the
+[e-mail opt-out](emails#optout) functionality.
diff --git a/docs/confreg/signups.md b/docs/confreg/signups.md
new file mode 100644 (file)
index 0000000..71787c3
--- /dev/null
@@ -0,0 +1,85 @@
+# Signups
+
+Signups are a way to have attendees sign up for specific events, such
+as a social event, directly from the registration page. For events
+that have attendee limits it supports setting such, as well as a last
+time to complete the signup.
+
+### Fields
+
+Author
+: The author of the signup. Only visible in the administrative
+interface.
+
+Title
+: Title of the signup
+
+Intro
+: Text describing the signup (basically the contents of the page). Can
+use markdown.
+
+Deadline
+: Timestamp for the last moment to sign up, or to change the signup.
+
+Maxsignups
+: Maximum number of attendees who can sign up.
+
+Options
+: A list of comma separated options to show for the signup. If no
+options are given, the options "Yes" and "No" will be shown. This is
+typically used for e.g. a dinner invitation and then with options
+"Yes", "Yes with a plus one" and "No".
+
+Optionvalues
+: By default, whatever is picked as an option counts the same, and the
+summary will show a count for each value. If a set of values is
+entered in this field, the number of values must be exactly the same
+as the number of options. If this is done, the results are still shown
+tabulated per option but there is *also* a total count shown where the
+values are weighed. This can for example be used in the dinner example
+above by assigning the weights "1,2,0" in which case the total value
+will then represent the total number of people going.
+
+Public
+: If checked, this signup is available to all *registered*
+attendees. A public signup cannot track "Awaiting response".
+
+Visible
+: If checked, then any attendee who has permissions on the signup
+(through it being public, or explicitly listed) can also see who else
+signed up.
+
+Available to registration types
+: For non-public signups, attendees of these registration types can
+sign up.
+
+Available to attendees
+: For non-public signups, these specific attendees (in addition to
+those of the permitted registration types) can sign up
+
+## Sending email
+
+Using the send email functionality, it is possible to send an email to
+attendees for a signup. This is a one-time email that goes to their
+mailbox but is *not* visible on the registration page after the fact
+(unlike regular [attendee emails](emails)).
+
+For each email a subject and a body can be specified (no formatting is
+done, so make sure to put reasonable linebreaks in place, and don't
+use markdown).
+
+The recipients can be picked to be all, all who have responded
+(regardless of which response), all who have not responded (to send
+reminders), and individually for each response. If emails should be
+sent to attendees with more than one response, the form has to be
+executed multiple time, once for each response.
+
+The emails will be sent from the main conference
+[contact address](super_conference).
+
+## Results
+
+The results will have a summary with how many attendees have picked
+each specific option, as well as the total value calculated from
+weighted values. Below it there will also be a complete list of every
+individual response.
diff --git a/docs/confreg/stepbystep.md b/docs/confreg/stepbystep.md
new file mode 100644 (file)
index 0000000..80b75cd
--- /dev/null
@@ -0,0 +1,30 @@
+# Step by step to create a new conference
+
+The following is not an exhaustive list, but hopefully enough to get
+off the ground.
+
+1. Create a new [conference](super_conference) (must be done by a
+   superuser).
+1. Configure all the metadata on the [conference](configuring).
+1. Set up call for papers
+    1. Add one or more [tracks](schedule#tracks), and decide if they
+       should be included in the call for papers.
+    1. Call for papers can now be opened
+1. Set up for registrations
+       1. Add one or more [registration days](registrations#days) that
+       can be connected to the registration types.
+    1. Add one or more
+       [registration classes](registrations#typesandclasses).
+       1. Add one or more
+       [registration types](registrations#typesandclasses).
+    1. If used, add one or more
+       [additional options](registrations#additionaloptions).
+    1. Registration can now be opened
+1. Set up schedule
+    1. Wait for the call for papers to finish.
+       1. [Vote](callforpapers) on talks.
+       1. [Decide](callforpapers) on talks and
+       [notify speakers](callforpapers).
+       1. Add one or more [rooms](schedule#rooms).
+       1. Add one or more [schedule slots](schedule).
+       1. Create a tentative [schedule](schedule) and publish it.
diff --git a/docs/confreg/super_conference.md b/docs/confreg/super_conference.md
new file mode 100644 (file)
index 0000000..0ecfcaf
--- /dev/null
@@ -0,0 +1,92 @@
+# Editing superuser parameters
+
+This documentation page covers how to edit the superuser only parameters
+for a conference, including [creating a new conference](#new). For
+information about how to edit the regular parameters for a conference, see
+the [separate page](configuring).
+
+## Creating a new conference
+
+To create a new conference, select the *Create new conference* button
+on the conference administration homepage. This is only available to
+superuser, since it allows referencing things like local paths on the
+server.
+
+Fill out all the fields as you normally would when editing the
+conference, per the [reference](#conferenceform).
+
+Note that an accounting object will be automatically created for a new
+conference with the same name as the conference urlname, if it does
+not already exist. This can be changed later by editing the conference
+if necessary. This accounting object is set to *active* by default.
+
+If the conference is held in a VAT jurisdiction that has never been
+used before, the VAT rates have to be registered in the accounting
+system *before* the new conference is created, and picked on
+creation. This can be delayed (by picking no VAT), but it's very
+important that it's properly configured before the first invoices are
+generated.
+
+## Reference
+
+### Superuser conference form <a name="conferenceform"></a>
+
+The form to edit superuser parameters has the following fields:
+
+Conference name
+:  The display name of the conference. Can change without invalidating
+any data.
+
+URL name
+:  The name used to form the url of the conference. As this field is also
+used to key urls and a number of other things, it is **strongly recommended**
+to never change this for a conference once it's been set up.
+
+Series
+:  The conference series this conference belongs to
+
+Start date
+:  The start date of the conference
+
+End date
+:  The end date of the conference
+
+Location
+:  The physical location (e.g. City, Country) of the conference
+
+Timediff
+:  The number of hours away from the system standard time this conference
+is. Used for schedule rendering and ical.
+
+Contact address
+:  Email address to contact the conference organisers on. This address is
+also used to send emails from. The server with the system installed on should
+preferably have DKIM set up for this domain.
+
+Sponsor address
+:  Email address to contact conference organisers on about sponsorship. This
+address is also used to send emails from. The server with the system installed on
+should preferably have DKIM set up for this domain.
+
+Conference URL
+:  URL to the conference homepage
+
+Administrators
+:  The people who should have full administrative permissions on this conference.
+Those people can then use the regular conference configuration form to set all
+other parameters for the conference.
+
+Jinja directory
+:  The directory where the Jinja templates for the conference are located on
+the server, if any. If no Jinja directory is specified, all conference pages will
+be rendered in the default templates. If an invalid directory is specified, all
+conference pages will generate an error.
+
+Accounting object
+:  The accounting object that should be assigned to all invoices for this conference
+
+VAT rate for registrations
+:  The VAT rate (if any) to apply to all invoices for registrations
+
+VAT rate for sponsorship
+:  The VAT rate (if any) to apply to all invoices for sponsorship
diff --git a/docs/confreg/tokens.md b/docs/confreg/tokens.md
new file mode 100644 (file)
index 0000000..505c952
--- /dev/null
@@ -0,0 +1,32 @@
+# Access tokens
+
+Access tokens are created tokens that enable "secret URLs" with access
+to specific information from the conference.
+
+This can for example be live-pulling data into a Google spreadsheet,
+or consuming it from a JavaScript based schedule webpage, or any
+other interesting ideas people can come up with.
+
+The idea between the specific access token objects is that each
+individual service that uses this should be given a separate
+token. This way it becomes possible to revoke a token for just one
+such consumer without shutting down all the others.
+
+### Reference
+
+The access token objects have the following fields:
+
+Token
+:  This is the secret URL token. It's automatically filled with random
+data when a new token is created, and it cannot be changed.
+
+Description
+:  Internal description. This is not used by the system anywhere, it's
+just for administrators to know what is what.
+
+Permissions
+:  This list the different kinds of access that can be retrieved using
+this token. For each access type it will also have a csv (comma
+separated) and tsv (tab separated) values link that includes the
+token. (More formats may be added in the future).
+
diff --git a/docs/confreg/volunteers.md b/docs/confreg/volunteers.md
new file mode 100644 (file)
index 0000000..540cc26
--- /dev/null
@@ -0,0 +1,49 @@
+# Volunteer schedule
+
+The volunteer schedule system ties together attendees with
+[volunteer slots](#slots).
+
+To begin, create the slots as needed in the administrative interface,
+and assign the volunteers using the
+[conference configuration](configuring). Volunteers can also be added
+later on, in which case they will just have to sign up for what's
+left.
+
+Once all slots are available, ask the volunteers to sign up. Anybody
+added as a volunteer in the conference configuration will get a link
+to the volunteer schedule on their registration page (once
+registration is completed).
+
+### Normal workflow
+
+Normally, each volunteer signs up for the slot(s) they are interested
+in working on. Once this is done, the administrator can either *confirm*
+or *delete* the volunteer for this specific slot.
+
+### Admin-driven workflow
+
+The other way to add volunteers is to have the administrator add them
+from the admin interface. In this case the assignment *also* becomes
+unconfirmed, and now requires the volunteer to confirm it from *their*
+registration page.
+
+
+## Volunteer slots <a name="slots"></a>
+
+Volunteer slots represents a task that need to be handled at a
+specific time and place.
+
+Timerange
+:  Represents the start and end time of this volunteer slot
+
+Title
+:  Name of the work. This can be e.g. "Registration desk" or "Room
+host in room B"
+
+Min staff
+:  The minimum number of people needed for this task
+
+Max staff
+:  The maximum number of people needed for this task
+
+
diff --git a/docs/confreg/vouchers.md b/docs/confreg/vouchers.md
new file mode 100644 (file)
index 0000000..a6dc298
--- /dev/null
@@ -0,0 +1,77 @@
+# Vouchers and discount codes
+
+Vouchers and discount codes are two different ways to make
+registrations cheaper or free for attendees. These can both be for
+marketing purposes from the conference, as sponsor benefits, or as
+almost anything.
+
+## Prepaid vouchers
+
+Prepaid vouchers are used to cover an entire registration. They are
+locked to one specific registration type. The voucher code is sent to
+the attendee, and the attendee performs their own registration and
+puts this secret key in the voucher field, which makes the
+registration free.
+
+If all that's wanted is to register for another user, it's better to
+use the [advanced registration flow](registrations). Vouchers are
+appropriate if the vouchers are distributed to a different
+organisation, and where the person paying does not have access to the
+information about the attendee.
+
+When creating vouchers, a *prepaid batch* is created, which contains a
+certain number of vouchers. The batch is paid, and the usage of
+vouchers can be tracked at this level.
+
+## Discount codes
+
+Discount codes are used to give a discount on a registration, but not
+a free one. They can either be percentage based (in which case it can
+either be a percentage of the registration cost alone, or a percentage
+of the registration cost together with any additional options
+selected) or a fixed amount.
+
+When making a registration, discount codes are added to the same field
+as voucher code, meaning the same registration cannot use both a
+voucher and a discount code at the same time.
+
+Discount codes can be limited to only be available for certain
+registration types, or to require certain additional option (e.g. "you
+get 20% off the main registration if you sign up for training
+X"). They can have a maximum number of uses, and a final date on which
+they can be used (e.g. the typical EARLYBIRD use).
+
+## Reference
+
+### Discount codes <a name="discountcodes"></a>
+
+Code
+: This is the actual code that should be entered.
+
+Discountpercentage
+: The percent discount that this code gives. Can't be specified at
+the same time as discountamount.
+
+Regonly
+: The percent applies only to the registration, not the full cost
+including additional options (only available if the type of
+discount code is percentage)
+
+Discountamount
+: The amount of discount this code gives. Can't be specified at the
+same time as discountpercentage.
+
+Validuntil
+: The date until which this code can be used.
+
+Maxuses
+: Maximum number of uses of this code.
+
+Requiresregtype
+: In order to use this discount code, one of the selected registration
+types must be used.
+
+Requiresoption
+: In order to use this discount code, one of the selected additional
+options have to be added to the registration.
+
diff --git a/docs/confreg/waitlist.md b/docs/confreg/waitlist.md
new file mode 100644 (file)
index 0000000..b86635e
--- /dev/null
@@ -0,0 +1,58 @@
+# Waitlist management
+
+The waitlist is enabled automatically when the number of *confirmed*
+attendees to the conference exceeds the number of attendees
+[configured](configuring) for it. There is no way to manually enable
+the waitlist, other than to reduce the number of attendees before
+waitlist to a number below the current amount.
+
+Once this is hit, every attendee who wants to sign up will be offered to
+sign up on the waitlist. If they want to do that, they will be added
+to the waitlist with an empty status, and an email is sent to the
+administrators.
+
+One or more attendees can be given an *offer* from the waitlist. This
+offer is given an expiry time, giving the chance to sign up before
+that time. The length of the offer is usually adapted to how much time
+is left until the conference -- to make sure that if the attendee
+doesn't take the offer, there is enough time to give it to somebody
+else. The system does *not* take things like office hours into account
+when calculating the offer time, that has to be done manually.
+
+Offers should always be given from the top of the waitlist. This will
+make sure that those who signed up early gets a chance first. The
+exception to this may be when different types of registrations can
+have different limits -- for example, a training attendee might get
+priority.
+
+When the offer is extended to a attendee, they will be sent an email and
+can complete the registration. If they complete it, they become a
+registered attendee like any other with no difference. If they decline
+it, or if the timer expires, their registration is returned back to
+the waitlist. If this happens it will go back to the *bottom* of the
+waitlist, and any other attendees on the waitlist will get another
+chance.
+
+The values at the top of the waitlist dashboard that are most useful
+when managing the waitlist are:
+
+
+Pending registrations with (invoice/bulk payment)
+: Number of attendees that are far enough into the process to have an
+invoice or a bulk payment generated. These can usually be expected to
+complete their payments and take their spot.
+
+Active offers on the waitlist
+: Number of attendees that have been given an offer, but have not
+completed it yet. This includes both those that have generated an
+invoice and those that have not.
+
+Total registrations including offered and invoiced
+: If all active offers are taken and completed, this is the number of
+attendees it will result in. This is typically the number to work off
+-- as long as this number is lower than the venue capacity, more
+offers can be given.
+
+## Waitlist workflow
+
+![Waitlist workflow](graphs/waitlist.svg)
diff --git a/docs/confreg/wiki.md b/docs/confreg/wiki.md
new file mode 100644 (file)
index 0000000..d5f3aa5
--- /dev/null
@@ -0,0 +1,76 @@
+# Wiki pages
+
+The registration system comes with a very simple wiki system for
+editing pages. It comes with a permissions system that makes it
+possible to use this system to publish text pages that are supposed to
+be viewable only by attendees (by restricting who can edit the page),
+or act as a "true wiki" to let attendees edit the pages. No pages can
+ever be viewed by users who are not confirmed attendees of the conference.
+
+Attendees can never create new pages, they must always be created by
+an administrator. But once created, attendees can edit them, if
+permissions allow.
+
+In the admin interface, the following fields can be set:
+
+url
+: This is the URL name of the page, *not* the complete URL. It can
+only contain alpha-numerical, underscore and dash. Unlike many other
+wikis, this is *not* derived from the page title.
+
+Title
+: This is the title of the page, used in page listings and in the
+title of the page itself. Can contain (almost) any characters.
+
+Author
+: Each page has an author that is the *registration* of the creator of
+the page. This is never shown to attendees, only administrator
+(however, mind that any *editing* of the page will show up).
+
+Contents
+: The initial contents of the page. Markdown is supported. Note that
+pages should normally *not* be edited here once they have been
+created, because that will edit the last revision *without* creating a
+new revision! Instead, edit the pages through the user interface,
+reached from the link *regular editor* above the form.
+
+Public view
+: If all *registered* attendees can view this page (fully public to
+non-registered attendees not supported).
+
+Public edit
+: If all *registered* attendees can edit this page.
+
+History
+: If attendees that have permissions to view the page (through it
+being public or through explicit permissions) can view the history of
+changes on the page, including who made them and the viewing of a
+diff.
+
+Viewer registration types
+: Registrations types that can view this page (if it is not public),
+for example if a page should be restricted to speakers.
+
+Editor registration types
+: Registration types that can *edit* this page (if it is not public).
+
+Viewer attendees
+: Specific attendees that can view this page (if it is not public
+*and* they are not registered using a registration type that provides
+access)
+
+Editor attendees
+: Specific attendees that can *edit* this page.
+
+## Wiki page subscriptions
+
+From the user interface, it is possible to *subscribe* to a wiki
+page. If this is done, the attendee will get an email on any changes
+made in the *user interface*. Changes made through the admin interface
+do *not* trigger emails.
+
+## Wiki page notifications
+
+New wiki pages and edits *in the backend system* will generate an
+email to the conference contact address (but not to regular attendees,
+subscribed or not).
index 3f7da1336e56275867df576a0a918accbf1eb082..a96103c02f075409c71ce27c35b5e2c8092213f9 100644 (file)
@@ -5,3 +5,7 @@ div.buttonrow .btn {
 div.backendform_checkbox ul {
     list-style-type: none;
 }
+
+div.backenddocs dd {
+    margin-left: 2em;
+}
index 12e606b8576d261d1cbf90f15bd068edb8eb8eaf..5a975f966901fa8ab700988a917fc039597952e7 100644 (file)
@@ -47,6 +47,7 @@ class BackendForm(ConcurrentProtectedModelForm):
        linked_objects = {}
        auto_cascade_delete_to = []
        fieldsets = []
+       helplink = None
 
        def __init__(self, conference, *args, **kwargs):
                self.conference = conference
@@ -150,6 +151,7 @@ class BackendForm(ConcurrentProtectedModelForm):
                return self[name]
 
 class BackendConferenceForm(BackendForm):
+       helplink = 'configuring#conferenceform'
        class Meta:
                model = Conference
                fields = ['active', 'callforpapersopen', 'callforsponsorsopen', 'feedbackopen',
@@ -179,6 +181,7 @@ class BackendConferenceForm(BackendForm):
        ]
 
 class BackendSuperConferenceForm(BackendForm):
+       helplink = 'super_conference#conferenceform'
        class Meta:
                model = Conference
                fields = ['conferencename', 'urlname', 'series', 'startdate', 'enddate', 'location',
@@ -203,12 +206,14 @@ class BackendSuperConferenceForm(BackendForm):
 
 
 class BackendConferenceSeriesForm(BackendForm):
+       helplink = "series"
        list_fields = ['name', ]
        class Meta:
                model = ConferenceSeries
                fields = ['name', ]
 
 class BackendRegistrationForm(BackendForm):
+       helplink = "registrations"
        class Meta:
                model = ConferenceRegistration
                fields = ['firstname', 'lastname', 'company', 'address', 'country', 'phone', 'shirtsize', 'dietary', 'twittername', 'nick', 'shareemail']
@@ -223,6 +228,7 @@ class BackendRegistrationForm(BackendForm):
                self.update_protected_fields()
 
 class BackendRegistrationClassForm(BackendForm):
+       helplink = 'registrations#regclasses'
        list_fields = ['regclass', 'badgecolor', 'badgeforegroundcolor']
        allow_copy_previous = True
        class Meta:
@@ -243,6 +249,7 @@ class BackendRegistrationClassForm(BackendForm):
                                                                  badgeforegroundcolor=source.badgeforegroundcolor).save()
 
 class BackendRegistrationTypeForm(BackendForm):
+       helplink = 'registrations#regtypes'
        list_fields = ['regtype', 'regclass', 'cost', 'active', 'sortkey']
        vat_fields = {'cost': 'reg'}
        allow_copy_previous = True
@@ -311,12 +318,14 @@ class BackendRegistrationTypeForm(BackendForm):
                                                source.regclass.regclass, source.regtype)
 
 class BackendRegistrationDayForm(BackendForm):
+       helplink = 'registrations#days'
        list_fields = [ 'day', ]
        class Meta:
                model = RegistrationDay
                fields = ['day', ]
 
 class BackendAdditionalOptionForm(BackendForm):
+       helplink = 'registrations#additionaloptions'
        list_fields = ['name', 'cost', 'maxcount', ]
        vat_fields = {'cost': 'reg'}
        auto_cascade_delete_to = ['registrationtype_requires_option', 'conferenceadditionaloption_requires_regtype',
@@ -334,6 +343,7 @@ class BackendAdditionalOptionForm(BackendForm):
                self.fields['mutually_exclusive'].queryset = ConferenceAdditionalOption.objects.filter(conference=self.conference).exclude(pk=self.instance.pk)
 
 class BackendTrackForm(BackendForm):
+       helplink = 'schedule#tracks'
        list_fields = ['trackname', 'sortkey']
        allow_copy_previous = True
        class Meta:
@@ -359,6 +369,7 @@ class BackendTrackForm(BackendForm):
                                ).save()
 
 class BackendRoomForm(BackendForm):
+       helplink = 'schedule#rooms'
        list_fields = ['roomname', 'sortkey']
        class Meta:
                model = Room
@@ -381,6 +392,7 @@ class BackendTransformConferenceDateTimeForm(django.forms.Form):
 
 
 class BackendConferenceSessionForm(BackendForm):
+       helplink = 'schedule#sessions'
        list_fields = [ 'title', 'speaker_list', 'status_string', 'starttime', 'track', 'room']
        verbose_field_names = {
                'speaker_list': 'Speakers',
@@ -489,6 +501,7 @@ class BackendConferenceSessionForm(BackendForm):
                return None
 
 class BackendConferenceSessionSlotForm(BackendForm):
+       helplink = 'schedule#slots'
        list_fields = [ 'starttime', 'endtime', ]
        allow_copy_previous = True
        copy_transform_form = BackendTransformConferenceDateTimeForm
@@ -518,6 +531,7 @@ class BackendConferenceSessionSlotForm(BackendForm):
 
 
 class BackendVolunteerSlotForm(BackendForm):
+       helplink = 'volunteers#slots'
        list_fields = [ 'timerange', 'title', 'min_staff', 'max_staff' ]
        allow_copy_previous = True
        copy_transform_form = BackendTransformConferenceDateTimeForm
@@ -564,6 +578,7 @@ class BackendVolunteerSlotForm(BackendForm):
                )
 
 class BackendFeedbackQuestionForm(BackendForm):
+       helplink = 'feedback#conference'
        list_fields = ['newfieldset', 'question', 'sortkey',]
        allow_copy_previous = True
 
@@ -599,12 +614,14 @@ class BackendFeedbackQuestionForm(BackendForm):
 
 
 class BackendNewDiscountCodeForm(django.forms.Form):
+       helplink='vouchers#discountcodes'
        codetype = django.forms.ChoiceField(choices=((1, 'Fixed amount discount'), (2, 'Percentage discount')))
 
        def get_newform_data(self):
                return self.cleaned_data['codetype']
 
 class BackendDiscountCodeForm(BackendForm):
+       helplink='vouchers#discountcodes'
        list_fields = ['code', 'validuntil', 'maxuses']
 
        form_before_new = BackendNewDiscountCodeForm
@@ -643,6 +660,7 @@ class BackendDiscountCodeForm(BackendForm):
 
 
 class BackendAccessTokenForm(BackendForm):
+       helplink = 'tokens'
        list_fields = ['token', 'description', 'permissions', ]
        readonly_fields = ['token', ]
 
index 985be3f795d4b25d84ecf9f6e7ed6c6f6e40fddb..41cdc0be1bfb6d1bdadc3cb8bae4760cda3d7bf4 100644 (file)
@@ -77,6 +77,7 @@ def backend_process_form(request, urlname, formclass, id, cancel_url='../', save
                                                'form': newform,
                                                'what': 'New {0}'.format(formclass.Meta.model._meta.verbose_name),
                                                'cancelurl': cancel_url,
+                                               'helplink': newform.helplink,
                                                'breadcrumbs': breadcrumbs,
                                        })
                                instance = instancemaker()
@@ -163,6 +164,7 @@ def backend_process_form(request, urlname, formclass, id, cancel_url='../', save
                'what': formclass.Meta.model._meta.verbose_name,
                'cancelurl': cancel_url,
                'breadcrumbs': breadcrumbs,
+               'helplink': form.helplink,
                'allow_delete': allow_delete and instance.pk,
                'adminurl': adminurl,
                'linked': [(url, handler, handler.get_list(form.instance)) for url, handler in form.linked_objects.items() if form.instance],
@@ -184,6 +186,7 @@ def backend_handle_copy_previous(request, formclass, restpieces, conference):
                        'savebutton': 'Copy',
                        'cancelurl': '../',
                        'breadcrumbs': [('../', formclass.Meta.model._meta.verbose_name_plural.capitalize()), ],
+                       'helplink': formclass.helplink,
                })
        elif len(restpieces) == 2:
                idlist = None
@@ -257,6 +260,7 @@ def backend_handle_copy_previous(request, formclass, restpieces, conference):
                                ('../../', formclass.Meta.model._meta.verbose_name_plural.capitalize()),
                                ('../', 'Copy {0}'.format(formclass.Meta.model._meta.verbose_name_plural.capitalize())),
                        ],
+                       'helplink': formclass.helplink,
                })
 
 
@@ -286,6 +290,7 @@ def backend_list_editor(request, urlname, formclass, resturl, allow_new=True, al
                        'allow_delete': allow_delete,
                        'allow_copy_previous': formclass.allow_copy_previous,
                        'breadcrumbs': breadcrumbs,
+                       'helplink': formclass.helplink,
                })
 
        if allow_new and resturl=='new':
@@ -528,6 +533,7 @@ def purge_personal_data(request, urlname):
 
        return render(request, 'confreg/admin_purge_personal_data.html', {
                'conference': conference,
+               'helplink': 'personaldata',
                'counts': exec_to_dict("""SELECT
   count(1) FILTER (WHERE shirtsize_id IS NOT NULL) AS "T-shirt size registrations",
   count(1) FILTER (WHERE dietary IS NOT NULL AND dietary != '') AS "Dietary needs",
diff --git a/postgresqleu/confreg/docsviews.py b/postgresqleu/confreg/docsviews.py
new file mode 100644 (file)
index 0000000..a033421
--- /dev/null
@@ -0,0 +1,68 @@
+from django.shortcuts import render
+from django.http import HttpResponseForbidden, Http404
+
+import codecs
+import os
+import re
+import markdown
+
+from models import Conference
+from backendviews import get_authenticated_conference
+
+reTitle = re.compile('<h1>([^<]+)</h1>')
+
+_reSvgInline = re.compile('<img alt="([^"]+)" src="([^"]+)\.svg" />')
+def _replaceSvgInline(m):
+       # Group 1 = alt text
+       # Group 2 = filename excluding svg
+       filename = 'docs/confreg/{0}.svg'.format(m.group(2))
+       if not os.path.isfile(filename):
+               return m.group(0)
+
+       with codecs.open(filename, 'r', 'utf8') as f:
+               return f.read()
+
+def docspage(request, urlname, page):
+       if urlname:
+               conference = get_authenticated_conference(request, urlname.rstrip('/'))
+       else:
+               # Allow a person who has *any* permissions on a conference to read the docs,
+               # because, well, they are docs.
+               if not request.user.is_superuser:
+                       if not Conference.objects.filter(administrators=request.user).exists():
+                               return HttpResponseForbidden("Access denied")
+               conference = None
+
+       if page:
+               page = page.rstrip('/')
+               urlpage = page
+       else:
+               page = "index"
+               urlpage = ''
+
+       # Do we have the actual docs file?
+       # It's safe to just put the filename in here, because the regexp in urls.py ensures
+       # that we can not get into a path traversal case.
+       filename = 'docs/confreg/{0}.md'.format(page)
+       if not os.path.isfile(filename):
+               raise Http404()
+
+       with open(filename) as f:
+               md = markdown.Markdown(extensions=['markdown.extensions.def_list'])
+               contents = md.convert(f.read())
+       contents = _reSvgInline.sub(lambda m: _replaceSvgInline(m), contents)
+
+       # Find the title
+       m = reTitle.search(contents)
+       if m:
+               title = m.group(1)
+       else:
+               title = 'PostgreSQL Europe Conference Administration'
+
+       return render(request, 'confreg/admin_backend_docpage.html', {
+               'conference': conference,
+               'page': page,
+               'contents': contents,
+               'title': title,
+               'urlpage': urlpage,
+       })
index dc87cb2f74cf59341f7b369e72a1757226dee01f..70f452ae622a80bc8b64b894c5f6f5dd1ba0360c 100644 (file)
@@ -50,6 +50,7 @@ def feedback_report(request, confname):
        return render(request, 'confreg/admin_conference_feedback.html', {
                'conference': conference,
                'feedback': sections,
+               'helplink': 'feedback',
        })
 
 
@@ -89,4 +90,5 @@ def feedback_sessions(request, confname):
                'minvotes': minvotes,
                'commented_sessions': commented_sessions,
                'breadcrumbs': (('/events/admin/{0}/reports/feedback/'.format(conference.urlname), 'Feedback'), ),
+               'helplink': 'feedback',
        })
index 3cc5bf3ecb73194583766251cdab49d39d0da809..a1e909b322a81894b522cffa5a99c13453f18dcc 100644 (file)
@@ -362,4 +362,5 @@ def pdfschedule(request, confname):
        return render(request, 'confreg/pdfschedule.html', {
                'conference': conference,
                'form': form,
+               'helplink': 'schedule#pdf',
        })
index d2f9873b981da9bc603b355dfcec25689a237052..bfd392ba0c49332c6915eeae4b36060c25e6a979 100644 (file)
@@ -37,6 +37,7 @@ def timereport(request):
                                        'maxpred': report.maxpred,
                                        'trendlines': report.does_trendlines and trendlines or '',
                                        'trendlines_supported': report.does_trendlines,
+                                       'helplink': 'reports#time',
                                        })
                        except ReportException, e:
                                messages.error(request, e)
@@ -48,6 +49,7 @@ def timereport(request):
 
        return render(request, 'confreg/timereport.html', {
                'form': form,
+               'helplink': 'reports#time',
                })
 
 
diff --git a/postgresqleu/confreg/templatetags/docslink.py b/postgresqleu/confreg/templatetags/docslink.py
new file mode 100644 (file)
index 0000000..f816979
--- /dev/null
@@ -0,0 +1,11 @@
+from django.template.defaultfilters import stringfilter
+from django import template
+
+register = template.Library()
+
+@register.filter(name='docslink')
+@stringfilter
+def docslink(value):
+       if '#' in value:
+               return '{0}/#{1}'.format(*value.split('#'))
+       return value + '/'
index 84873cd19680e941e95a615f15f32a506a394f68..19f3b7b215fc0f88d65a54430ef187c83e2e71f2 100644 (file)
@@ -1871,6 +1871,7 @@ def listvouchers(request, confname):
        return render(request, 'confreg/prepaid_list.html', {
                'conference': conference,
                'batches': batches,
+               'helplink': 'vouchers',
        })
 
 def viewvouchers(request, confname, batchid):
@@ -1891,6 +1892,7 @@ def viewvouchers(request, confname, batchid):
                'vouchers': vouchers,
                'vouchermailtext': vouchermailtext,
                'breadcrumbs': (('/events/admin/{0}/prepaid/list/'.format(conference.urlname), 'Prepaid vouchers'),),
+               'helplink': 'vouchers',
        })
 
 @transaction.atomic
@@ -2176,6 +2178,7 @@ def talkvote(request, confname):
                        'conference': conference,
                        'isadmin': isadmin,
                    'status_choices': STATUS_CHOICES,
+                       'helplink': 'callforpapers',
                        })
 
 @login_required
@@ -2291,6 +2294,7 @@ def createschedule(request, confname):
                        'tracks': tracks,
                        'sesswidth': min(600 / len(allrooms), 150),
                        'availableheight': len(sessions)*75,
+                       'helplink': 'schedule',
                        })
 
 @login_required
@@ -2352,6 +2356,7 @@ def reports(request, confname):
                        'additionaloptions': conference.conferenceadditionaloption_set.all(),
                        'adv_fields': attendee_report_fields,
                        'adv_filters': attendee_report_filters(conference),
+                       'helplink': 'reports#attendee',
                    })
 
 
@@ -2398,6 +2403,7 @@ def simple_report(request, confname):
                'conference': conference,
                'columns': [dd for dd in collist if not dd.startswith('_')],
                'data': d,
+               'helplink': 'reports',
        })
 
 @login_required
@@ -2493,6 +2499,7 @@ def admin_registration_dashboard(request, urlname):
        return render(request, 'confreg/admin_registration_dashboard.html', {
                'conference': conference,
                'tables': tables,
+               'helplink': 'registrations',
        })
 
 def admin_registration_list(request, urlname):
@@ -2522,6 +2529,7 @@ def admin_registration_list(request, urlname):
                'regs': ConferenceRegistration.objects.select_related('regtype').select_related('registrationwaitlistentry').filter(conference=conference).order_by((revsort and '-' or '') + sortmap[skey], '-created'),
                'regsummary': exec_to_dict("SELECT count(1) FILTER (WHERE payconfirmedat IS NOT NULL) AS confirmed, count(1) FILTER (WHERE payconfirmedat IS NULL) AS unconfirmed FROM confreg_conferenceregistration WHERE conference_id=%(confid)s", {'confid': conference.id})[0],
                'breadcrumbs': (('/events/admin/{0}/regdashboard/'.format(urlname), 'Registration dashboard'),),
+               'helplink': 'registrations',
        })
 
 def admin_registration_single(request, urlname, regid):
@@ -2542,6 +2550,7 @@ def admin_registration_single(request, urlname, regid):
                        ('/events/admin/{0}/regdashboard/'.format(urlname), 'Registration dashboard'),
                        ('/events/admin/{0}/regdashboard/list/'.format(urlname), 'Registration list'),
                ),
+               'helplink': 'registrations',
        })
 
 @transaction.atomic
@@ -2561,6 +2570,7 @@ def admin_registration_cancel(request, urlname, regid):
                return render(request, 'confreg/admin_registration_cancel.html', {
                        'conference': conference,
                        'reg': reg,
+                       'helplink': 'waitlist',
                })
 
 @transaction.atomic
@@ -2570,6 +2580,7 @@ def admin_waitlist(request, urlname):
        if conference.attendees_before_waitlist <= 0:
                return render(request, 'confreg/admin_waitlist_inactive.html', {
                        'conference': conference,
+                       'helplink': 'waitlist',
                        })
 
        num_confirmedregs = ConferenceRegistration.objects.filter(conference=conference, payconfirmedat__isnull=False).count()
@@ -2625,6 +2636,7 @@ def admin_waitlist(request, urlname):
                'waitlist': waitlist,
                'waitlist_cleared': waitlist_cleared,
                'form': form,
+               'helplink': 'waitlist',
                })
 
 @transaction.atomic
@@ -2694,6 +2706,7 @@ def admin_attendeemail(request, urlname):
                'conference': conference,
                'mails': mails,
                'form': form,
+               'helplink': 'emails',
        })
 
 def admin_attendeemail_view(request, urlname, mailid):
@@ -2705,6 +2718,7 @@ def admin_attendeemail_view(request, urlname, mailid):
                'conference': conference,
                'mail': mail,
                'breadcrumbs': (('/events/admin/{0}/mail/'.format(conference.urlname), 'Attendee emails'), ),
+               'helplink': 'emails',
                })
 
 @transaction.atomic
@@ -2900,6 +2914,7 @@ def transfer_reg(request, urlname):
                'form': form,
                'steps': steps,
                'stephash': stephash,
+               'helplink': 'registrations#transfer',
        })
 
 
@@ -2984,6 +2999,7 @@ def crossmail(request):
                'form': form,
                'recipients': recipients,
                'conferences': Conference.objects.all(),
+               'helplink': 'emails#crossconference',
                })
 
 
index 51631df835686ed0d0001231b2d12d1f933b79e1..1a17cc0dfffccf27e498677f11a62be0dddc649a 100644 (file)
@@ -191,6 +191,7 @@ def admin(request, urlname):
        return render(request, 'confwiki/admin.html', {
                'conference': conference,
                'pages': pages,
+               'helplink': 'wiki',
        })
 
 @transaction.atomic
@@ -245,6 +246,7 @@ def admin_edit_page(request, urlname, pageid):
                'form': form,
                'page': page,
                'breadcrumbs': (('/events/admin/{0}/wiki/'.format(conference.urlname), 'Wiki'),),
+               'helplink': 'wiki',
        })
 
 
@@ -325,6 +327,7 @@ def signup_admin(request, urlname):
        return render(request, 'confwiki/signup_admin.html', {
                'conference': conference,
                'signups': signups,
+               'helplink': 'signups',
        })
 
 @transaction.atomic
@@ -382,6 +385,7 @@ def signup_admin_edit(request, urlname, signupid):
                'signup': signup,
                'results': results,
                'breadcrumbs': (('/events/admin/{0}/signups/'.format(conference.urlname), 'Signups'),),
+               'helplink': 'signups',
        })
 
 
@@ -458,4 +462,5 @@ def signup_admin_sendmail(request, urlname, signupid):
                        ('/events/admin/{0}/signups/'.format(conference.urlname), 'Signups'),
                        ('/events/admin/{0}/signups/{1}/'.format(conference.urlname, signup.id), signup.title),
                ),
+               'helplink': 'signups',
        })
index 9306a27acaca4da52ff31c4395dfb53b2c381b70..4b05d372a787aa320be10de07e260f6f6518cea3 100644 (file)
@@ -13,6 +13,7 @@ import postgresqleu.confreg.mobileviews
 import postgresqleu.confreg.feedback
 import postgresqleu.confreg.pdfschedule
 import postgresqleu.confreg.volsched
+import postgresqleu.confreg.docsviews
 import postgresqleu.confwiki.views
 import postgresqleu.membership.views
 import postgresqleu.elections.views
@@ -107,6 +108,7 @@ urlpatterns = [
        url(r'^events/admin/crossmail/$', postgresqleu.confreg.views.crossmail),
        url(r'^events/admin/crossmail/options/$', postgresqleu.confreg.views.crossmailoptions),
        url(r'^events/admin/reports/time/$', postgresqleu.confreg.reporting.timereport),
+       url(r'^events/admin/(?P<urlname>[^/]+/)?docs/(?P<page>\w+/)?$', postgresqleu.confreg.docsviews.docspage),
        url(r'^events/admin/([^/]+)/reports/$', postgresqleu.confreg.views.reports),
        url(r'^events/admin/([^/]+)/reports/simple/$', postgresqleu.confreg.views.simple_report),
        url(r'^events/admin/([^/]+)/reports/advanced/$', postgresqleu.confreg.views.advanced_report),
diff --git a/template/confreg/admin_backend_docpage.html b/template/confreg/admin_backend_docpage.html
new file mode 100644 (file)
index 0000000..69453a5
--- /dev/null
@@ -0,0 +1,10 @@
+{%extends "confreg/confadmin_base.html"%}
+{%block title%}{{title}}{%endblock%}
+{%block extrahead%}
+<base href="/events/admin/{%if conference and conference.urlname%}{{conference.urlname}}/{%endif%}docs/{%if urlpage%}{{urlpage}}{%endif%}">
+{%endblock%}
+{%block layoutblock%}
+<div class="backenddocs">
+{{contents|safe}}
+</div>
+{%endblock%}
index 0cfa4d0afa953f6ad0308ad094d080ddb5d406d6..57cce6da6511b6bd1ed694551f85bce7d73225f0 100644 (file)
@@ -1,4 +1,5 @@
 {%load alertmap%}
+{%load docslink%}
 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
     "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
 <html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en" dir="ltr">
@@ -34,6 +35,9 @@
           <li><a href="{{k}}">{{v}}</a></li>
           {%endfor%}
         </ul>
+        <ul class="nav navbar-nav navbar-right">
+          <li><a href="/events/admin/{%if conference and conference.urlname%}{{conference.urlname}}/{%endif%}docs/{%if helplink%}{{helplink|docslink}}{%endif%}">Help</a></li>
+        </ul>
        </div>
      </div>
    </nav>