Change iterative to algorithm in slot allocator
[Melange.git] / app / soc / views / models / program.py
blob12511a2b8781300b4cf23eedc72db69368dd3772
1 #!/usr/bin/python2.5
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.
18 """
20 __authors__ = [
21 '"Sverre Rabbelier" <sverre@rabbelier.nl>',
22 '"Lennard de Rijk" <ljvderijk@gmail.com>',
26 import os
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.
63 """
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.
81 Params:
82 params: a dict with params for this View
83 """
85 rights = access.Checker(params)
86 rights['any_access'] = ['allow']
87 rights['show'] = ['allow']
88 rights['create'] = [('checkSeeded', ['checkHasActiveRoleForScope',
89 host_logic.logic])]
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__'])]
99 new_params = {}
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']
113 patterns = []
114 patterns += [
115 (r'^%(url_name)s/(?P<access_type>assign_slots)/%(key_fields)s$',
116 'soc.views.models.%(module_name)s.assign_slots',
117 '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'] = [
135 {'name': 'link_id',
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),
152 reference_fields = [
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')),
163 result = {}
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(),
172 required=True)
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.
196 Args:
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']
205 order = ['name']
207 if not use_cache:
208 fun = self._getData
209 else:
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
220 return result
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.
229 contents = []
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
237 filter = {
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,
251 need_content=True)
253 if aa_list:
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.
277 Args:
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
289 filter = {
290 'scope': program,
291 'status': 'active',
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')
306 if submit:
307 program.slots_allocation = result
308 program.put()
310 orgs = {}
311 applications = {}
312 max = {}
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
321 algorithm = 1
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)
329 data = []
331 # TODO: remove adjustment here and in the JS
332 for link_id, count in result.iteritems():
333 org = orgs[link_id]
334 data.append({
335 'link_id': link_id,
336 'slots': count,
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
362 filter = {
363 'scope': program,
364 'status': 'active',
367 content = self._getAcceptedOrgsList(description, org_params, filter, False)
369 contents = [content]
371 return_url = "http://%(host)s%(index)s" % {
372 'host' : os.environ['HTTP_HOST'],
373 'index': redirects.getSlotsRedirect(program, params)
376 context = {
377 'total_slots': program.slots,
378 'uses_json': True,
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
390 multiple slots.
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,
422 'slots >': 0,
423 'status': 'active'}
425 query = org_logic.logic.getQueryForFields(fields)
427 to_json = {
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)
438 if duplicates:
439 # we have stored information
440 context['duplicate_cache_content'] = duplicates.json_representation
441 context['date_of_calculation'] = duplicates.calculated_on
442 else:
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, {})
471 try:
472 limit = max(0, int(get_dict['limit']))
473 offset = max(0, int(get_dict['offset']))
474 except ValueError:
475 return self.json(request, {})
477 program_entity = program_logic.logic.getFromKeyFieldsOr404(kwargs)
479 fields = {'scope': program_entity,
480 'slots >': 0,
481 'status': 'active'}
483 org_entities = org_logic.logic.getForFields(fields,
484 limit=limit, offset=offset)
486 orgs_data = {}
487 proposals_data = []
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,
495 'status': 'active',
496 'user': org.founder}
498 org_admin = org_admin_logic.logic.getForFields(fields, unique=True)
500 if org_admin:
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
514 continue
516 # store information about the org
517 orgs_data[org.key().name()] = org_data
519 fields = {'org': org,
520 'mentor !=': None,
521 'status': 'pending'}
522 order = ['-score']
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)
553 if not entity:
554 # there is no existing entity so create a new timeline
555 fields['timeline'] = self._createTimelineForType(fields)
556 else:
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)
574 return timeline
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.
584 Args:
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)
595 menus = []
597 rights.setCurrentUser(id, user)
599 for entity in entities:
600 items = []
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)
607 try:
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:
638 pass
640 items = sidebar.getSidebarMenu(id, user, items, params=params)
641 if not items:
642 continue
644 menu = {}
645 menu['heading'] = entity.short_name
646 menu['items'] = items
647 menu['group'] = 'Programs'
648 menus.append(menu)
650 return menus
652 def _getTimeDependentEntries(self, program_entity, params, id, user):
653 """Returns a list with time dependent menu items.
655 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
662 items += [
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,
667 'org_signup_start'):
668 filter = {
669 'applicant': user,
670 'scope': program_entity,
673 if org_app_logic.logic.getForFields(filter, unique=True):
674 # add the 'List my Organization Applications' link
675 items += [
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,
683 'status': 'active'}
684 student_entity = student_logic.logic.getForFields(filter, unique=True)
686 if student_entity:
687 items += self._getStudentEntries(program_entity, student_entity,
688 params, id, user)
690 # get mentor and org_admin entity for this user and program
691 filter = {'user': user,
692 'program': program_entity,
693 'status': 'active'}
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')]
719 return items
721 def _getStudentEntries(self, program_entity, student_entity,
722 params, id, user):
723 """Returns a list with menu items for students in a specific program.
726 items = []
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')]
744 return items
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
749 specific program.
752 # TODO(ljvderijk) think about adding specific org items like submit review
754 items = []
756 return items
759 view = View()
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)