3 # Copyright 2008 the Melange authors.
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
9 # http://www.apache.org/licenses/LICENSE-2.0
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
17 """Views for Programs.
21 '"Sverre Rabbelier" <sverre@rabbelier.nl>',
22 '"Lennard de Rijk" <ljvderijk@gmail.com>',
28 from django
import forms
29 from django
import http
30 from django
.utils
.translation
import ugettext
32 from soc
.logic
import allocations
33 from soc
.logic
import cleaning
34 from soc
.logic
import dicts
35 from soc
.logic
.helper
import timeline
as timeline_helper
36 from soc
.logic
.models
import host
as host_logic
37 from soc
.logic
.models
import mentor
as mentor_logic
38 from soc
.logic
.models
import organization
as org_logic
39 from soc
.logic
.models
import org_admin
as org_admin_logic
40 from soc
.logic
.models
import org_app
as org_app_logic
41 from soc
.logic
.models
import student_proposal
as student_proposal_logic
42 from soc
.logic
.models
import program
as program_logic
43 from soc
.logic
.models
import student
as student_logic
44 from soc
.views
import helper
45 from soc
.views
import out_of_band
46 from soc
.views
.helper
import access
47 from soc
.views
.helper
import decorators
48 from soc
.views
.helper
import lists
49 from soc
.views
.helper
import redirects
50 from soc
.views
.helper
import widgets
51 from soc
.views
.models
import presence
52 from soc
.views
.models
import document
as document_view
53 from soc
.views
.models
import sponsor
as sponsor_view
54 from soc
.views
.sitemap
import sidebar
56 import soc
.cache
.logic
57 import soc
.logic
.models
.program
58 import soc
.models
.work
61 class View(presence
.View
):
62 """View methods for the Program model.
65 DEF_ACCEPTED_ORGS_MSG_FMT
= ugettext("These organizations have"
66 " been accepted into %(name)s, but they have not yet completed"
67 " their organization profile. You can still learn more about"
68 " each organization by visiting the links below.")
70 DEF_CREATED_ORGS_MSG_FMT
= ugettext("These organizations have been"
71 " accepted into %(name)s and have completed their organization"
72 " profiles. You can learn more about each organization by"
73 " visiting the links below.")
75 DEF_SLOTS_ALLOCATION_MSG
= ugettext("Use this view to assign slots.")
77 def __init__(self
, params
=None):
78 """Defines the fields and methods required for the base View class
79 to provide the user with list, public, create, edit and delete views.
82 params: a dict with params for this View
85 rights
= access
.Checker(params
)
86 rights
['any_access'] = ['allow']
87 rights
['show'] = ['allow']
88 rights
['create'] = [('checkSeeded', ['checkHasActiveRoleForScope',
90 rights
['edit'] = ['checkIsHostForProgram']
91 rights
['delete'] = ['checkIsDeveloper']
92 rights
['assign_slots'] = ['checkIsHostForProgram']
93 rights
['slots'] = ['checkIsHostForProgram']
94 rights
['show_duplicates'] = ['checkIsHostForProgram']
95 rights
['assigned_proposals'] = ['checkIsHostForProgram']
96 rights
['accepted_orgs'] = [('checkisAfterEvent',
97 ['accepted_organization_announced_deadline', '__all__'])]
100 new_params
['logic'] = soc
.logic
.models
.program
.logic
101 new_params
['rights'] = rights
103 new_params
['scope_view'] = sponsor_view
104 new_params
['scope_redirect'] = redirects
.getCreateRedirect
106 new_params
['name'] = "Program"
107 new_params
['sidebar_grouping'] = 'Programs'
108 new_params
['document_prefix'] = "program"
110 new_params
['extra_dynaexclude'] = ['timeline', 'org_admin_agreement',
111 'mentor_agreement', 'student_agreement']
115 (r
'^%(url_name)s/(?P<access_type>assign_slots)/%(key_fields)s$',
116 'soc.views.models.%(module_name)s.assign_slots',
118 (r
'^%(url_name)s/(?P<access_type>slots)/%(key_fields)s$',
119 'soc.views.models.%(module_name)s.slots',
120 'Assign slots (JSON)'),
121 (r
'^%(url_name)s/(?P<access_type>show_duplicates)/%(key_fields)s$',
122 'soc.views.models.%(module_name)s.show_duplicates',
123 'Show duplicate slot assignments'),
124 (r
'^%(url_name)s/(?P<access_type>assigned_proposals)/%(key_fields)s$',
125 'soc.views.models.%(module_name)s.assigned_proposals',
126 "Assigned proposals for multiple organizations"),
127 (r
'^%(url_name)s/(?P<access_type>accepted_orgs)/%(key_fields)s$',
128 'soc.views.models.%(module_name)s.accepted_orgs',
129 "List all accepted organizations"),
132 new_params
['extra_django_patterns'] = patterns
134 new_params
['create_dynafields'] = [
136 'base': forms
.fields
.CharField
,
137 'label': 'Program Link ID',
141 # TODO add clean field to check for uniqueness in link_id and scope_path
142 new_params
['create_extra_dynaproperties'] = {
143 'description': forms
.fields
.CharField(widget
=helper
.widgets
.TinyMCE(
144 attrs
={'rows':10, 'cols':40})),
145 'accepted_orgs_msg': forms
.fields
.CharField(
146 widget
=helper
.widgets
.TinyMCE(attrs
={'rows':10, 'cols':40})),
147 'scope_path': forms
.CharField(widget
=forms
.HiddenInput
, required
=True),
148 'workflow': forms
.ChoiceField(choices
=[('gsoc','Project-based'),
149 ('ghop','Task-based')], required
=True),
153 ('org_admin_agreement_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
154 ugettext('Organization Admin Agreement Document link ID')),
155 ('mentor_agreement_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
156 ugettext('Mentor Agreement Document link ID')),
157 ('student_agreement_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
158 ugettext('Student Agreement Document link ID')),
159 ('home_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
160 ugettext('Home page Document link ID')),
165 for key
, help_text
, label
in reference_fields
:
166 result
[key
] = widgets
.ReferenceField(
167 reference_url
='document', filter=['__scoped__'],
168 filter_fields
={'prefix': new_params
['document_prefix']},
169 required
=False, label
=label
, help_text
=help_text
)
171 result
['workflow'] = forms
.CharField(widget
=widgets
.ReadOnlyInput(),
173 result
['clean'] = cleaning
.clean_refs(new_params
,
174 [i
for i
,_
,_
in reference_fields
])
176 new_params
['edit_extra_dynaproperties'] = result
178 document_references
= [
179 ('org_admin_agreement_link_id', 'org_admin_agreement',
180 lambda x
: x
.org_admin_agreement
),
181 ('mentor_agreement_link_id', 'mentor_agreement',
182 lambda x
: x
.mentor_agreement
),
183 ('student_agreement_link_id', 'student_agreement',
184 lambda x
: x
.student_agreement
),
187 new_params
['references'] = document_references
189 params
= dicts
.merge(params
, new_params
, sub_merge
=True)
191 super(View
, self
).__init
__(params
=params
)
193 def _getAcceptedOrgsList(self
, description
, params
, filter, use_cache
):
194 """Returns a list with all accepted orgs.
197 description: the description of the list
198 params: the params to use
199 filter: the filter to use
200 use_cache: whether or not to use the cache
203 logic
= params
['logic']
210 # only cache if all profiles are created
211 fun
= soc
.cache
.logic
.cache(self
._getData
)
212 entities
= fun(logic
.getModel(), filter, order
, logic
)
214 result
= dicts
.rename(params
, params
['list_params'])
215 result
['action'] = (redirects
.getHomeRedirect
, params
)
216 result
['description'] = description
217 result
['pagination'] = 'soc/list/no_pagination.html'
218 result
['data'] = entities
222 @decorators.merge_params
223 @decorators.check_access
224 def acceptedOrgs(self
, request
, access_type
,
225 page_name
=None, params
=None, filter=None, **kwargs
):
226 """See base.View.list.
230 logic
= params
['logic']
232 program_entity
= logic
.getFromKeyFieldsOr404(kwargs
)
234 fmt
= {'name': program_entity
.name
}
235 description
= self
.DEF_ACCEPTED_ORGS_MSG_FMT
% fmt
238 'status': 'accepted',
239 'scope': program_entity
,
242 from soc
.views
.models
import org_app
as org_app_view
243 aa_params
= org_app_view
.view
.getParams().copy() # accepted applications
245 # define the list redirect action to show the notification
246 del aa_params
['list_key_order']
247 aa_params
['list_action'] = (redirects
.getHomeRedirect
, aa_params
)
248 aa_params
['list_description'] = description
250 aa_list
= lists
.getListContent(request
, aa_params
, filter, idx
=0,
254 contents
.append(aa_list
)
256 use_cache
= not aa_list
# only cache if there are no aa's left
257 description
= self
.DEF_CREATED_ORGS_MSG_FMT
% fmt
259 filter['status'] = ['new', 'active']
261 from soc
.views
.models
.organization
import view
as org_view
262 ao_params
= org_view
.getParams().copy() # active orgs
263 ao_list
= self
._getAcceptedOrgsList
(description
, ao_params
, filter, use_cache
)
265 contents
.append(ao_list
)
267 params
= params
.copy()
268 params
['list_msg'] = program_entity
.accepted_orgs_msg
270 return self
._list
(request
, params
, contents
, page_name
)
272 @decorators.merge_params
273 @decorators.check_access
274 def slots(self
, request
, acces_type
, page_name
=None, params
=None, **kwargs
):
275 """Returns a JSON object with all orgs allocation.
278 request: the standard Django HTTP request object
279 access_type : the name of the access type which should be checked
280 page_name: the page name displayed in templates as page and header title
281 params: a dict with params for this View, not used
284 from django
.utils
import simplejson
286 program
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
287 program_slots
= program
.slots
294 query
= org_logic
.logic
.getQueryForFields(filter=filter)
295 organizations
= org_logic
.logic
.getAll(query
)
297 locked_slots
= adjusted_slots
= {}
299 if request
.method
== 'POST' and 'result' in request
.POST
:
300 result
= request
.POST
['result']
301 submit
= request
.GET
.get('submit')
303 from_json
= simplejson
.loads(result
)
304 locked_slots
= dicts
.groupDictBy(from_json
, 'locked', 'slots')
307 program
.slots_allocation
= result
314 for org
in organizations
:
315 orgs
[org
.link_id
] = org
316 applications
[org
.link_id
] = org
.nr_applications
317 max[org
.link_id
] = min(org
.nr_mentors
, org
.slots_desired
)
319 max_slots_per_org
= program
.max_slots
320 min_slots_per_org
= program
.min_slots
323 allocator
= allocations
.Allocator(orgs
.keys(), applications
, max,
324 program_slots
, max_slots_per_org
,
325 min_slots_per_org
, algorithm
)
327 result
= allocator
.allocate(locked_slots
)
331 # TODO: remove adjustment here and in the JS
332 for link_id
, count
in result
.iteritems():
337 'locked': locked_slots
.get(link_id
, 0),
338 'adjustment': adjusted_slots
.get(link_id
, 0),
341 return self
.json(request
, data
)
343 @decorators.merge_params
344 @decorators.check_access
345 def assignSlots(self
, request
, access_type
, page_name
=None,
346 params
=None, **kwargs
):
347 """View that allows to assign slots to orgs.
350 from soc
.views
.models
import organization
as organization_view
352 org_params
= organization_view
.view
.getParams().copy()
353 org_params
['list_template'] = 'soc/program/allocation/allocation.html'
354 org_params
['list_heading'] = 'soc/program/allocation/heading.html'
355 org_params
['list_row'] = 'soc/program/allocation/row.html'
356 org_params
['list_pagination'] = 'soc/list/no_pagination.html'
358 program
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
360 description
= self
.DEF_SLOTS_ALLOCATION_MSG
367 content
= self
._getAcceptedOrgsList
(description
, org_params
, filter, False)
371 return_url
= "http://%(host)s%(index)s" % {
372 'host' : os
.environ
['HTTP_HOST'],
373 'index': redirects
.getSlotsRedirect(program
, params
)
377 'total_slots': program
.slots
,
379 'uses_slot_allocator': True,
380 'return_url': return_url
,
383 return self
._list
(request
, org_params
, contents
, page_name
, context
)
385 @decorators.merge_params
386 @decorators.check_access
387 def showDuplicates(self
, request
, access_type
, page_name
=None,
388 params
=None, **kwargs
):
389 """View in which a host can see which students have been assigned
392 For params see base.view.Public().
395 from django
.utils
import simplejson
397 from soc
.logic
.models
.proposal_duplicates
import logic
as duplicates_logic
399 program_entity
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
401 if request
.POST
and request
.POST
.get('result'):
402 # store result in the datastore
403 fields
= {'link_id': program_entity
.link_id
,
404 'scope': program_entity
,
405 'scope_path': program_entity
.key().name(),
406 'json_representation' : request
.POST
['result']
408 key_name
= duplicates_logic
.getKeyNameFromFields(fields
)
409 duplicates_logic
.updateOrCreateFromKeyName(fields
, key_name
)
411 response
= simplejson
.dumps({'status': 'done'})
412 return http
.HttpResponse(response
)
414 context
= helper
.responses
.getUniversalContext(request
)
415 helper
.responses
.useJavaScript(context
, params
['js_uses_all'])
416 context
['uses_duplicates'] = True
417 context
['uses_json'] = True
418 context
['page_name'] = page_name
420 # get all orgs for this program who are active and have slots assigned
421 fields
= {'scope': program_entity
,
425 query
= org_logic
.logic
.getQueryForFields(fields
)
428 'nr_of_orgs': query
.count(),
429 'program_key': program_entity
.key().name()}
430 json
= simplejson
.dumps(to_json
)
431 context
['info'] = json
432 context
['offset_length'] = 10
434 fields
= {'link_id': program_entity
.link_id
,
435 'scope': program_entity
}
436 duplicates
= duplicates_logic
.getForFields(fields
, unique
=True)
439 # we have stored information
440 context
['duplicate_cache_content'] = duplicates
.json_representation
441 context
['date_of_calculation'] = duplicates
.calculated_on
443 # no information stored
444 context
['duplicate_cache_content'] = simplejson
.dumps({})
446 template
= 'soc/program/show_duplicates.html'
448 return helper
.responses
.respond(request
, template
=template
, context
=context
)
450 @decorators.merge_params
451 @decorators.check_access
452 def assignedProposals(self
, request
, access_type
, page_name
=None,
453 params
=None, filter=None, **kwargs
):
454 """Returns a JSON dict containing all the proposals that would have
455 a slot assigned for a specific set of orgs.
457 The request.GET limit and offset determines how many and which
458 organizations should be returned.
460 For params see base.View.public().
462 Returns: JSON object with a collection of orgs and proposals. Containing
463 identification information and contact information.
466 get_dict
= request
.GET
468 if not (get_dict
.get('limit') and get_dict
.get('offset')):
469 return self
.json(request
, {})
472 limit
= max(0, int(get_dict
['limit']))
473 offset
= max(0, int(get_dict
['offset']))
475 return self
.json(request
, {})
477 program_entity
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
479 fields
= {'scope': program_entity
,
483 org_entities
= org_logic
.logic
.getForFields(fields
,
484 limit
=limit
, offset
=offset
)
489 # for each org get the proposals who will be assigned a slot
490 for org
in org_entities
:
492 org_data
= {'name': org
.name
}
494 fields
= {'scope': org
,
498 org_admin
= org_admin_logic
.logic
.getForFields(fields
, unique
=True)
501 org_data
['admin_name'] = org_admin
.name()
502 org_data
['admin_email'] = org_admin
.email
504 # check if there are already slots taken by this org
505 fields
= {'org': org
,
506 'status': 'accepted'}
508 query
= student_proposal_logic
.logic
.getQueryForFields(fields
)
510 slots_left_to_assign
= max(0, org
.slots
- query
.count())
512 if slots_left_to_assign
== 0:
513 # no slots left so next org
516 # store information about the org
517 orgs_data
[org
.key().name()] = org_data
519 fields
= {'org': org
,
524 # get the the number of proposals that would be assigned a slot
525 student_proposal_entities
= student_proposal_logic
.logic
.getForFields(
526 fields
, limit
=slots_left_to_assign
, order
=order
)
528 # store each proposal in the dictionary
529 for proposal
in student_proposal_entities
:
530 student_entity
= proposal
.scope
532 proposals_data
.append(
533 {'key_name': proposal
.key().name(),
534 'proposal_title': proposal
.title
,
535 'student_key': student_entity
.key().name(),
536 'student_name': student_entity
.name(),
537 'student_contact': student_entity
.email
,
538 'org_key': org
.key().name()
541 # return all the data in JSON format
542 data
= {'orgs': orgs_data
,
543 'proposals': proposals_data
}
545 return self
.json(request
, data
)
547 def _editPost(self
, request
, entity
, fields
):
548 """See base._editPost().
551 super(View
, self
)._editPost
(request
, entity
, fields
)
554 # there is no existing entity so create a new timeline
555 fields
['timeline'] = self
._createTimelineForType
(fields
)
557 # use the timeline from the entity
558 fields
['timeline'] = entity
.timeline
560 def _createTimelineForType(self
, fields
):
561 """Creates and stores a timeline model for the given type of program.
564 workflow
= fields
['workflow']
566 timeline_logic
= program_logic
.logic
.TIMELINE_LOGIC
[workflow
]
568 properties
= timeline_logic
.getKeyFieldsFromFields(fields
)
569 key_name
= timeline_logic
.getKeyNameFromFields(properties
)
571 properties
['scope'] = fields
['scope']
573 timeline
= timeline_logic
.updateOrCreateFromKeyName(properties
, key_name
)
576 @decorators.merge_params
577 def getExtraMenus(self
, id, user
, params
=None):
578 """Returns the extra menu's for this view.
580 A menu item is generated for each program that is currently
581 running. The public page for each program is added as menu item,
582 as well as all public documents for that program.
585 params: a dict with params for this View.
588 logic
= params
['logic']
589 rights
= params
['rights']
591 # only get all invisible and visible programs
592 fields
= {'status': ['invisible', 'visible']}
593 entities
= logic
.getForFields(fields
)
597 rights
.setCurrentUser(id, user
)
599 for entity
in entities
:
602 if entity
.status
== 'visible':
603 # show the documents for this program, even for not logged in users
604 items
+= document_view
.view
.getMenusForScope(entity
, params
)
605 items
+= self
._getTimeDependentEntries
(entity
, params
, id, user
)
608 # check if the current user is a host for this program
609 rights
.doCachedCheck('checkIsHostForProgram',
610 {'scope_path': entity
.scope_path
,
611 'link_id': entity
.link_id
}, [])
613 if entity
.status
== 'invisible':
614 # still add the document links so hosts can see how it looks like
615 items
+= document_view
.view
.getMenusForScope(entity
, params
)
616 items
+= self
._getTimeDependentEntries
(entity
, params
, id, user
)
618 items
+= [(redirects
.getReviewOverviewRedirect(
619 entity
, {'url_name': 'org_app'}),
620 "Review Organization Applications", 'any_access')]
621 # add link to edit Program Profile
622 items
+= [(redirects
.getEditRedirect(entity
, params
),
623 'Edit Program Profile','any_access')]
624 # add link to Assign Slots
625 items
+= [(redirects
.getAssignSlotsRedirect(entity
, params
),
626 'Assign Slots','any_access')]
627 # add link to edit Program Timeline
628 items
+= [(redirects
.getEditRedirect(entity
, {'url_name': 'timeline'}),
629 "Edit Program Timeline", 'any_access')]
630 # add link to create a new Program Document
631 items
+= [(redirects
.getCreateDocumentRedirect(entity
, 'program'),
632 "Create a New Document", 'any_access')]
633 # add link to list all Program Document
634 items
+= [(redirects
.getListDocumentsRedirect(entity
, 'program'),
635 "List Documents", 'any_access')]
637 except out_of_band
.Error
:
640 items
= sidebar
.getSidebarMenu(id, user
, items
, params
=params
)
645 menu
['heading'] = entity
.short_name
646 menu
['items'] = items
647 menu
['group'] = 'Programs'
652 def _getTimeDependentEntries(self
, program_entity
, params
, id, user
):
653 """Returns a list with time dependent menu items.
657 #TODO(ljvderijk) Add more timeline dependent entries
658 timeline_entity
= program_entity
.timeline
660 if timeline_helper
.isActivePeriod(timeline_entity
, 'org_signup'):
661 # add the organization signup link
663 (redirects
.getApplyRedirect(program_entity
, {'url_name': 'org_app'}),
664 "Apply to become an Organization", 'any_access')]
666 if user
and timeline_helper
.isAfterEvent(timeline_entity
,
670 'scope': program_entity
,
673 if org_app_logic
.logic
.getForFields(filter, unique
=True):
674 # add the 'List my Organization Applications' link
676 (redirects
.getListSelfRedirect(program_entity
,
677 {'url_name' : 'org_app'}),
678 "List My Organization Applications", 'any_access')]
680 # get the student entity for this user and program
681 filter = {'user': user
,
682 'scope': program_entity
,
684 student_entity
= student_logic
.logic
.getForFields(filter, unique
=True)
687 items
+= self
._getStudentEntries
(program_entity
, student_entity
,
690 # get mentor and org_admin entity for this user and program
691 filter = {'user': user
,
692 'program': program_entity
,
694 mentor_entity
= mentor_logic
.logic
.getForFields(filter, unique
=True)
695 org_admin_entity
= org_admin_logic
.logic
.getForFields(filter, unique
=True)
697 if mentor_entity
or org_admin_entity
:
698 items
+= self
._getOrganizationEntries
(program_entity
, org_admin_entity
,
699 mentor_entity
, params
, id, user
)
701 if user
and not (student_entity
or mentor_entity
or org_admin_entity
):
702 if timeline_helper
.isActivePeriod(timeline_entity
, 'student_signup'):
703 # this user does not have a role yet for this program
704 items
+= [('/student/apply/%s' % (program_entity
.key().name()),
705 "Register as a Student", 'any_access')]
707 deadline
= 'accepted_organization_announced_deadline'
709 if timeline_helper
.isAfterEvent(timeline_entity
, deadline
):
710 url
= redirects
.getAcceptedOrgsRedirect(program_entity
, params
)
711 # add a link to list all the organizations
712 items
+= [(url
, "List participating Organizations", 'any_access')]
714 if not student_entity
:
715 # add apply to become a mentor link
716 items
+= [('/org/apply_mentor/%s' % (program_entity
.key().name()),
717 "Apply to become a Mentor", 'any_access')]
721 def _getStudentEntries(self
, program_entity
, student_entity
,
723 """Returns a list with menu items for students in a specific program.
728 timeline_entity
= program_entity
.timeline
730 if timeline_helper
.isActivePeriod(timeline_entity
, 'student_signup'):
731 items
+= [('/student_proposal/list_orgs/%s' % (
732 student_entity
.key().name()),
733 "Submit your Student Proposal", 'any_access')]
735 if timeline_helper
.isAfterEvent(timeline_entity
, 'student_signup_start'):
736 items
+= [(redirects
.getListSelfRedirect(student_entity
,
737 {'url_name':'student_proposal'}),
738 "List my Student Proposals", 'any_access')]
740 items
+= [(redirects
.getEditRedirect(student_entity
,
741 {'url_name': 'student'}),
742 "Edit my Student Profile", 'any_access')]
746 def _getOrganizationEntries(self
, program_entity
, org_admin_entity
,
747 mentor_entity
, params
, id, user
):
748 """Returns a list with menu items for org admins and mentors in a
752 # TODO(ljvderijk) think about adding specific org items like submit review
761 accepted_orgs
= decorators
.view(view
.acceptedOrgs
)
762 admin
= decorators
.view(view
.admin
)
763 assign_slots
= decorators
.view(view
.assignSlots
)
764 assigned_proposals
= decorators
.view(view
.assignedProposals
)
765 create
= decorators
.view(view
.create
)
766 delete
= decorators
.view(view
.delete
)
767 edit
= decorators
.view(view
.edit
)
768 list = decorators
.view(view
.list)
769 public
= decorators
.view(view
.public
)
770 export
= decorators
.view(view
.export
)
771 show_duplicates
= decorators
.view(view
.showDuplicates
)
772 slots
= decorators
.view(view
.slots
)
773 home
= decorators
.view(view
.home
)
774 pick
= decorators
.view(view
.pick
)