Merge lp:~sinzui/launchpad/sprint-attendence-1 into lp:launchpad/db-devel
- sprint-attendence-1
- Merge into db-devel
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 | ||||
Related bugs: |
|
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 |
Commit message
Description of the change
Curtis Hovey (sinzui) wrote : | # |
Abel Deuring (adeuring) wrote : | # |
Hi Curits,
a nice branch, just two minor nitpicks, see below.
Abel
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
> @@ -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/
> --- lib/lp/
> +++ lib/lp/
[...]
> +The +register views
> +------
I think you can remove a few '-' in the line above.
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://
If we like the widget, we can remove some custom code in bugs and code by using a single line to enable this widget.
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
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://
We now have an easy way to present boolean values are comprehensible choices in the UI:
custom_widget(
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 :)
Preview Diff
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 | + /> 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" /> 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 | + /> 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" /> 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) |
This is my branch to expose SprintAttendanc e.is_physical in the UI. people. canonical. com/~curtis/ is-physical. png for an
Most of this branch adds missing test coverage.
See http://
example of the yes-no widget.
lp:~sinzui/launchpad/sprint-attendence-1 /bugs.launchpad .net/bugs/ 461945 radio-widget implementation: no one
Diff size: 663
Launchpad bug: https:/
Test command: ./bin/test -vv -t sprint -t launchpad-
Pre-
Target release: 3.1.10
= Expose SprintAttendanc e.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: /launchpad/ doc/launchpad- radio-widget. txt /widgets/ itemswidgets. py blueprints/ browser/ sprint. py blueprints/ browser/ sprintattendanc e.py blueprints/ browser/ tests/sprintatt endance- views.txt blueprints/ browser/ tests/test_ views.py blueprints/ doc/sprint- meeting- export. txt blueprints/ doc/sprint. txt blueprints/ doc/sprintatten dance.txt blueprints/ interfaces/ sprint. py blueprints/ interfaces/ sprintattendanc e.py blueprints/ model/sprint. py blueprints/ model/sprintatt endance. py blueprints/ tests/test_ doc.py
lib/canonical
lib/canonical
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
lib/lp/
== Test ==
* lib/canonical/ launchpad/ doc/launchpad- radio-widget. txt nRadioWidget renders a blueprints/ browser/ tests/sprintatt endance- views.txt blueprints/ browser/ tests/test_ views.py nalLayer blueprints/ doc/sprint- meeting- export. txt blueprints/ doc/sprint. txt blueprints/ doc/sprintatten dance.txt blueprints/ tests/test_ doc.py nalLayer
* Added test to verify that LaunchpadBoolea
yes-no radio widget for a boolean field,
* lib/lp/
* 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/
* Run the tests on the DatabaseFunctio
* lib/lp/
* Updated the attend() call to pass an is_physical argument.
* lib/lp/
* Added missing coverage about how the attend() method works.
* Verified that is_physical is accepted as an argument.
* lib/lp/
* Added missing test coverage for how the SprintAttendance object works.
* Verified that the is_physical property is supported.
* lib/lp/
* Run the tests on the DatabaseFunctio
== Implementation ==
* lib/canonical/ widgets/ itemswidgets. py
* C...