~launchpad-pqm/launchpad/devel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
Person
======

The Person class is what we use to represent Launchpad users, teams and
some people which have done work on the free software community but are
not Launchpad users.

    >>> from zope.component import getUtility
    >>> from lp.services.identity.interfaces.emailaddress import (
    ...     IEmailAddressSet)
    >>> from lp.services.webapp.testing import verifyObject
    >>> from lp.registry.interfaces.person import (
    ...     IHasStanding,
    ...     IPerson,
    ...     IPersonSet,
    ...     )
    >>> from lp.registry.interfaces.product import IProductSet
    >>> from lp.translations.interfaces.hastranslationimports import (
    ...     IHasTranslationImports)

Any Person object (either a person or a team) implements IPerson...

    >>> personset = getUtility(IPersonSet)
    >>> foobar = personset.getByName('name16')
    >>> foobar.is_team
    False

    >>> verifyObject(IPerson, foobar)
    True

    >>> ubuntu_team = personset.getByName('ubuntu-team')
    >>> ubuntu_team.is_team
    True

    >>> verifyObject(IPerson, ubuntu_team)
    True


...and IHasTranslationImports...

    >>> IHasTranslationImports.providedBy(foobar)
    True

    >>> verifyObject(IHasTranslationImports, foobar)
    True

    >>> IHasTranslationImports.providedBy(ubuntu_team)
    True

    >>> verifyObject(IHasTranslationImports, ubuntu_team)
    True

...and IHasStanding.  Teams can technically also have standing, but it's
meaningless because teams cannot post to mailing lists.

    >>> IHasStanding.providedBy(foobar)
    True

    >>> verifyObject(IHasStanding, foobar)
    True


The IPersonSet utility
----------------------

Access to people (Persons or Teams) is done through the IPersonSet
utility:

    >>> from lp.services.webapp.testing import verifyObject

    >>> verifyObject(IPersonSet, personset)
    True

You can create a new person using the createPersonAndEmail method of
IPersonSet. All you need for that is a valid email address. You can also
hide the user email addresses.

Some of our scripts may create Person entries, and in these cases they
must provide a rationale and a comment (optional) for the creation of
that Person entry. These are displayed on the home pages of unvalidated
Launchpad profiles, to make it clear that those profiles were not
created by the people they represent and why they had to be created.
Because the comment will be displayed verbatim in a web page, it must
start with the word "when" followed by a description of the action that
caused the entry to be created.

    >>> from lp.services.identity.interfaces.emailaddress import (
    ...     EmailAddressStatus)
    >>> from lp.registry.interfaces.person import PersonCreationRationale
    >>> p, email = personset.createPersonAndEmail(
    ...     'randomuser@randomhost.com', PersonCreationRationale.POFILEIMPORT,
    ...     comment='when importing the Portuguese translation of firefox',
    ...     hide_email_addresses=True)
    >>> import transaction
    >>> transaction.commit()
    >>> p.teamowner is None
    True

    >>> email.status == EmailAddressStatus.NEW
    True

    >>> p.is_valid_person # Not valid because no preferred email address
    False

    >>> p.hide_email_addresses
    True

Since this person has chosen to hide his email addresses they won't be
visible to other users which are not admins.

    >>> from lp.services.webapp.authorization import check_permission
    >>> login('randomuser@randomhost.com')
    >>> check_permission('launchpad.View', email)
    True

    >>> login('test@canonical.com')
    >>> check_permission('launchpad.View', email)
    False

    >>> login('guilherme.salgado@canonical.com')
    >>> check_permission('launchpad.View', email)
    True

    >>> login(ANONYMOUS)

By default, newly created Person entries will have
AccountStatus.NOACCOUNT as their account_status. This is only changed
if/when we turn that entry into an actual user account.  Note that both
the Person and the EmailAddress have accounts when they are created
using the createPersonAndEmail() method.

    >>> p.account_status
    <DBItem AccountStatus.NOACCOUNT...

    >>> email.accountID == p.accountID
    True

    >>> p.setPreferredEmail(email)
    >>> email.status
    <DBItem EmailAddressStatus.PREFERRED...

    >>> p.account_status
    <DBItem AccountStatus.NOACCOUNT...

    >>> from lp.services.identity.model.account import Account
    >>> from lp.services.database.lpstorm import IMasterStore
    >>> account = IMasterStore(Account).get(Account, p.accountID)
    >>> account.activate(
    ...     "Activated by doc test.",
    ...     password=p.password,
    ...     preferred_email=email)
    >>> p.account_status
    <DBItem AccountStatus.ACTIVE...

The user can add additional email addresses. The
validateAndEnsurePreferredEmail() method verifies that the email belongs
to the person, and it updates the email address's status.

    >>> emailset = getUtility(IEmailAddressSet)
    >>> validated_email = emailset.new(
    ...     'validated@canonical.com', p, account=p.account)
    >>> validated_email.status
    <DBItem EmailAddressStatus.NEW...

    >>> login('randomuser@randomhost.com')
    >>> p.validateAndEnsurePreferredEmail(validated_email)
    >>> validated_email.status
    <DBItem EmailAddressStatus.VALIDATED...

The user can add a new address and set it as the preferred address. The
setPreferredEmail() method updated the address's status.

    >>> preferred_email = emailset.new(
    ...     'preferred@canonical.com', p, account=p.account)
    >>> preferred_email.status
    <DBItem EmailAddressStatus.NEW...

    >>> p.setPreferredEmail(preferred_email)
    >>> preferred_email.status
    <DBItem EmailAddressStatus.PREFERRED...

    >>> login(ANONYMOUS)

In the case of teams, though, the account_status is not changed as their
account_status must always be set to NOACCOUNT. (Notice how we use
setContactAddress() rather than setPreferredEmail() here, since the
latter can be used only for people and the former only for teams)

    >>> team = factory.makeTeam(name='foo', displayname='foobaz')
    >>> team.account_status
    <DBItem AccountStatus.NOACCOUNT...

    >>> email = emailset.new('foo@baz.com', team)
    >>> team.setContactAddress(email)
    >>> email.status
    <DBItem EmailAddressStatus.PREFERRED...

    >>> team.account_status
    <DBItem AccountStatus.NOACCOUNT...

Unlike people, teams don't need a contact address, so we can pass None
to setContactAddress() to leave a team without a contact address.

    >>> team.setContactAddress(None)
    >>> print team.preferredemail
    None

When a new sourcepackage is imported and a Person entry has to be
created because we don't know about the maintainer of that package, the
code to create the person should look like this:

    >>> person, emailaddress = personset.createPersonAndEmail(
    ...     'random@random.com', PersonCreationRationale.SOURCEPACKAGEIMPORT,
    ...     comment='when the ed package was imported into Ubuntu Breezy')
    >>> person.is_valid_person
    False

    >>> person.creation_comment
    u'when the ed package was imported into Ubuntu Breezy'

Checking .is_valid_person issues a DB query to the
ValidPersonOrTeamCache, unless it's already been cached. To avoid many
small queries when checking whether a lot of people are valid,
getValidPersons() can be used. This is useful for filling the ORM cache,
so that code in other places can check .is_valid_person, without it
issuing a DB query.

    >>> non_valid_person = person
    >>> non_valid_person.is_valid_person
    False

    >>> foobar.is_valid_person
    True

    >>> valid_persons = personset.getValidPersons([non_valid_person, foobar])
    >>> [person.name for person in valid_persons]
    [u'name16']


Accounts
........

A Person may be linked to an Account.

    >>> login('no-priv@canonical.com')
    >>> person = personset.getByEmail('no-priv@canonical.com')
    >>> person.account.openid_identifiers.any().identifier
    u'no-priv_oid'


Adapting an Account into a Person
.................................

And when the person is linked to an account, it's possible to adapt that
account into an IPerson.

    >>> IPerson(person.account) == person
    True

We can't adapt an account which has no person associated with, though.

    >>> from lp.services.identity.interfaces.account import (
    ...     AccountCreationRationale, IAccountSet)
    >>> personless_account = getUtility(IAccountSet).new(
    ...     AccountCreationRationale.UNKNOWN, 'Display name')
    >>> print IPerson(personless_account, None)
    None

Our security adapters expect to get passed an IPerson, but we use
IAccounts to represent logged in users, so we need to adapt them into
IPerson before passing that to the adapters.  Since our Account table
has no reference to the Person table, the adaptation may end up hitting
the DB, which is something we don't want our security adapters to be
doing as they're called tons of times for every page we render.  For
that reason, whenever there is a browser request, we will cache the
IPerson objects associated to the accounts we adapt.

Up to now there was no browser request we could use, so no caching was
done.

    >>> from lp.services.webapp.servers import LaunchpadTestRequest
    >>> request = LaunchpadTestRequest()
    >>> print request.annotations.get('launchpad.person_to_account_cache')
    None

Now we log in with the request so that whenever we adapt an account into
a Person, the Person is cached in the request.

    >>> login('foo.bar@canonical.com', request)
    >>> IPerson(person.account)
    <Person...No Privileges Person)>

    >>> cache = request.annotations.get('launchpad.person_to_account_cache')
    >>> from zope.security.proxy import removeSecurityProxy
    >>> cache[removeSecurityProxy(person.account)]
    <Person...No Privileges Person)>

If we manually change the cache, the adapter will be fooled and will
return the wrong object.

    >>> cache[removeSecurityProxy(person.account)] = 'foo'
    >>> IPerson(person.account)
    'foo'

If the cached value is None, though, the adapter will look up the
correct Person again and update the cache.

    >>> cache[removeSecurityProxy(person.account)] = None
    >>> IPerson(person.account)
    <Person...No Privileges Person)>

    >>> cache[removeSecurityProxy(person.account)]
    <Person...No Privileges Person)>


Personal standing
.................

People have a property called 'personal standing', which affects for
example their ability to post to mailing lists they are not members of.
It's a form of automatic moderation.  Most people have unknown standing,
which is the default.

    >>> login('foo.bar@canonical.com')
    >>> lifeless = personset.getByName('lifeless')
    >>> lifeless.personal_standing
    <DBItem PersonalStanding.UNKNOWN...

A person also has a reason for why their standing is what it is.  The
default value of None means that no reason for the personal_standing
value is available.

    >>> print lifeless.personal_standing_reason
    None

A Launchpad administrator may change a person's standing, and may give a
reason for the change.

    >>> from lp.registry.interfaces.person import PersonalStanding
    >>> lifeless.personal_standing = PersonalStanding.GOOD
    >>> lifeless.personal_standing_reason = 'Such a cool guy!'

    >>> lifeless.personal_standing
    <DBItem PersonalStanding.GOOD...

    >>> print lifeless.personal_standing_reason
    Such a cool guy!

Non-administrators may not change a person's standing.

    >>> login('test@canonical.com')
    >>> lifeless.personal_standing = PersonalStanding.POOR
    Traceback (most recent call last):
    ...
    Unauthorized: ...

    >>> lifeless.personal_standing_reason = 'Such a cool guy!'
    Traceback (most recent call last):
    ...
    Unauthorized: ...

    >>> login('foo.bar@canonical.com')
    >>> lifeless.personal_standing
    <DBItem PersonalStanding.GOOD...

    >>> print lifeless.personal_standing_reason
    Such a cool guy!

    >>> login(ANONYMOUS)


Ubuntu Code of Conduct signees
..............................

Some people have signed the latest version of the Ubuntu Code of Conduct
and others have not.

    >>> foobar.is_ubuntu_coc_signer
    True

    >>> lifeless.is_ubuntu_coc_signer
    False


Teams
-----

As we said above, the Person class is overloaded to represent teams so
we may have Person objects which are, in fact, teams. To find out
whether a given object is a person or a team we can use the is_team
property of IPerson or check if the object provides the ITeam interface.

    >>> from lp.registry.interfaces.person import ITeam
    >>> ddaa = personset.getByName('ddaa')
    >>> ddaa.is_team
    False

    >>> ITeam.providedBy(ddaa)
    False

    >>> landscape_devs = personset.getByName('landscape-developers')
    >>> landscape_devs.is_team
    True

    >>> ITeam.providedBy(landscape_devs)
    True

    >>> verifyObject(ITeam, landscape_devs)
    True

Also note that a team will never have a Launchpad account, so its
account_status will always be NOACCOUNT.

    >>> landscape_devs.account_status
    <DBItem AccountStatus.NOACCOUNT...


Creating teams
..............

Teams are created by the IPersonSet.newTeam() method, which takes the
team owner and some of the team's details, returning the newly created
team.

    >>> new_team = personset.newTeam(ddaa, 'new-team', 'Just a new team')
    >>> new_team.name
    u'new-team'

    >>> new_team.teamowner.name
    u'ddaa'

If the given name is already in use by another team/person, an exception
is raised.

    >>> personset.newTeam(ddaa, 'ddaa', 'Just a new team')
    Traceback (most recent call last):
    ...
    NameAlreadyTaken:...

PersonSet.newTeam() will also fire an ObjectCreatedEvent for the newly
created team.

    >>> from zope.lifecycleevent.interfaces import IObjectCreatedEvent
    >>> from lp.testing.event import TestEventListener
    >>> def print_event(team, event):
    ...     print "ObjectCreatedEvent fired for team '%s'" % team.name

    >>> listener = TestEventListener(
    ...     ITeam, IObjectCreatedEvent, print_event)
    >>> another_team = personset.newTeam(ddaa, 'new3', 'Another a new team')
    ObjectCreatedEvent fired for team 'new3'

    >>> listener.unregister()


Turning people into teams
.........................

Launchpad may create Person entries automatically and it always assumes
these are actual people.  Sometimes, though, these should actually be
teams, so we provide an easy way to turn one of these auto created
entries into teams.

    >>> not_a_person, dummy = personset.createPersonAndEmail(
    ...     'foo@random.com', PersonCreationRationale.SOURCEPACKAGEIMPORT,
    ...     comment='when the ed package was imported into Ubuntu Feisty')
    >>> transaction.commit()
    >>> not_a_person.is_team
    False

    >>> not_a_person.is_valid_person
    False

    >>> not_a_person.account_status
    <DBItem AccountStatus.NOACCOUNT...

    # Empty stub.test_emails as later we'll want to show that no
    # notifications are sent when we add the owner as a member of
    # the team.

    >>> from lp.services.mail import stub
    >>> stub.test_emails = []

    >>> not_a_person.convertToTeam(team_owner=ddaa)
    >>> not_a_person.is_team
    True

    >>> ITeam.providedBy(not_a_person)
    True

    >>> verifyObject(ITeam, not_a_person)
    True

    >>> print removeSecurityProxy(not_a_person).password
    None

The team owner is also added as an administrator of its team.

    >>> [member.name for member in not_a_person.adminmembers]
    [u'ddaa']

    # As said previously, no notifications are sent when we add the
    # team owner as a member of his team.

    >>> transaction.commit()
    >>> stub.test_emails
    []

And we can even add other members to our new team!

    >>> login('foo.bar@canonical.com')
    >>> ignored = not_a_person.addMember(lifeless, reviewer=ddaa)
    >>> login(ANONYMOUS)
    >>> [member.name for member in not_a_person.activemembers]
    [u'ddaa', u'lifeless']

This functionality is only available for non-team Person entries whose
account_status is NOACCOUNT, though.

    >>> ddaa.account_status
    <DBItem AccountStatus.ACTIVE...

    >>> ddaa.convertToTeam(team_owner=landscape_devs)
    Traceback (most recent call last):
    ...
    AssertionError: Only Person entries whose account_status is NOACCOUNT...

    >>> not_a_person.convertToTeam(team_owner=landscape_devs)
    Traceback (most recent call last):
    ...
    AlreadyConvertedException: foo-v has already been converted to a team.


Team members
............

The relationship between a person and a team is stored in
TeamMemberships table. TeamMemberships have a status (which can be any
item of TeamMembershipStatus) and represent the current state of the
relationship between that person and that team. Only
TeamMembershipStatus with an ADMIN or APPROVED status are considered
active.

    >>> [member.displayname for member in landscape_devs.approvedmembers]
    [u'Guilherme Salgado']

    >>> [member.displayname for member in landscape_devs.adminmembers]
    [u'Sample Person']

The IPerson.activemembers property will always include all approved and
admin members of that team.

    >>> [member.displayname for member in landscape_devs.activemembers]
    [u'Guilherme Salgado', u'Sample Person']

TeamMemberships with a PROPOSED or INVITED status represent a
person/team which has proposed himself as a member or which has been
invited to join the team.

    >>> [member.displayname for member in landscape_devs.proposedmembers]
    [u'Foo Bar']

    >>> [member.displayname for member in landscape_devs.invited_members]
    [u'Launchpad Developers']

Similarly, we have IPerson.pendingmembers which includes both invited
and proposed members.

    >>> [member.displayname for member in landscape_devs.pendingmembers]
    [u'Foo Bar', u'Launchpad Developers']

Finally, we have EXPIRED and DEACTIVATED TeamMemberships, which
represent former (inactive) members of a team.

    >>> [member.displayname for member in landscape_devs.expiredmembers]
    [u'Karl Tilbury']

    >>> [member.displayname for member in landscape_devs.deactivatedmembers]
    [u'No Privileges Person']

We can get a list of all inactive members of a team with the
IPerson.inactivemembers property.

    >>> [member.displayname for member in landscape_devs.inactivemembers]
    [u'Karl Tilbury', u'No Privileges Person']

We can also iterate over the TeamMemberships themselves, which is useful
when we want to display details about them rather than just the member.

    >>> [(membership.person.displayname, membership.status.name)
    ...  for membership in landscape_devs.member_memberships]
    [(u'Guilherme Salgado', 'APPROVED'), (u'Sample Person', 'ADMIN')]

    >>> [(membership.person.displayname, membership.status.name)
    ...  for membership in landscape_devs.getInvitedMemberships()]
    [(u'Launchpad Developers', 'INVITED')]

    >>> [(membership.person.displayname, membership.status.name)
    ...  for membership in landscape_devs.getProposedMemberships()]
    [(u'Foo Bar', 'PROPOSED')]

    >>> [(membership.person.displayname, membership.status.name)
    ...  for membership in landscape_devs.getInactiveMemberships()]
    [(u'Karl Tilbury', 'EXPIRED'), (u'No Privileges Person', 'DEACTIVATED')]

An IPerson has an inTeam method to allow us to easily check if a person
is a member (directly or through other teams) of a team. It accepts an
object implementing IPerson, which is the common use case when checking
permissions.

    >>> ddaa.is_valid_person
    True

    >>> vcs_imports = personset.getByName('vcs-imports')
    >>> lifeless.inTeam(vcs_imports) and ddaa.inTeam(vcs_imports)
    True

That method can also be used to check that a given IPerson is a member
of itself. We can do that because people and teams have
TeamParticipation entries for themselves.

    >>> ddaa.inTeam(ddaa)
    True

    >>> ddaa.hasParticipationEntryFor(ddaa)
    True

    >>> vcs_imports.inTeam(vcs_imports)
    True

    >>> vcs_imports.hasParticipationEntryFor(vcs_imports)
    True


Email notifications to teams
............................

If a team has a contact email address, all notifications we send to the
team will go to that address.

    >>> login('no-priv@canonical.com')
    >>> ubuntu_team = personset.getByName('ubuntu-team')
    >>> ubuntu_team.preferredemail.email
    u'support@ubuntu.com'

    >>> from lp.services.mail.helpers import get_contact_email_addresses
    >>> get_contact_email_addresses(ubuntu_team)
    set(['support@ubuntu.com'])

On the other hand, if a team doesn't have a contact email address, all
notifications we send to the team will go to the preferred email of each
direct member of that team.

    >>> vcs_imports.preferredemail is None
    True

    >>> sorted(member.preferredemail.email
    ...        for member in vcs_imports.activemembers)
    [u'david.allouche@canonical.com', u'foo.bar@canonical.com',
     u'robertc@robertcollins.net']

    >>> sorted(get_contact_email_addresses(vcs_imports))
    ['david.allouche@canonical.com', 'foo.bar@canonical.com',
     'robertc@robertcollins.net']


Team Visibility
...............

A Team can have its visibility attribute set to
PersonVisibility.PUBLIC or PersonVisibility.PRIVATE.

PRIVATE teams are hidden from view from non-members but they are
allowed to actually do things in Launchpad.

The PublicPersonChoice for interface classes and the
validate_public_person for database classes only allow public teams to
be assigned to the specified field.

The validators will raise a PrivatePersonLinkageError exception if an
invalid team is passed to the constructor or is used to set one of the
attributes.

Private teams can be subscribed to bugs.

    >>> login('foo.bar@canonical.com')
    >>> from lp.bugs.interfaces.bug import IBugSet
    >>> from lp.registry.interfaces.person import (
    ...     IPersonSet, PersonVisibility)
    >>> from lp.bugs.model.bugsubscription import BugSubscription
    >>> person_set = getUtility(IPersonSet)
    >>> bug_set = getUtility(IBugSet)
    >>> bug = bug_set.get(1)
    >>> guadamen = person_set.getByName('guadamen')
    >>> salgado = personset.getByName('salgado')
    >>> private_team = factory.makeTeam(salgado, name='private-team',
    ...     displayname='Private Team',
    ...     visibility=PersonVisibility.PRIVATE)
    >>> private_team_owner = private_team.teamowner
    >>> bug_subscription = BugSubscription(bug=bug, person=private_team,
    ...     subscribed_by=guadamen)

And they can subscribe others to bugs.

    >>> bug_subscription = BugSubscription(bug=bug, person=guadamen,
    ...     subscribed_by=private_team)

Teams also have a 'private' attribute that is true if the team is
private and false for public teams.  It is also false for people.

    >>> private_team.private
    True

    >>> guadamen.private
    False

    >>> salgado.private
    False

Latest Team Memberships
-----------------------

The key concept in displaying the latest team memberships is that the
team list is actually sorted by date joined.

    >>> from zope.component import getUtility
    >>> from lp.registry.interfaces.person import IPersonSet
    >>> personset = getUtility(IPersonSet)
    >>> foobar = personset.getByName('name16')
    >>> membership_list = foobar.getLatestApprovedMembershipsForPerson()
    >>> for membership in membership_list:
    ...     print membership.datejoined
    2009-07-09 11:58:38.122886+00:00
    2008-05-14 12:07:14.227450+00:00
    2007-01-17 14:13:39.692693+00:00
    2006-05-15 22:23:29.062603+00:00
    2005-10-13 13:03:41.668724+00:00


Searching
---------

You can search based on a person's name or displayname, or any of the
email addresses that belongs to a person using the methods provided by
IPersonSet.

While we don't have Full Text Indexes in the emailaddress table, we'll
be trying to match the text only against the beginning of an email
address:

    # First we'll define a utility function to help us displaying
    # the results.

    >>> naked_emailset = removeSecurityProxy(getUtility(IEmailAddressSet))
    >>> def print_people(results):
    ...     for person in results:
    ...         emails = [email.email
    ...                   for email in naked_emailset.getByPerson(person)]
    ...         print "%s (%s): %s" % (
    ...             person.displayname, person.name, emails)

    >>> print_people(personset.find('ubuntu'))
    Mirror Administrators (ubuntu-mirror-admins): []
    Sigurd Gartmann (sigurd-ubuntu): [u'sigurd-ubuntu@brogar.org']
    Ubuntu Doc Team (doc): [u'doc@lists.ubuntu.com']
    Ubuntu Gnome Team (name18): []
    Ubuntu Security Team (ubuntu-security): []
    Ubuntu Team (ubuntu-team): [u'support@ubuntu.com']
    Ubuntu Technical Board (techboard): []
    Ubuntu Translators (ubuntu-translators): []

    >>> print_people(personset.find('steve.alexander'))
    Steve Alexander (stevea): [u'steve.alexander@ubuntulinux.com']

    >>> print_people(personset.find('steve.alexander@'))
    Steve Alexander (stevea): [u'steve.alexander@ubuntulinux.com']

    >>> list(personset.find('eve.alexander@'))
    []

    >>> list(personset.find('eve.alexander'))
    []

The teams returned are dependent upon the team's visibility (privacy)
and whether the logged in user is a member of those teams.

Anonymous users cannot see non-public teams, such as 'private-team'.

    >>> login(ANONYMOUS)
    >>> print_people(personset.find('team'))
    Another a new team (new3): []
    Hoary Gnome Team (name21): []
    HWDB Team (hwdb-team): []
    Just a new team (new-team): []
    No Team Memberships (no-team-memberships):
      [u'no-team-memberships@test.com']
    Other Team (otherteam): []
    Simple Team (simple-team): []
    Team Membership Janitor (team-membership-janitor): []
    testing Spanish team (testing-spanish-team): []
    Ubuntu Doc Team (doc): [u'doc@lists.ubuntu.com']
    Ubuntu Gnome Team (name18): []
    Ubuntu Security Team (ubuntu-security): []
    Ubuntu Team (ubuntu-team): [u'support@ubuntu.com']
    Warty Gnome Team (warty-gnome): []
    Warty Security Team (name20): []

But Owner, a member of that team, will see it in the results.

    >>> login_person(private_team_owner)
    >>> print_people(personset.find('team'))
    Another a new team (new3): []
    Hoary Gnome Team (name21): []
    HWDB Team (hwdb-team): []
    Just a new team (new-team): []
    No Team Memberships (no-team-memberships):
      [u'no-team-memberships@test.com']
    Other Team (otherteam): []
    Private Team (private-team): []
    Simple Team (simple-team): []
    Team Membership Janitor (team-membership-janitor): []
    testing Spanish team (testing-spanish-team): []
    Ubuntu Doc Team (doc): [u'doc@lists.ubuntu.com']
    Ubuntu Gnome Team (name18): []
    Ubuntu Security Team (ubuntu-security): []
    Ubuntu Team (ubuntu-team): [u'support@ubuntu.com']
    Warty Gnome Team (warty-gnome): []
    Warty Security Team (name20): []

Searching for people and teams without specifying some text to filter
the results will cause no people/teams to be returned.

    >>> list(personset.find(''))
    []

Searching only for People based on their names or email addresses:

    >>> print_people(personset.findPerson('james.blackwell'))
    James Blackwell (jblack): [u'james.blackwell@ubuntulinux.com']

    >>> print_people(personset.findPerson('dave'))
    Dave Miller (justdave): [u'dave.miller@ubuntulinux.com',
                             u'justdave@bugzilla.org']

The created_before and created_after arguments can be used to restrict
the matches by the IPerson.datecreated value.

    >>> from datetime import datetime
    >>> import pytz

    >>> created_after = datetime(2008, 6, 27, tzinfo=pytz.UTC)
    >>> created_before = datetime(2008, 7, 1, tzinfo=pytz.UTC)
    >>> print_people(personset.findPerson(text='',
    ...     created_after=created_after, created_before=created_before))
    Brad Crittenden (bac): [u'bac@canonical.com']

By default, when searching only for people, any person whose account is
inactive is not included in the list, but we can tell findPerson to
include them as well.

    >>> from lp.services.identity.interfaces.account import AccountStatus
    >>> dave = personset.getByName('justdave')
    >>> removeSecurityProxy(dave).account_status = AccountStatus.DEACTIVATED
    >>> transaction.commit()
    >>> list(personset.findPerson('dave'))
    []

    >>> print_people(
    ...     personset.findPerson('dave', exclude_inactive_accounts=False))
    Dave Miller (justdave): [u'dave.miller@ubuntulinux.com',
                             u'justdave@bugzilla.org']

    >>> removeSecurityProxy(dave).account_status = AccountStatus.ACTIVE
    >>> flush_database_updates()
    >>> login(ANONYMOUS)

Searching only for Teams based on their names or email addresses:

    >>> print_people(personset.findTeam('support'))
    Ubuntu Team (ubuntu-team): [u'support@ubuntu.com']

    >>> print_people(personset.findTeam('translators'))
    Ubuntu Translators (ubuntu-translators): []

    >>> print_people(personset.findTeam('team'))
    Another a new team (new3): []
    Hoary Gnome Team (name21): []
    HWDB Team (hwdb-team): []
    Just a new team (new-team): []
    Other Team (otherteam): []
    Simple Team (simple-team): []
    testing Spanish team (testing-spanish-team): []
    Ubuntu Gnome Team (name18): []
    Ubuntu Security Team (ubuntu-security): []
    Ubuntu Team (ubuntu-team): [u'support@ubuntu.com']
    Warty Gnome Team (warty-gnome): []
    Warty Security Team (name20): []

The Owner user is a member of the private team 'myteam' so
the previous search will include myteam in the results.

    >>> login('owner@canonical.com')
    >>> print_people(personset.findTeam('team'))
    Another a new team (new3): []
    Hoary Gnome Team (name21): []
    HWDB Team (hwdb-team): []
    Just a new team (new-team): []
    My Team (myteam): []
    Other Team (otherteam): []
    Simple Team (simple-team): []
    testing Spanish team (testing-spanish-team): []
    Ubuntu Gnome Team (name18): []
    Ubuntu Security Team (ubuntu-security): []
    Ubuntu Team (ubuntu-team): [u'support@ubuntu.com']
    Warty Gnome Team (warty-gnome): []
    Warty Security Team (name20): []

Searching for users with non-ASCII characters in their name works.

    >>> [found_person] = personset.find(u'P\xf6ll\xe4')
    >>> found_person.displayname
    u'Matti P\xf6ll\xe4'

    >>> bjorns_team = factory.makeTeam(salgado, name='bjorn-team',
    ...     displayname=u'Team Bj\xf6rn')
    >>> [found_person] = personset.find(u'Bj\xf6rn')
    >>> found_person.displayname
    u'Team Bj\xf6rn'

You can get the top overall contributors, that is, the people with the
most karma.

    >>> for person in personset.getTopContributors(limit=3):
    ...     print "%s: %s" % (person.name, person.karma)
    name16: 241
    name12: 138
    mark: 130


Packages related to a person
----------------------------

To obtain the packages a person is related to, we can use:

 1. getLatestMaintainedPackages(),
 2. getLatestUploadedButNotMaintainedPackages(),
 3. getLatestUploadedPPAPackages

The 1st will return the latest SourcePackageReleases related to a person
in which he is listed as the Maintainer. The second will return the
latest SourcePackageReleases a person uploaded (and where he isn't the
maintainer).

Both, 1st and 2nd methods, only consider sources upload to primary
archives.

The 3rd method returns SourcePackageReleases uploaded by the person in
question to any PPA.

    >>> mark = personset.getByName('mark')
    >>> for sprelease in mark.getLatestMaintainedPackages():
    ...     print (sprelease.name,
    ...            sprelease.upload_distroseries.fullseriesname,
    ...            sprelease.version)
    (u'alsa-utils', u'Debian Sid', u'1.0.9a-4')
    (u'pmount', u'Ubuntu Hoary', u'0.1-2')
    (u'netapplet', u'Ubuntu Warty', u'0.99.6-1')
    (u'netapplet', u'Ubuntu Hoary', u'1.0-1')
    (u'alsa-utils', u'Ubuntu Warty', u'1.0.8-1ubuntu1')
    (u'mozilla-firefox', u'Ubuntu Warty', u'0.9')
    (u'evolution', u'Ubuntu Hoary', u'1.0')

    >>> for sprelease in mark.getLatestUploadedButNotMaintainedPackages():
    ...     print (sprelease.name,
    ...            sprelease.upload_distroseries.fullseriesname,
    ...            sprelease.version)
    (u'foobar', u'Ubuntu Breezy-autotest', u'1.0')
    (u'cdrkit', u'Ubuntu Breezy-autotest', u'1.0')
    (u'libstdc++', u'Ubuntu Hoary', u'b8p')
    (u'cnews', u'Ubuntu Hoary', u'cr.g7-37')
    (u'linux-source-2.6.15', u'Ubuntu Hoary', u'2.6.15.3')
    (u'alsa-utils', u'Ubuntu Hoary', u'1.0.9a-4ubuntu1')

    >>> mark_spreleases = mark.getLatestUploadedPPAPackages()
    >>> for sprelease in mark_spreleases:
    ...     print (sprelease.name,
    ...            sprelease.version,
    ...            sprelease.creator.name,
    ...            sprelease.maintainer.name,
    ...            sprelease.upload_archive.owner.name,
    ...            sprelease.upload_distroseries.fullseriesname)
    (u'iceweasel', u'1.0', u'mark', u'name16', u'mark', u'Ubuntu Warty')

We will change modify the first SourcePackageRelease to reproduce the
issue mentioned in bug 157303, where source with same creator and
maintainer got omitted from the results:

    >>> any_spr = mark_spreleases[0]
    >>> naked_spr = removeSecurityProxy(any_spr)
    >>> naked_spr.maintainer = mark
    >>> flush_database_updates()

    >>> mark_spreleases = mark.getLatestUploadedPPAPackages()
    >>> for sprelease in mark_spreleases:
    ...     print (sprelease.name,
    ...            sprelease.version,
    ...            sprelease.creator.name,
    ...            sprelease.maintainer.name,
    ...            sprelease.upload_archive.owner.name,
    ...            sprelease.upload_distroseries.fullseriesname)
    (u'iceweasel', u'1.0', u'mark', u'mark', u'mark', u'Ubuntu Warty')


Packages a Person is subscribed to
----------------------------------

IPerson.getBugSubscriberPackages returns this list of packages, sorted
alphabetically by package name.

    >>> login('no-priv@canonical.com')
    >>> from lp.registry.interfaces.distribution import (
    ...     IDistributionSet)
    >>> no_priv = getUtility(IPersonSet).getByName('no-priv')
    >>> ubuntu = getUtility(IDistributionSet).getByName('ubuntu')
    >>> pmount = ubuntu.getSourcePackage('pmount')
    >>> pmount.addBugSubscription(no_priv, no_priv)
    <...StructuralSubscription object at ...>

    >>> mozilla_firefox = ubuntu.getSourcePackage('mozilla-firefox')
    >>> mozilla_firefox.addBugSubscription(no_priv, no_priv)
    <...StructuralSubscription object at ...>

    >>> [package.name for package in no_priv.getBugSubscriberPackages()]
    [u'mozilla-firefox', u'pmount']


Pillars owned or driven by a person or team
-------------------------------------------

To obtain all distributions, project groups and projects owned or driven
by a person or team, we can use the getOwnedOrDrivenPillars() method of
IPerson. This method returns PillarNames ordered by distribution,
project groups and projects.

    >>> login(ANONYMOUS)

    >>> from lp.registry.interfaces.distribution import IDistribution
    >>> from lp.registry.interfaces.product import IProduct
    >>> from lp.registry.interfaces.projectgroup import IProjectGroup

    >>> def print_pillar(pillarname):
    ...     pillar = pillarname.pillar
    ...     if IDistribution.providedBy(pillar):
    ...         pillar_type = 'distribution'
    ...     elif IProjectGroup.providedBy(pillar):
    ...         pillar_type = 'project group'
    ...     elif IProduct.providedBy(pillar):
    ...         pillar_type = 'project'
    ...     print "%s: %s (%s)" % (
    ...         pillar_type, pillar.displayname, pillar.name)

    >>> for pillarname in mark.getOwnedOrDrivenPillars():
    ...     print_pillar(pillarname)
    distribution: Debian (debian)
    distribution: Gentoo (gentoo)
    distribution: Kubuntu (kubuntu)
    distribution: Red Hat (redhat)
    project group: Apache (apache)
    project: Derby (derby)
    project: alsa-utils (alsa-utils)

    >>> for pillarname in ubuntu_team.getOwnedOrDrivenPillars():
    ...     print_pillar(pillarname)
    distribution: Ubuntu (ubuntu)
    distribution: ubuntutest (ubuntutest)
    project: Tomcat (tomcat)


Project owned by a person or team
---------------------------------

To obtain active projects owned by a person or team, we can use the
getOwnedProjects() method of IPerson.  This method returns projects
ordered by displayname.

    >>> for project in mark.getOwnedProjects():
    ...     print project.displayname
    Derby
    Tomcat
    alsa-utils

The method does not return inactive projects.

    >>> login('foo.bar@canonical.com')
    >>> from lp.app.interfaces.launchpad import ILaunchpadCelebrities
    >>> registry_member = factory.makePerson()
    >>> celebs = getUtility(ILaunchpadCelebrities)
    >>> registry = celebs.registry_experts
    >>> ignored = registry.addMember(registry_member, registry.teamowner)

    >>> login_person(registry_member)
    >>> derby = getUtility(IProductSet).getByName('derby')
    >>> derby.active = False
    >>> for project in mark.getOwnedProjects():
    ...     print project.displayname
    Tomcat
    alsa-utils

    >>> for project in ubuntu_team.getOwnedProjects():
    ...     print project.displayname
    Tomcat

David does not own any projects.

    >>> list(ddaa.getOwnedProjects())
    []

The results returned can be filtered by providing a token to refine the
search.

    >>> for project in mark.getOwnedProjects(match_name='java'):
    ...     print project.displayname
    Tomcat

Searching for a non-existent project returns no matches.

    >>> list(mark.getOwnedProjects(match_name='nosuchthing'))
    []


Languages
---------

Users can set their preferred languages, retrievable as
Person.languages.

    >>> daf = personset.getByName('daf')
    >>> carlos = personset.getByName('carlos')

    >>> for language in carlos.languages:
    ...     print language.code, language.englishname
    ca     Catalan
    en     English
    es     Spanish

To add new languages we use Person.addLanguage().

    >>> from lp.services.worlddata.interfaces.language import ILanguageSet
    >>> languageset = getUtility(ILanguageSet)
    >>> carlos.addLanguage(languageset['pt_BR'])
    >>> [lang.code for lang in carlos.languages]
    [u'ca', u'en', u'pt_BR', u'es']

Adding a language which is already in the person's preferred ones will
be a no-op.

    >>> carlos.addLanguage(languageset['es'])
    >>> [lang.code for lang in carlos.languages]
    [u'ca', u'en', u'pt_BR', u'es']

And to remove languages we use Person.removeLanguage().

    >>> carlos.removeLanguage(languageset['pt_BR'])
    >>> [lang.code for lang in carlos.languages]
    [u'ca', u'en', u'es']

Trying to remove a language which is not in the person's preferred ones
will be a no-op.

    >>> carlos.removeLanguage(languageset['pt_BR'])
    >>> [lang.code for lang in carlos.languages]
    [u'ca', u'en', u'es']

The Person.languages list is ordered alphabetically by the languages'
English names.

    >>> for language in daf.languages:
    ...     print language.code, language.englishname
    en_GB  English (United Kingdom)
    ja     Japanese
    cy     Welsh


Specification Lists
-------------------

We should be able to generate lists of specifications for people based
on certain criteria:

First, Carlos does not have any completed specifications assigned to
him:

    >>> from lp.blueprints.enums import SpecificationFilter
    >>> carlos.specifications(filter=[
    ...     SpecificationFilter.ASSIGNEE,
    ...     SpecificationFilter.COMPLETE]).count()
    0

Next, Carlos has three incomplete specs *related* to him:

    >>> filter = []
    >>> for spec in carlos.specifications(filter=filter):
    ...     print spec.name, spec.is_complete, spec.informational
    svg-support False False
    extension-manager-upgrades False True
    media-integrity-check False False

Carlos has 2 specifications assigned to him:

    >>> for spec in carlos.assigned_specs:
    ...     print spec.name
    svg-support
    extension-manager-upgrades

But from these two, only one has started.

    >>> [(spec.name, spec.is_started)
    ...  for spec in carlos.assigned_specs_in_progress]
    [(u'svg-support', True)]

Just for fun, lets check the SAB. He should have one spec for which he
is the approver.

    >>> mark = getUtility(IPersonSet).getByName('mark')
    >>> filter = [SpecificationFilter.APPROVER]
    >>> for spec in mark.specifications(filter=filter):
    ...     print spec.name
    extension-manager-upgrades

The Foo Bar person has a single spec which has feedback requested:

    >>> filter = [SpecificationFilter.FEEDBACK]
    >>> for spec in foobar.specifications(filter=filter):
    ...     print spec.name
    e4x

But has registered 5 of them:

    >>> filter = [SpecificationFilter.CREATOR]
    >>> print foobar.specifications(filter=filter).count()
    5

Now Celso, on the other hand, has 2 specs related to him:

    >>> cprov = personset.getByName('cprov')
    >>> cprov.specifications().count()
    2

On one of those, he is the approver:

    >>> filter = [SpecificationFilter.APPROVER]
    >>> for spec in cprov.specifications(filter=filter):
    ...     print spec.name
    svg-support

And on another one, he is the drafter

    >>> filter = [SpecificationFilter.DRAFTER]
    >>> for spec in cprov.specifications(filter=filter):
    ...     print spec.name
    e4x

We can filter for specifications that contain specific text:

    >>> for spec in cprov.specifications(filter=['svg']):
    ...     print spec.name
    svg-support

Inactive products are excluded from the listings.

    >>> from lp.testing import login
    >>> from lp.registry.interfaces.product import IProductSet
    >>> firefox = getUtility(IProductSet).getByName('firefox')
    >>> login("mark@example.com")

    # Unlink the source packages so the project can be deactivated.
    >>> from lp.testing import unlink_source_packages
    >>> unlink_source_packages(firefox)
    >>> firefox.active = False
    >>> flush_database_updates()
    >>> cprov.specifications(filter=['svg']).count()
    0

Reset firefox so we don't mess up later tests.

    >>> firefox.active = True
    >>> flush_database_updates()


Branches
--------

** See branch.txt for API related to branches.


Distribution uploaders
----------------------

We can ascertain whether a person has uploader rights to a distribution
or not.

    >>> cprov = getUtility(IPersonSet).getByName('cprov')
    >>> ubuntu = getUtility(IDistributionSet)['ubuntu']
    >>> cprov.isUploader(ubuntu)
    True

'kiko' is not an uploader to Ubuntu:

    >>> kiko = getUtility(IPersonSet).getByName('kiko')
    >>> kiko.isUploader(ubuntu)
    False


Bug contribution
----------------

We can check whether a person has any bugs assigned to them, either
within the context of a specific bug target, or in Launchpad in general.

A person with bugs assigned to them in a context is considered a 'Bug
Contributor'.

    >>> from lp.bugs.interfaces.bugtask import BugTaskSearchParams

    >>> cprov.searchTasks(
    ...     BugTaskSearchParams(user=foobar, assignee=cprov)).count()
    0

Celso has no bug tasks assigned to him. In other words, he isn't a bug
contributor.

    >>> cprov.isBugContributor(user=foobar)
    False

We assign a bug task to Celso.

    >>> from lp.bugs.interfaces.bugtask import IBugTaskSet
    >>> search_params = BugTaskSearchParams(user=foobar)
    >>> search_params.setProduct(firefox)
    >>> firefox_bugtask = getUtility(IBugTaskSet).search(search_params)[0]
    >>> firefox_bugtask.transitionToAssignee(cprov)
    >>> flush_database_updates()

Now Celso is a bug contributor in Launchpad.

    >>> cprov.isBugContributor(user=foobar)
    True

Celso is a bug contributer in the context of the `firefox` product.

    >>> from lp.registry.interfaces.projectgroup import IProjectGroupSet
    >>> cprov.isBugContributorInTarget(user=foobar, target=firefox)
    True

And also in the context of the `mozilla` project, by association.

    >>> cprov.isBugContributorInTarget(user=foobar,
    ...     target=getUtility(IProjectGroupSet).getByName('mozilla'))
    True

But not in other contexts.

    >>> cprov.isBugContributorInTarget(user=foobar,
    ...     target=getUtility(IProductSet).getByName('jokosher'))
    False


Creating a Person without an email address
------------------------------------------

Although createPersonAndEmail() is the usual method to use when creating
a new Person, there is also a method that can be used when a Person
needs to be created without an email address, for example when the
Person is being created as the result of an import from an external
bugtracker.

The method createPersonWithoutEmail() is used in these situations. This
takes some parameters similar to those taken by createPersonAndEmail()
but, since an emailless Person cannot be considered to be valid, it
takes no parameters regarding to emails or passwords.

    >>> foo_bar = getUtility(IPersonSet).getByEmail('foo.bar@canonical.com')
    >>> new_person = person_set.createPersonWithoutEmail(
    ...     'ix', PersonCreationRationale.BUGIMPORT,
    ...     comment="when importing bugs", displayname="Ford Prefect",
    ...     registrant=foo_bar)

    >>> print new_person.name
    ix

    >>> print new_person.displayname
    Ford Prefect

    >>> print new_person.preferredemail
    None

    >>> print new_person.creation_rationale.name
    BUGIMPORT

    >>> print new_person.registrant.name
    name16


The _newPerson() method
-----------------------

The PersonSet database class has a method _newPerson(), which is used to
create new Person objects. This isn't exposed in the interface, so to
test it we need to instantiate PersonSet directly.

    >>> from lp.registry.model.person import PersonSet
    >>> person_set = PersonSet()

_newPerson() accepts parameters for name displayname and rationale. It
also takes the parameters hide_email_addresses, comment and registrant.

    >>> person_set._newPerson(
    ...     'new-name', 'New Person', True,
    ...     PersonCreationRationale.BUGIMPORT, "testing _newPerson().",
    ...     foo_bar)
    <Person at ...>

If the name passed to _newPerson() is already taken, a NameAlreadyTaken
error will be raised.

    >>> person_set._newPerson(
    ...     'new-name', 'New Person', True,
    ...     PersonCreationRationale.BUGIMPORT)
    Traceback (most recent call last):
      ...
    NameAlreadyTaken: The name 'new-name' is already taken.

If the name passed to _newPerson() isn't valid an InvalidName error will
be raised.

    >>> person_set._newPerson(
    ...     "ThisIsn'tValid", 'New Person', True,
    ...     PersonCreationRationale.BUGIMPORT)
    Traceback (most recent call last):
      ...
    InvalidName: ThisIsn'tValid is not a valid name for a person.


Probationary users
------------------

Users without karma have not demostrated their intentions and may not
have the same privileges as users who have made contributions. Users who
have made recent contributions are not on probation.

    >>> active_user = personset.getByName('name12')
    >>> active_user.is_probationary
    False

    >>> active_user.karma > 0
    True

    >>> active_user.is_valid_person
    True

New users (those without karma) are on probation.

    >>> new_user = factory.makePerson()
    >>> new_user.is_probationary
    True

    >>> new_user.karma > 0
    False

    >>> new_user.is_valid_person
    True

Teams are never on probation.

    >>> team = factory.makeTeam()
    >>> team.is_probationary
    False

    >>> team.karma > 0
    False

    >>> team.is_valid_person
    False