Merge lp:~sinzui/launchpad/sprint-attendence-1 into lp:launchpad/db-devel

Proposed by Curtis Hovey
Status: Merged
Approved by: Curtis Hovey
Approved revision: no longer in the source branch.
Merged at revision: not available
Proposed branch: lp:~sinzui/launchpad/sprint-attendence-1
Merge into: lp:launchpad/db-devel
Diff against target: 726 lines (+361/-60)
15 files modified
lib/canonical/launchpad/doc/launchpad-radio-widget.txt (+48/-0)
lib/canonical/widgets/itemswidgets.py (+40/-0)
lib/lp/blueprints/browser/sprint.py (+5/-2)
lib/lp/blueprints/browser/sprintattendance.py (+16/-11)
lib/lp/blueprints/browser/tests/sprintattendance-views.txt (+126/-26)
lib/lp/blueprints/browser/tests/test_views.py (+2/-2)
lib/lp/blueprints/doc/sprint-meeting-export.txt (+1/-1)
lib/lp/blueprints/doc/sprint.txt (+48/-0)
lib/lp/blueprints/doc/sprintattendance.txt (+60/-0)
lib/lp/blueprints/interfaces/sprint.py (+1/-1)
lib/lp/blueprints/interfaces/sprintattendance.py (+7/-2)
lib/lp/blueprints/model/sprint.py (+4/-2)
lib/lp/blueprints/model/sprintattendance.py (+2/-4)
lib/lp/blueprints/stories/sprints/20-sprint-registration.txt (+0/-6)
lib/lp/blueprints/tests/test_doc.py (+1/-3)
To merge this branch: bzr merge lp:~sinzui/launchpad/sprint-attendence-1
Reviewer Review Type Date Requested Status
Martin Albisetti (community) ui Approve
Abel Deuring (community) code Approve
Canonical Launchpad Engineering code an ui Pending
Review via email: mp+14184@code.launchpad.net
To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :
Download full text (4.1 KiB)

This is my branch to expose SprintAttendance.is_physical in the UI.
Most of this branch adds missing test coverage.
See http://people.canonical.com/~curtis/is-physical.png for an
example of the yes-no widget.

    lp:~sinzui/launchpad/sprint-attendence-1
    Diff size: 663
    Launchpad bug: https://bugs.launchpad.net/bugs/461945
    Test command: ./bin/test -vv -t sprint -t launchpad-radio-widget
    Pre-implementation: no one
    Target release: 3.1.10

= Expose SprintAttendance.is_physical in the UI =

SprintAttendence has an is_physical column. The interface, model, view, and
CSV report must use it.

== Rules ==

    * Update the interface and model.
    * Add it to the base class for adding it to a sprint attendance
    * Update the CSV export to use it.

ADDENDUM
    * There were a lot of basic tests that had to be added before any
      code changes could be made
    * The test were running on the wrong layer.

== QA ==

Visit a sprint on staging
    * Register yourself for the sprint, Choose no, you will be physically
      there.
    * Export the list of attendees.
    * Verify that your entry has True listed in the the sprint.

== Lint ==

Linting changed files:
  lib/canonical/launchpad/doc/launchpad-radio-widget.txt
  lib/canonical/widgets/itemswidgets.py
  lib/lp/blueprints/browser/sprint.py
  lib/lp/blueprints/browser/sprintattendance.py
  lib/lp/blueprints/browser/tests/sprintattendance-views.txt
  lib/lp/blueprints/browser/tests/test_views.py
  lib/lp/blueprints/doc/sprint-meeting-export.txt
  lib/lp/blueprints/doc/sprint.txt
  lib/lp/blueprints/doc/sprintattendance.txt
  lib/lp/blueprints/interfaces/sprint.py
  lib/lp/blueprints/interfaces/sprintattendance.py
  lib/lp/blueprints/model/sprint.py
  lib/lp/blueprints/model/sprintattendance.py
  lib/lp/blueprints/tests/test_doc.py

== Test ==

    * lib/canonical/launchpad/doc/launchpad-radio-widget.txt
      * Added test to verify that LaunchpadBooleanRadioWidget renders a
        yes-no radio widget for a boolean field,
    * lib/lp/blueprints/browser/tests/sprintattendance-views.txt
      * Updated the tests to use the common helpers.
        I will fix the bad indentation after I create the MP so that the
        changes are easier to read.
      * Added a test for Physical attendance.
      * Added coverage of how the child +attend and +register views differ.
      * Updated the CSV test to show that is_physical is exported.
    * lib/lp/blueprints/browser/tests/test_views.py
      * Run the tests on the DatabaseFunctionalLayer
    * lib/lp/blueprints/doc/sprint-meeting-export.txt
      * Updated the attend() call to pass an is_physical argument.
    * lib/lp/blueprints/doc/sprint.txt
      * Added missing coverage about how the attend() method works.
      * Verified that is_physical is accepted as an argument.
    * lib/lp/blueprints/doc/sprintattendance.txt
      * Added missing test coverage for how the SprintAttendance object works.
      * Verified that the is_physical property is supported.
    * lib/lp/blueprints/tests/test_doc.py
      * Run the tests on the DatabaseFunctionalLayer

== Implementation ==

    * lib/canonical/widgets/itemswidgets.py
      * C...

Read more...

Revision history for this message
Abel Deuring (adeuring) wrote :

Hi Curits,

a nice branch, just two minor nitpicks, see below.

Abel

> === modified file 'lib/lp/blueprints/browser/sprint.py'
> --- lib/lp/blueprints/browser/sprint.py 2009-09-23 14:04:31 +0000
> +++ lib/lp/blueprints/browser/sprint.py 2009-10-29 17:05:26 +0000
> @@ -518,7 +518,9 @@
> 'Country',
> 'Timezone',
> 'Arriving',
> - 'Leaving')]
> + 'Leaving',
> + 'Physical present',

English is not my native language, so I may be wrong, but shouldn't
this be "Physically present"?

[...]

> === modified file 'lib/lp/blueprints/browser/tests/sprintattendance-views.txt'
> --- lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2009-05-11 18:19:21 +0000
> +++ lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2009-10-29 17:05:26 +0000

[...]

> +The +register views
> +-------------------------------

I think you can remove a few '-' in the line above.

review: Approve (code)
Revision history for this message
Curtis Hovey (sinzui) wrote :

Hi Martin.

I invented a yes/no widget to show boolean fields the launchpad way. Is this the launchpad way: http://people.canonical.com/~curtis/is-physical.png

If we like the widget, we can remove some custom code in bugs and code by using a single line to enable this widget.

Revision history for this message
Martin Albisetti (beuno) wrote :

What do you think about instead of a yes/no question, we do something like:

How will you be attending?
o Remotely
o Physically

review: Needs Fixing (ui)
Revision history for this message
Curtis Hovey (sinzui) wrote :

> What do you think about instead of a yes/no question, we do something like:
>
> How will you be attending?
> o Remotely
> o Physically

Done. http://people.canonical.com/~curtis/boolean-radio-widget.png

We now have an easy way to present boolean values are comprehensible choices in the UI:
    custom_widget(
        'is_physical', LaunchpadBooleanRadioWidget, orientation='vertical',
        true_label="Physically", false_label="Remotely")

Revision history for this message
Martin Albisetti (beuno) wrote :

You're my hero.
You already got the bonus points, but if you want to enter the hall of fame, you could drop de ":" after the question mark :)

review: Approve (ui)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/canonical/launchpad/doc/launchpad-radio-widget.txt'
2--- lib/canonical/launchpad/doc/launchpad-radio-widget.txt 2009-06-15 21:05:06 +0000
3+++ lib/canonical/launchpad/doc/launchpad-radio-widget.txt 2009-11-06 03:05:24 +0000
4@@ -110,3 +110,51 @@
5 </table>
6 <input name="field.branch_type-empty-marker" type="hidden" value="1" />
7
8+
9+LaunchpadBooleanRadioWidget
10+---------------------------
11+
12+The LaunchpadBooleanRadioWidget renders a boolean field as radio buttons.
13+This widget is uses the LaunchpadRadioWidget to render the items. The values
14+are rendered as 'yes' and 'no'; a missing value radio item is not rendered.
15+
16+ >>> from zope.schema import Bool
17+ >>> from canonical.widgets import LaunchpadBooleanRadioWidget
18+
19+ >>> field = Bool(
20+ ... __name__='sentient',
21+ ... title=u"Are you sentient?",
22+ ... description=u"Are you human or a bot?",
23+ ... required=False, readonly=False, default=True)
24+
25+ >>> class Agent:
26+ ... def __init__(self, sentient):
27+ ... self.sentient = sentient
28+
29+ >>> agent = Agent(True)
30+ >>> bound_field = field.bind(agent)
31+ >>> radio_widget = LaunchpadBooleanRadioWidget(bound_field, request)
32+ >>> print radio_widget()
33+ <label style="font-weight: normal"><input
34+ class="radioType" checked="checked" id="field.sentient.0"
35+ name="field.sentient" type="radio" value="yes"
36+ />&nbsp;yes</label><br
37+ /><label style="font-weight: normal"><input
38+ class="radioType" id="field.sentient.1" name="field.sentient"
39+ type="radio" value="no" />&nbsp;no</label>
40+ <input name="field.sentient-empty-marker" type="hidden" value="1" />
41+
42+The labels for True and False values can be set using the true_label and
43+false_label attributes.
44+
45+ >>> radio_widget.true_label = 'I think therefore I am'
46+ >>> radio_widget.false_label = 'I am a turing test'
47+ >>> print radio_widget()
48+ <label style="font-weight: normal"><input
49+ class="radioType" checked="checked" id="field.sentient.0"
50+ name="field.sentient" type="radio" value="yes"
51+ />&nbsp;I think therefore I am</label><br
52+ /><label style="font-weight: normal"><input
53+ class="radioType" id="field.sentient.1" name="field.sentient"
54+ type="radio" value="no" />&nbsp;I am a turing test</label>
55+ <input name="field.sentient-empty-marker" type="hidden" value="1" />
56
57=== modified file 'lib/canonical/widgets/itemswidgets.py'
58--- lib/canonical/widgets/itemswidgets.py 2009-06-25 05:30:52 +0000
59+++ lib/canonical/widgets/itemswidgets.py 2009-11-06 03:05:24 +0000
60@@ -8,6 +8,7 @@
61 __all__ = [
62 'CheckBoxMatrixWidget',
63 'LabeledMultiCheckBoxWidget',
64+ 'LaunchpadBooleanRadioWidget',
65 'LaunchpadDropdownWidget',
66 'LaunchpadRadioWidget',
67 'LaunchpadRadioWidgetWithDescription',
68@@ -17,11 +18,14 @@
69 import math
70
71 from zope.schema.interfaces import IChoice
72+from zope.schema.vocabulary import SimpleVocabulary
73 from zope.app.form.browser import MultiCheckBoxWidget
74 from zope.app.form.browser.itemswidgets import DropdownWidget, RadioWidget
75 from zope.app.form.browser.widget import renderElement
76+
77 from lazr.enum import IEnumeratedType
78
79+
80 class LaunchpadDropdownWidget(DropdownWidget):
81 """A Choice widget that doesn't encloses itself in <div> tags."""
82
83@@ -181,6 +185,42 @@
84 % ''.join(rendered_items))
85
86
87+class LaunchpadBooleanRadioWidget(LaunchpadRadioWidget):
88+ """Render a Bool field as radio widget.
89+
90+ The `LaunchpadRadioWidget` does the rendering. Only the True-False values
91+ are rendered; a missing value item is not rendered. The default labels
92+ are rendered as 'yes' and 'no', but can be changed by setting the widget's
93+ true_label and false_label attributes.
94+ """
95+
96+ TRUE = 'yes'
97+ FALSE = 'no'
98+
99+ def __init__(self, field, request):
100+ """Initialize the widget."""
101+ vocabulary = SimpleVocabulary.fromItems(
102+ ((self.TRUE, True), (self.FALSE, False)))
103+ super(LaunchpadBooleanRadioWidget, self).__init__(
104+ field, vocabulary, request)
105+ # Suppress the missing value behaviour; this is a boolean field.
106+ self.required = True
107+ self._displayItemForMissingValue = False
108+ # Set the default labels for true and false values.
109+ self.true_label = 'yes'
110+ self.false_label = 'no'
111+
112+ def _renderItem(self, index, text, value, name, cssClass, checked=False):
113+ """Render the item with the preferred true and false labels."""
114+ if value == self.TRUE:
115+ text = self.true_label
116+ else:
117+ # value == self.FALSE.
118+ text = self.false_label
119+ return super(LaunchpadBooleanRadioWidget, self)._renderItem(
120+ index, text, value, name, cssClass, checked=checked)
121+
122+
123 class CheckBoxMatrixWidget(LabeledMultiCheckBoxWidget):
124 """A CheckBox widget which organizes the inputs in a grid.
125
126
127=== modified file 'lib/lp/blueprints/browser/sprint.py'
128--- lib/lp/blueprints/browser/sprint.py 2009-09-23 14:04:31 +0000
129+++ lib/lp/blueprints/browser/sprint.py 2009-11-06 03:05:24 +0000
130@@ -518,7 +518,9 @@
131 'Country',
132 'Timezone',
133 'Arriving',
134- 'Leaving')]
135+ 'Leaving',
136+ 'Physically present',
137+ )]
138 for attendance in self.context.attendances:
139 time_zone = ''
140 location = attendance.attendee.location
141@@ -542,7 +544,8 @@
142 country,
143 time_zone,
144 attendance.time_starts.strftime('%Y-%m-%dT%H:%M:%SZ'),
145- attendance.time_ends.strftime('%Y-%m-%dT%H:%M:%SZ')))
146+ attendance.time_ends.strftime('%Y-%m-%dT%H:%M:%SZ'),
147+ attendance.is_physical))
148 # CSV can't handle unicode, so we force encoding
149 # everything as UTF-8
150 rows = [[self.encode_value(column)
151
152=== modified file 'lib/lp/blueprints/browser/sprintattendance.py'
153--- lib/lp/blueprints/browser/sprintattendance.py 2009-09-21 12:39:15 +0000
154+++ lib/lp/blueprints/browser/sprintattendance.py 2009-11-06 03:05:24 +0000
155@@ -17,13 +17,18 @@
156 from canonical.launchpad.webapp import (
157 LaunchpadFormView, action, canonical_url, custom_widget)
158 from canonical.widgets.date import DateTimeWidget
159+from canonical.widgets.itemswidgets import LaunchpadBooleanRadioWidget
160
161
162 class BaseSprintAttendanceAddView(LaunchpadFormView):
163
164 schema = ISprintAttendance
165+ field_names = ['time_starts', 'time_ends', 'is_physical']
166 custom_widget('time_starts', DateTimeWidget)
167 custom_widget('time_ends', DateTimeWidget)
168+ custom_widget(
169+ 'is_physical', LaunchpadBooleanRadioWidget, orientation='vertical',
170+ true_label="Physically", false_label="Remotely", hint=None)
171
172 def setUpWidgets(self):
173 LaunchpadFormView.setUpWidgets(self)
174@@ -103,7 +108,10 @@
175 def next_url(self):
176 return canonical_url(self.context)
177
178+ cancel_url = next_url
179+
180 _local_timeformat = '%H:%M on %A, %Y-%m-%d'
181+
182 @property
183 def local_start(self):
184 """The sprint start time, in the local time zone, as text."""
185@@ -122,8 +130,6 @@
186 class SprintAttendanceAttendView(BaseSprintAttendanceAddView):
187 """A view used to register your attendance at a sprint."""
188
189- field_names = ['time_starts', 'time_ends']
190-
191 label = "Register your attendance"
192
193 @property
194@@ -132,7 +138,8 @@
195 for attendance in self.context.attendances:
196 if attendance.attendee == self.user:
197 return dict(time_starts=attendance.time_starts,
198- time_ends=attendance.time_ends)
199+ time_ends=attendance.time_ends,
200+ is_physical=attendance.is_physical)
201 # If this person is not yet registered, then default to showing the
202 # full sprint dates.
203 return {'time_starts': self.context.time_starts,
204@@ -141,19 +148,15 @@
205 @action(_('Register'), name='register')
206 def register_action(self, action, data):
207 time_starts, time_ends = self.getDates(data)
208- self.context.attend(self.user, time_starts, time_ends)
209-
210- @property
211- def cancel_url(self):
212- """Canceling goes back to the sprint page."""
213- return canonical_url(self.context)
214+ is_physical = data['is_physical']
215+ self.context.attend(self.user, time_starts, time_ends, is_physical)
216
217
218 class SprintAttendanceRegisterView(BaseSprintAttendanceAddView):
219 """A view used to register someone else's attendance at a sprint."""
220
221 label = 'Register someone else'
222- field_names = ['attendee', 'time_starts', 'time_ends']
223+ field_names = ['attendee'] + list(BaseSprintAttendanceAddView.field_names)
224
225 @property
226 def initial_values(self):
227@@ -164,4 +167,6 @@
228 @action(_('Register'), name='register')
229 def register_action(self, action, data):
230 time_starts, time_ends = self.getDates(data)
231- self.context.attend(data['attendee'], time_starts, time_ends)
232+ is_physical = data['is_physical']
233+ self.context.attend(
234+ data['attendee'], time_starts, time_ends, is_physical)
235
236=== modified file 'lib/lp/blueprints/browser/tests/sprintattendance-views.txt'
237--- lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2009-05-11 18:19:21 +0000
238+++ lib/lp/blueprints/browser/tests/sprintattendance-views.txt 2009-11-06 03:05:24 +0000
239@@ -1,30 +1,44 @@
240-= Sprint Attendance Page =
241+Sprint Attendance Pages
242+=======================
243
244 SprintAttendanceAddView is the view that handles attendance to meetings.
245-
246- >>> from zope.component import getMultiAdapter
247- >>> from canonical.launchpad.ftests import login
248- >>> from canonical.launchpad.interfaces import ISprintSet
249- >>> from canonical.launchpad.webapp.servers import LaunchpadTestRequest
250-
251-First we define a helper function.
252+The view descends from BaseSprintAttendanceAddView which provides date
253+handling.
254+
255+ >>> from lp.blueprints.browser.sprintattendance import (
256+ ... BaseSprintAttendanceAddView)
257+ >>> from canonical.launchpad.interfaces import ISprintSet
258+
259+ >>> ubz = getUtility(ISprintSet)['ubz']
260+ >>> sprint_attendance_view = create_view(ubz, name='+attend')
261+ >>> isinstance(sprint_attendance_view, BaseSprintAttendanceAddView)
262+ True
263+
264+The view captures the user's time_start and time_ends attendance.
265+
266+ >>> sprint_attendance_view.field_names
267+ ['time_starts', 'time_ends', 'is_physical']
268+
269+The view also defines a next_url and cancel_url.
270+
271+ >>> print sprint_attendance_view.next_url
272+ http://launchpad.dev/sprints/ubz
273+
274+ >>> print sprint_attendance_view.cancel_url
275+ http://launchpad.dev/sprints/ubz
276+
277+A helper function to test date handling.
278
279 >>> def create_sprint_attendance_view(sprint, dates):
280 ... time_starts, time_ends = dates
281 ... form = {
282 ... 'field.time_starts': time_starts,
283 ... 'field.time_ends': time_ends,
284+ ... 'field.is_physical': 'yes',
285 ... 'field.actions.register': 'Register'}
286- ... request = LaunchpadTestRequest(form=form)
287- ... request.method = 'POST'
288- ... view = getMultiAdapter((sprint, request), name='+attend')
289- ... return view
290-
291-Grab the sprint we'll need to test this view.
292-
293- >>> ubz = getUtility(ISprintSet)['ubz']
294-
295-This sprint doesn't have any attendees.
296+ ... return create_initialized_view(sprint, name='+attend', form=form)
297+
298+This sprint doesn't have any attendees. It dose have the required dates set.
299
300 >>> [attendee.name for attendee in ubz.attendees]
301 []
302@@ -39,7 +53,6 @@
303
304 >>> dates = ['2005-11-15', '2005-10-09']
305 >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
306- >>> sprint_attendance_view.initialize()
307 >>> print sprint_attendance_view.getFieldError('time_ends')
308 The end time must be after the start time.
309
310@@ -48,7 +61,6 @@
311
312 >>> dates = ['2006-01-01', '2006-02-01']
313 >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
314- >>> sprint_attendance_view.initialize()
315 >>> print sprint_attendance_view.getFieldError('time_starts')
316 Please pick a date before 2005-11-16 19:11
317
318@@ -57,7 +69,6 @@
319
320 >>> dates = ['2005-07-01', '2005-08-01']
321 >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
322- >>> sprint_attendance_view.initialize()
323 >>> print sprint_attendance_view.getFieldError('time_ends')
324 Please pick a date after 2005-10-07 19:30
325
326@@ -67,7 +78,6 @@
327
328 >>> dates = ['2005-10-07 09:00', '2005-11-17 19:05']
329 >>> sprint_attendance_view = create_sprint_attendance_view(ubz, dates)
330- >>> sprint_attendance_view.initialize()
331 >>> sprint_attendance_view.errors
332 []
333
334@@ -84,7 +94,95 @@
335 True
336
337
338-== Exporting the list of attendees ==
339+Physical attendance
340+-------------------
341+
342+The most common kind of attendance is that the user will be physically present
343+at the sprint.
344+
345+ >>> person = factory.makePerson(name='brown')
346+ >>> login_person(person)
347+ >>> form = {
348+ ... 'field.time_starts': '2005-10-07 09:00',
349+ ... 'field.time_ends': '2005-10-17 19:05',
350+ ... 'field.is_physical': 'yes',
351+ ... 'field.actions.register': 'Register'}
352+ >>> view = create_initialized_view(ubz, name='+attend', form=form)
353+ >>> view.errors
354+ []
355+
356+ >>> [sprint_attendance] = [attendance for attendance in ubz.attendances
357+ ... if attendance.attendee.name == 'brown']
358+ >>> sprint_attendance.is_physical
359+ True
360+
361+Some users attend the sprint virtually, such as via IRC, VOIP, or by using
362+their psychotic powers :).
363+
364+ >>> person = factory.makePerson(name='black')
365+ >>> login_person(person)
366+ >>> form = {
367+ ... 'field.time_starts': '2005-10-07 09:00',
368+ ... 'field.time_ends': '2005-10-17 19:05',
369+ ... 'field.is_physical': 'no',
370+ ... 'field.actions.register': 'Register'}
371+ >>> view = create_initialized_view(ubz, name='+attend', form=form)
372+ >>> view.errors
373+ []
374+
375+ >>> [sprint_attendance] = [attendance for attendance in ubz.attendances
376+ ... if attendance.attendee.name == 'black']
377+ >>> sprint_attendance.is_physical
378+ False
379+
380+
381+The +attend view
382+----------------
383+
384+The +attend view has a label.
385+
386+ >>> sprint_attendance_view = create_view(ubz, name='+attend')
387+ >>> print sprint_attendance_view.label
388+ Register your attendance
389+
390+
391+The +register views
392+-------------------
393+
394+The +register view has a label too.
395+
396+ >>> view = create_view(ubz, name='+register')
397+ >>> print view.label
398+ Register someone else
399+
400+The view descends from BaseSprintAttendanceAddView.
401+
402+ >>> isinstance(view, BaseSprintAttendanceAddView)
403+ True
404+
405+It also requires the attendee field so that a user can register someone else.
406+
407+ >>> view.field_names
408+ ['attendee', 'time_starts', 'time_ends', 'is_physical']
409+
410+ >>> person = factory.makePerson(name='greene')
411+ >>> form = {
412+ ... 'field.attendee': 'greene',
413+ ... 'field.time_starts': '2005-10-07 09:00',
414+ ... 'field.time_ends': '2005-10-17 19:05',
415+ ... 'field.is_physical': 'yes',
416+ ... 'field.actions.register': 'Register'}
417+ >>> view = create_initialized_view(ubz, name='+register', form=form)
418+ >>> view.errors
419+ []
420+
421+ >>> for attendee in ubz.attendees:
422+ ... print attendee.name
423+ black brown greene name12
424+
425+
426+Exporting the list of attendees
427+-------------------------------
428
429 The list of a sprint's attendees can be exported as a CSV file,
430 containing some details about each of the attendees.
431@@ -93,9 +191,11 @@
432 include it.
433
434 >>> view = create_view(ubz, '+attendees-csv')
435- >>> print view.render()
436- Launchpad username,Display name,...Timezone,...
437- name12,Sample Person...Australia/Perth...
438+ >>> lines = view.render().strip().splitlines()
439+ >>> print lines[0]
440+ Launchpad username,Display name,...Timezone,...Physically present
441+ >>> print lines[-1]
442+ name12,Sample Person,...Australia/Perth,...True
443
444 However, some people may set their location/timezone as hidden, so if
445 that's the case we won't include the person's timezone.
446
447=== modified file 'lib/lp/blueprints/browser/tests/test_views.py'
448--- lib/lp/blueprints/browser/tests/test_views.py 2009-06-25 00:00:26 +0000
449+++ lib/lp/blueprints/browser/tests/test_views.py 2009-11-06 03:05:24 +0000
450@@ -11,7 +11,7 @@
451
452 from canonical.launchpad.testing.systemdocs import (
453 LayeredDocFileSuite, setUp, tearDown)
454-from canonical.testing import LaunchpadFunctionalLayer
455+from canonical.testing import DatabaseFunctionalLayer
456
457
458 here = os.path.dirname(os.path.realpath(__file__))
459@@ -31,7 +31,7 @@
460 path = filename
461 one_test = LayeredDocFileSuite(
462 path, setUp=setUp, tearDown=tearDown,
463- layer=LaunchpadFunctionalLayer,
464+ layer=DatabaseFunctionalLayer,
465 stdout_logging_level=logging.WARNING
466 )
467 suite.addTest(one_test)
468
469=== modified file 'lib/lp/blueprints/doc/sprint-meeting-export.txt'
470--- lib/lp/blueprints/doc/sprint-meeting-export.txt 2009-08-13 15:12:16 +0000
471+++ lib/lp/blueprints/doc/sprint-meeting-export.txt 2009-11-06 03:05:24 +0000
472@@ -81,7 +81,7 @@
473
474 >>> time_starts = datetime(2005, 10, 8, 7, 0, 0, tzinfo=timezone('UTC'))
475 >>> time_ends = datetime(2005, 11, 17, 20, 0, 0, tzinfo=timezone('UTC'))
476- >>> ubz.attend(sampleperson, time_starts, time_ends)
477+ >>> ubz.attend(sampleperson, time_starts, time_ends, True)
478 <SprintAttendance at ...>
479
480 >>> view = getMultiAdapter((ubz, request), name='+temp-meeting-export')
481
482=== modified file 'lib/lp/blueprints/doc/sprint.txt'
483--- lib/lp/blueprints/doc/sprint.txt 2009-07-24 12:55:03 +0000
484+++ lib/lp/blueprints/doc/sprint.txt 2009-11-06 03:05:24 +0000
485@@ -198,3 +198,51 @@
486
487 >>> paris.isDriver(admin_person)
488 True
489+
490+
491+Sprint attendance
492+-----------------
493+
494+The sprint attend() method adds a user's attendance to a sprint.
495+
496+ >>> import datetime
497+ >>> import pytz
498+
499+ >>> person = factory.makePerson(name='mustard')
500+ >>> UTC = pytz.timezone('UTC')
501+ >>> time_starts = datetime.datetime(2005, 10, 7, 9, 0, 0, 0, UTC)
502+ >>> time_ends = datetime.datetime(2005, 10, 17, 19, 5, 0, 0, UTC)
503+ >>> sprint_attendance = ubz.attend(person, time_starts, time_ends, True)
504+
505+The attend() method can update a user's attendance if there is already a
506+ISprintAttendance for the user.
507+
508+ >>> print sprint_attendance.attendee.name
509+ mustard
510+ >>> print sprint_attendance.time_starts
511+ 2005-10-07 09:00:00+00:00
512+ >>> print sprint_attendance.time_ends
513+ 2005-10-17 19:05:00+00:00
514+ >>> print sprint_attendance.is_physical
515+ True
516+
517+ >>> time_starts = datetime.datetime(2005, 10, 8, 9, 0, 0, 0, UTC)
518+ >>> time_ends = datetime.datetime(2005, 10, 16, 19, 5, 0, 0, UTC)
519+ >>> new_attendance = ubz.attend(person, time_starts, time_ends, False)
520+ >>> print new_attendance.attendee.name
521+ mustard
522+ >>> print new_attendance.time_starts
523+ 2005-10-08 09:00:00+00:00
524+ >>> print new_attendance.time_ends
525+ 2005-10-16 19:05:00+00:00
526+ >>> print new_attendance.is_physical
527+ False
528+
529+The sprint attendances property returns a list of SprintAttendance objects.
530+
531+ >>> ubz.attendances
532+ [<SprintAttendance ...>]
533+
534+ >>> for attendance in ubz.attendances:
535+ ... print attendance.attendee.name
536+ mustard
537
538=== added file 'lib/lp/blueprints/doc/sprintattendance.txt'
539--- lib/lp/blueprints/doc/sprintattendance.txt 1970-01-01 00:00:00 +0000
540+++ lib/lp/blueprints/doc/sprintattendance.txt 2009-11-06 03:05:24 +0000
541@@ -0,0 +1,60 @@
542+SprintAttendance
543+================
544+
545+The SprintAttendance object links a person to a sprint. It records additional
546+information about the attendance. The start and end date-times are required
547+and they must be UTC
548+
549+ >>> import datetime
550+ >>> import pytz
551+ >>> from lp.blueprints.model.sprintattendance import SprintAttendance
552+
553+ >>> sprint = factory.makeSprint(title='lunarbase')
554+ >>> person = factory.makePerson(name='scarlet')
555+ >>> UTC = pytz.timezone('UTC')
556+ >>> time_starts = datetime.datetime(2019, 6, 21, 0, 0, 0, 0, UTC)
557+ >>> time_ends = datetime.datetime(2019, 7, 4, 0, 0, 0, 0, UTC)
558+ >>> sprint_attendance = SprintAttendance(
559+ ... sprint=sprint, attendee=person,
560+ ... time_starts=time_starts, time_ends=time_ends)
561+
562+The SprintAttendance object implements ISprintAttendance.
563+
564+ >>> from canonical.launchpad.webapp.testing import verifyObject
565+ >>> from lp.blueprints.interfaces.sprintattendance import (
566+ ... ISprintAttendance)
567+ >>> verifyObject(ISprintAttendance, sprint_attendance)
568+ True
569+
570+The sprint and user can be accessed via the sprint and user attributes.
571+
572+ >>> print sprint_attendance.sprint.title
573+ lunarbase
574+ >>> print sprint_attendance.attendee.name
575+ scarlet
576+
577+The time of the users arrival and departure can be retrieved from the
578+time_start and time_end attributes respectively.
579+
580+ >>> print sprint_attendance.time_starts
581+ 2019-06-21 00:00:00+00:00
582+ >>> print sprint_attendance.time_ends
583+ 2019-07-04 00:00:00+00:00
584+
585+SprintAttendance records whether the user intend to be physically present
586+at the sprint; a false value implies virtual attendance. The default value
587+is true.
588+
589+ >>> sprint_attendance.is_physical
590+ True
591+
592+The is_physical value can be specified when a SprintAttendance instance is
593+created.
594+
595+ >>> person_2 = factory.makePerson(name='plum')
596+ >>> sprint_attendance_2 = SprintAttendance(
597+ ... sprint=sprint, attendee=person_2,
598+ ... time_starts=time_starts, time_ends=time_ends,
599+ ... is_physical=False)
600+ >>> sprint_attendance_2.is_physical
601+ False
602
603=== modified file 'lib/lp/blueprints/interfaces/sprint.py'
604--- lib/lp/blueprints/interfaces/sprint.py 2009-09-03 23:06:50 +0000
605+++ lib/lp/blueprints/interfaces/sprint.py 2009-11-06 03:05:24 +0000
606@@ -143,7 +143,7 @@
607 """
608
609 # subscription-related methods
610- def attend(person, time_starts, time_ends):
611+ def attend(person, time_starts, time_ends, is_physical):
612 """Record that this person will be attending the Sprint."""
613
614 def removeAttendance(person):
615
616=== modified file 'lib/lp/blueprints/interfaces/sprintattendance.py'
617--- lib/lp/blueprints/interfaces/sprintattendance.py 2009-06-25 00:00:26 +0000
618+++ lib/lp/blueprints/interfaces/sprintattendance.py 2009-11-06 03:05:24 +0000
619@@ -12,7 +12,7 @@
620 ]
621
622 from zope.interface import Interface
623-from zope.schema import Choice, Datetime
624+from zope.schema import Bool, Choice, Datetime
625 from canonical.launchpad import _
626 from canonical.launchpad.fields import PublicPersonChoice
627
628@@ -33,4 +33,9 @@
629 "Please ensure the time reflects accurately "
630 "when you will no longer be available for sessions at this event, to "
631 "assist those planning the schedule."))
632-
633+ is_physical = Bool(
634+ title=_("How will you be attending?"),
635+ description=_(
636+ "True, you will be physically present, "
637+ "or false, you will be remotely present."),
638+ required=False, readonly=False, default=True)
639
640=== modified file 'lib/lp/blueprints/model/sprint.py'
641--- lib/lp/blueprints/model/sprint.py 2009-08-19 23:11:16 +0000
642+++ lib/lp/blueprints/model/sprint.py 2009-11-06 03:05:24 +0000
643@@ -265,16 +265,18 @@
644 filter=[SpecificationFilter.PROPOSED]).count()
645
646 # attendance
647- def attend(self, person, time_starts, time_ends):
648+ def attend(self, person, time_starts, time_ends, is_physical):
649 """See `ISprint`."""
650 # first see if a relevant attendance exists, and if so, update it
651 for attendance in self.attendances:
652 if attendance.attendee.id == person.id:
653 attendance.time_starts = time_starts
654 attendance.time_ends = time_ends
655+ attendance.is_physical = is_physical
656 return attendance
657 # since no previous attendance existed, create a new one
658- return SprintAttendance(sprint=self, attendee=person,
659+ return SprintAttendance(
660+ sprint=self, attendee=person, is_physical=is_physical,
661 time_starts=time_starts, time_ends=time_ends)
662
663 def removeAttendance(self, person):
664
665=== modified file 'lib/lp/blueprints/model/sprintattendance.py'
666--- lib/lp/blueprints/model/sprintattendance.py 2009-06-25 00:00:26 +0000
667+++ lib/lp/blueprints/model/sprintattendance.py 2009-11-06 03:05:24 +0000
668@@ -9,13 +9,12 @@
669
670 from zope.interface import implements
671
672-from sqlobject import ForeignKey
673+from sqlobject import BoolCol, ForeignKey
674
675 from lp.blueprints.interfaces.sprintattendance import ISprintAttendance
676 from lp.registry.interfaces.person import validate_public_person
677
678 from canonical.database.datetimecol import UtcDateTimeCol
679-
680 from canonical.database.sqlbase import SQLBase
681
682
683@@ -33,5 +32,4 @@
684 storm_validator=validate_public_person, notNull=True)
685 time_starts = UtcDateTimeCol(notNull=True)
686 time_ends = UtcDateTimeCol(notNull=True)
687-
688-
689+ is_physical = BoolCol(dbName='is_physical', notNull=True, default=True)
690
691=== modified file 'lib/lp/blueprints/stories/sprints/20-sprint-registration.txt'
692--- lib/lp/blueprints/stories/sprints/20-sprint-registration.txt 2009-09-21 12:39:15 +0000
693+++ lib/lp/blueprints/stories/sprints/20-sprint-registration.txt 2009-11-06 03:05:24 +0000
694@@ -165,12 +165,6 @@
695 >>> browser.getLink('Export attendees to CSV').click()
696 >>> print browser.headers['content-type']
697 text/csv
698- >>> print browser.contents
699- Launchpad username,Display name,Email,IRC nickname,Phone,Organization,City,Country,Timezone,Arriving,Leaving
700- carlos,Carlos Perelló Marín,carlos@canonical.com,"carlos, qarlos",,,,,,2006-01-10T09:00:00Z,2006-02-12T05:41:00Z
701- name12,Sample Person,,,,,,,Australia/Perth,2006-01-10T01:00:00Z,2006-02-12T07:30:00Z
702- salgado,Guilherme Salgado,guilherme.salgado@canonical.com,,+55 16 3374-2027,Something,whatever,France,,2006-01-09T23:00:00Z,2006-02-12T07:30:00Z
703- <BLANKLINE>
704
705 >>> carlos_browser = setupBrowser(auth='Basic carlos@canonical.com:test')
706 >>> carlos_browser.open('http://launchpad.dev/sprints/ubz')
707
708=== modified file 'lib/lp/blueprints/tests/test_doc.py'
709--- lib/lp/blueprints/tests/test_doc.py 2009-07-17 00:26:05 +0000
710+++ lib/lp/blueprints/tests/test_doc.py 2009-11-06 03:05:24 +0000
711@@ -7,9 +7,7 @@
712
713 import logging
714 import os
715-import unittest
716
717-from canonical.launchpad.testing.pages import PageTestSuite
718 from canonical.launchpad.testing.systemdocs import (
719 LayeredDocFileSuite, setUp, tearDown)
720 from canonical.launchpad.ftests.test_system_documentation import(
721@@ -33,4 +31,4 @@
722
723
724 def test_suite():
725- return build_test_suite(here, special)
726+ return build_test_suite(here, special, layer=DatabaseFunctionalLayer)

Subscribers

People subscribed via source and target branches

to status/vote changes: