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
import simplejson
31 from django
.utils
.translation
import ugettext
33 from soc
.logic
import allocations
34 from soc
.logic
import cleaning
35 from soc
.logic
import dicts
36 from soc
.logic
.helper
import timeline
as timeline_helper
37 from soc
.logic
.models
import host
as host_logic
38 from soc
.logic
.models
import mentor
as mentor_logic
39 from soc
.logic
.models
import organization
as org_logic
40 from soc
.logic
.models
import org_admin
as org_admin_logic
41 from soc
.logic
.models
import org_app
as org_app_logic
42 from soc
.logic
.models
import student_proposal
as student_proposal_logic
43 from soc
.logic
.models
import program
as program_logic
44 from soc
.logic
.models
import student
as student_logic
45 from soc
.logic
.models
.document
import logic
as document_logic
46 from soc
.views
import helper
47 from soc
.views
import out_of_band
48 from soc
.views
.helper
import access
49 from soc
.views
.helper
import decorators
50 from soc
.views
.helper
import lists
51 from soc
.views
.helper
import redirects
52 from soc
.views
.helper
import widgets
53 from soc
.views
.models
import presence
54 from soc
.views
.models
import document
as document_view
55 from soc
.views
.models
import sponsor
as sponsor_view
56 from soc
.views
.sitemap
import sidebar
58 import soc
.logic
.models
.program
59 import soc
.models
.work
62 class View(presence
.View
):
63 """View methods for the Program model.
66 def __init__(self
, params
=None):
67 """Defines the fields and methods required for the base View class
68 to provide the user with list, public, create, edit and delete views.
71 params: a dict with params for this View
74 rights
= access
.Checker(params
)
75 rights
['any_access'] = ['allow']
76 rights
['show'] = ['allow']
77 rights
['create'] = [('checkSeeded', ['checkHasActiveRoleForScope',
79 rights
['edit'] = ['checkIsHostForProgram']
80 rights
['delete'] = ['checkIsDeveloper']
81 rights
['assign_slots'] = ['checkIsDeveloper']
82 rights
['slots'] = ['checkIsDeveloper']
83 rights
['show_duplicates'] = ['checkIsHostForProgram']
84 rights
['assigned_proposals'] = ['checkIsHostForProgram']
87 new_params
['logic'] = soc
.logic
.models
.program
.logic
88 new_params
['rights'] = rights
90 new_params
['scope_view'] = sponsor_view
91 new_params
['scope_redirect'] = redirects
.getCreateRedirect
93 new_params
['name'] = "Program"
94 new_params
['sidebar_grouping'] = 'Programs'
95 new_params
['document_prefix'] = "program"
97 new_params
['extra_dynaexclude'] = ['timeline', 'org_admin_agreement',
98 'mentor_agreement', 'student_agreement']
102 (r
'^%(url_name)s/(?P<access_type>assign_slots)/%(key_fields)s$',
103 'soc.views.models.%(module_name)s.assign_slots',
105 (r
'^%(url_name)s/(?P<access_type>slots)/%(key_fields)s$',
106 'soc.views.models.%(module_name)s.slots',
107 'Assign slots (JSON)'),
108 (r
'^%(url_name)s/(?P<access_type>show_duplicates)/%(key_fields)s$',
109 'soc.views.models.%(module_name)s.show_duplicates',
110 'Show duplicate slot assignments'),
111 (r
'^%(url_name)s/(?P<access_type>assigned_proposals)/%(key_fields)s$',
112 'soc.views.models.%(module_name)s.assigned_proposals',
113 "Assigned proposals for multiple orgs"),
116 new_params
['extra_django_patterns'] = patterns
118 # TODO add clean field to check for uniqueness in link_id and scope_path
119 new_params
['create_extra_dynaproperties'] = {
120 'description': forms
.fields
.CharField(widget
=helper
.widgets
.TinyMCE(
121 attrs
={'rows':10, 'cols':40})),
122 'scope_path': forms
.CharField(widget
=forms
.HiddenInput
, required
=True),
123 'workflow': forms
.ChoiceField(choices
=[('gsoc','Project-based'),
124 ('ghop','Task-based')], required
=True),
128 ('org_admin_agreement_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
129 ugettext('Organization Admin Agreement Document link ID')),
130 ('mentor_agreement_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
131 ugettext('Mentor Agreement Document link ID')),
132 ('student_agreement_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
133 ugettext('Student Agreement Document link ID')),
134 ('home_link_id', soc
.models
.work
.Work
.link_id
.help_text
,
135 ugettext('Home page Document link ID')),
140 for key
, help_text
, label
in reference_fields
:
141 result
[key
] = widgets
.ReferenceField(
142 reference_url
='document', filter=['__scoped__'],
143 filter_fields
={'prefix': new_params
['document_prefix']},
144 required
=False, label
=label
, help_text
=help_text
)
146 result
['workflow'] = forms
.CharField(widget
=widgets
.ReadOnlyInput(),
148 result
['clean'] = cleaning
.clean_refs(new_params
,
149 [i
for i
,_
,_
in reference_fields
])
151 new_params
['edit_extra_dynaproperties'] = result
153 document_references
= [
154 ('org_admin_agreement_link_id', 'org_admin_agreement',
155 lambda x
: x
.org_admin_agreement
),
156 ('mentor_agreement_link_id', 'mentor_agreement',
157 lambda x
: x
.mentor_agreement
),
158 ('student_agreement_link_id', 'student_agreement',
159 lambda x
: x
.student_agreement
),
162 new_params
['references'] = document_references
164 params
= dicts
.merge(params
, new_params
, sub_merge
=True)
166 super(View
, self
).__init
__(params
=params
)
168 @decorators.merge_params
169 @decorators.check_access
170 def slots(self
, request
, acces_type
, page_name
=None, params
=None, **kwargs
):
171 """Returns a JSON object with all orgs allocation.
174 request: the standard Django HTTP request object
175 access_type : the name of the access type which should be checked
176 page_name: the page name displayed in templates as page and header title
177 params: a dict with params for this View, not used
180 program
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
181 slots
= program
.slots
188 query
= org_logic
.logic
.getQueryForFields(filter=filter)
189 organizations
= org_logic
.logic
.getAll(query
)
191 locked_slots
= adjusted_slots
= {}
193 if request
.method
== 'POST' and 'result' in request
.POST
:
194 result
= request
.POST
['result']
196 from_json
= simplejson
.loads(result
)
198 locked_slots
= dicts
.groupDictBy(from_json
, 'locked', 'slots')
199 adjusted_slots
= dicts
.groupDictBy(from_json
, 'adjustment')
205 for org
in organizations
:
208 'status': ['new', 'pending']
210 orgs
[org
.link_id
] = org
211 query
= student_proposal_logic
.logic
.getQueryForFields(filter=filter)
212 proposals
= student_proposal_logic
.logic
.getAll(query
)
213 applications
[org
.link_id
] = len(proposals
)
214 mentors
[org
.link_id
] = len([i
for i
in proposals
if i
.mentor
!= None])
216 # TODO: Use configuration variables here
217 max_slots_per_org
= 40
218 min_slots_per_org
= 2
221 allocator
= allocations
.Allocator(orgs
.keys(), applications
, mentors
,
222 slots
, max_slots_per_org
,
223 min_slots_per_org
, iterative
)
225 result
= allocator
.allocate(locked_slots
, adjusted_slots
)
229 for link_id
, count
in result
.iteritems():
234 'locked': locked_slots
.get(link_id
, 0),
235 'adjustment': adjusted_slots
.get(link_id
, 0),
238 return self
.json(request
, data
)
240 @decorators.merge_params
241 @decorators.check_access
242 def assignSlots(self
, request
, access_type
, page_name
=None,
243 params
=None, **kwargs
):
244 """View that allows to assign slots to orgs.
247 from soc
.views
.models
import organization
as organization_view
249 org_params
= organization_view
.view
.getParams().copy()
250 org_params
['list_template'] = 'soc/program/allocation/allocation.html'
251 org_params
['list_heading'] = 'soc/program/allocation/heading.html'
252 org_params
['list_row'] = 'soc/program/allocation/row.html'
253 org_params
['list_pagination'] = 'soc/list/no_pagination.html'
255 program
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
262 content
= lists
.getListContent(request
, org_params
, filter=filter)
265 return_url
= "http://%(host)s%(index)s" % {
266 'host' : os
.environ
['HTTP_HOST'],
267 'index': redirects
.getSlotsRedirect(program
, params
)
271 'total_slots': program
.slots
,
273 'uses_slot_allocator': True,
274 'return_url': return_url
,
277 return self
._list
(request
, org_params
, contents
, page_name
, context
)
279 @decorators.merge_params
280 @decorators.check_access
281 def showDuplicates(self
, request
, access_type
, page_name
=None,
282 params
=None, **kwargs
):
283 """View in which a host can see which students have been assigned multiple slots.
285 For params see base.view.Public().
288 from django
.utils
import simplejson
290 program_entity
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
292 context
= helper
.responses
.getUniversalContext(request
)
293 helper
.responses
.useJavaScript(context
, params
['js_uses_all'])
294 context
['page_name'] = page_name
296 # get all orgs for this program who are active and have slots assigned
297 fields
= {'scope': program_entity
,
301 query
= org_logic
.logic
.getQueryForFields(fields
)
304 'nr_of_orgs': query
.count(),
305 'program_key': program_entity
.key().name()}
306 json
= simplejson
.dumps(to_json
)
307 context
['info'] = json
309 # TODO(ljvderijk) cache the result of the duplicate calculation
310 context
['duplicate_cache_content'] = simplejson
.dumps({})
312 template
= 'soc/program/show_duplicates.html'
314 return helper
.responses
.respond(request
, template
=template
, context
=context
)
316 @decorators.merge_params
317 @decorators.check_access
318 def assignedProposals(self
, request
, access_type
, page_name
=None,
319 params
=None, filter=None, **kwargs
):
320 """Returns a JSON dict containing all the proposals that would have
321 a slot assigned for a specific set of orgs.
323 The request.GET limit and offset determines how many and which
324 organizations should be returned.
326 For params see base.View.public().
328 Returns: JSON object with a collection of orgs and proposals. Containing
329 identification information and contact information.
332 get_dict
= request
.GET
334 if not (get_dict
.get('limit') or get_dict
.get('offset')):
335 return self
.json(request
, {})
338 limit
= max(0, int(get_dict
['limit']))
339 offset
= max(0, int(get_dict
['offset']))
341 return self
.json(request
, {})
343 program_entity
= program_logic
.logic
.getFromKeyFieldsOr404(kwargs
)
345 fields
= {'scope': program_entity
,
349 org_entities
= org_logic
.logic
.getForFields(fields
, limit
=limit
, offset
=offset
)
354 # for each org get the proposals who will be assigned a slot
355 for org
in org_entities
:
357 org_data
= {'name': org
.name
}
359 fields
= {'scope': org
,
363 org_admin
= org_admin_logic
.logic
.getForFields(fields
, unique
=True)
366 org_data
['admin_name'] = org_admin
.name()
367 org_data
['admin_email'] = org_admin
.email
369 # check if there are already slots taken by this org
370 fields
= {'org': org
,
371 'status': 'accepted'}
373 query
= student_proposal_logic
.logic
.getQueryForFields(fields
)
376 slots_left_to_assign
= max(0, org
.slots
- query
.count())
378 if slots_left_to_assign
== 0:
379 # no slots left so next org
382 # store information about the org
383 orgs_data
[org
.key().name()] = org_data
385 fields
= {'org': org
,
390 # get the the number of proposals that would be assigned a slot
391 student_proposal_entities
= student_proposal_logic
.logic
.getForFields(
392 fields
, limit
=slots_left_to_assign
, order
=order
)
396 # store each proposal in the dictionary
397 for proposal
in student_proposal_entities
:
398 student_entity
= proposal
.scope
400 proposals_data
[proposal
.key().name()] = {
401 'proposal_title': proposal
.title
,
402 'student_key': student_entity
.key().name(),
403 'student_name': student_entity
.name(),
404 'student_contact': student_entity
.email
,
405 'org_key': org
.key().name()
408 # store it with the other org data
409 proposals_data
['proposals'] = proposal_data
411 # return all the data in JSON format
412 data
= {'orgs': orgs_data
,
413 'proposals': proposals_data
}
415 return self
.json(request
, data
)
417 def _editPost(self
, request
, entity
, fields
):
418 """See base._editPost().
421 super(View
, self
)._editPost
(request
, entity
, fields
)
424 # there is no existing entity so create a new timeline
425 fields
['timeline'] = self
._createTimelineForType
(fields
)
427 # use the timeline from the entity
428 fields
['timeline'] = entity
.timeline
430 def _createTimelineForType(self
, fields
):
431 """Creates and stores a timeline model for the given type of program.
434 workflow
= fields
['workflow']
436 timeline_logic
= program_logic
.logic
.TIMELINE_LOGIC
[workflow
]
438 properties
= timeline_logic
.getKeyFieldsFromFields(fields
)
439 key_name
= timeline_logic
.getKeyNameFromFields(properties
)
441 properties
['scope'] = fields
['scope']
443 timeline
= timeline_logic
.updateOrCreateFromKeyName(properties
, key_name
)
446 @decorators.merge_params
447 def getExtraMenus(self
, id, user
, params
=None):
448 """Returns the extra menu's for this view.
450 A menu item is generated for each program that is currently
451 running. The public page for each program is added as menu item,
452 as well as all public documents for that program.
455 params: a dict with params for this View.
458 logic
= params
['logic']
459 rights
= params
['rights']
461 # only get all invisible and visible programs
462 fields
= {'status': ['invisible', 'visible']}
463 entities
= logic
.getForFields(fields
)
467 rights
.setCurrentUser(id, user
)
469 for entity
in entities
:
472 if entity
.status
== 'visible':
473 # show the documents for this program, even for not logged in users
474 items
+= document_view
.view
.getMenusForScope(entity
, params
)
475 items
+= self
._getTimeDependentEntries
(entity
, params
, id, user
)
478 # check if the current user is a host for this program
479 rights
.doCachedCheck('checkIsHostForProgram',
480 {'scope_path': entity
.scope_path
,
481 'link_id': entity
.link_id
}, [])
483 if entity
.status
== 'invisible':
484 # still add the document links so hosts can see how it looks like
485 items
+= document_view
.view
.getMenusForScope(entity
, params
)
486 items
+= self
._getTimeDependentEntries
(entity
, params
, id, user
)
488 items
+= [(redirects
.getReviewOverviewRedirect(
489 entity
, {'url_name': 'org_app'}),
490 "Review Organization Applications", 'any_access')]
491 # add link to edit Program Profile
492 items
+= [(redirects
.getEditRedirect(entity
, params
),
493 'Edit Program Profile','any_access')]
494 # add link to edit Program Timeline
495 items
+= [(redirects
.getEditRedirect(entity
, {'url_name': 'timeline'}),
496 "Edit Program Timeline", 'any_access')]
497 # add link to create a new Program Document
498 items
+= [(redirects
.getCreateDocumentRedirect(entity
, 'program'),
499 "Create a New Document", 'any_access')]
500 # add link to list all Program Document
501 items
+= [(redirects
.getListDocumentsRedirect(entity
, 'program'),
502 "List Documents", 'any_access')]
504 except out_of_band
.Error
:
507 items
= sidebar
.getSidebarMenu(id, user
, items
, params
=params
)
512 menu
['heading'] = entity
.short_name
513 menu
['items'] = items
514 menu
['group'] = 'Programs'
519 def _getTimeDependentEntries(self
, program_entity
, params
, id, user
):
520 """Returns a list with time dependent menu items.
524 #TODO(ljvderijk) Add more timeline dependent entries
525 timeline_entity
= program_entity
.timeline
527 if timeline_helper
.isActivePeriod(timeline_entity
, 'org_signup'):
528 # add the organization signup link
530 (redirects
.getApplyRedirect(program_entity
, {'url_name': 'org_app'}),
531 "Apply to become an Organization", 'any_access')]
533 if user
and timeline_helper
.isAfterEvent(timeline_entity
, 'org_signup_start'):
536 'scope': program_entity
,
539 if org_app_logic
.logic
.getForFields(filter, unique
=True):
540 # add the 'List my Organization Applications' link
542 (redirects
.getListSelfRedirect(program_entity
,
543 {'url_name' : 'org_app'}),
544 "List My Organization Applications", 'any_access')]
546 # get the student entity for this user and program
547 filter = {'user': user
,
548 'scope': program_entity
,
550 student_entity
= student_logic
.logic
.getForFields(filter, unique
=True)
553 items
+= self
._getStudentEntries
(program_entity
, student_entity
,
556 # get mentor and org_admin entity for this user and program
557 filter = {'user': user
,
558 'program': program_entity
,
560 mentor_entity
= mentor_logic
.logic
.getForFields(filter, unique
=True)
561 org_admin_entity
= org_admin_logic
.logic
.getForFields(filter, unique
=True)
563 if mentor_entity
or org_admin_entity
:
564 items
+= self
._getOrganizationEntries
(program_entity
, org_admin_entity
,
565 mentor_entity
, params
, id, user
)
567 if not (student_entity
or mentor_entity
or org_admin_entity
):
568 if timeline_helper
.isActivePeriod(timeline_entity
, 'student_signup'):
569 # this user does not have a role yet for this program
570 items
+= [('/student/apply/%s' % (program_entity
.key().name()),
571 "Register as a Student", 'any_access')]
573 if timeline_helper
.isAfterEvent(timeline_entity
,
574 'accepted_organization_announced_deadline'):
575 # add a link to list all the organizations
576 items
+= [(redirects
.getPublicListRedirect(program_entity
,
577 {'url_name': 'org'}),
578 "List participating Organizations", 'any_access')]
580 if not student_entity
:
581 # add apply to become a mentor link
582 items
+= [('/org/apply_mentor/%s' % (program_entity
.key().name()),
583 "Apply to become a Mentor", 'any_access')]
587 def _getStudentEntries(self
, program_entity
, student_entity
,
589 """Returns a list with menu items for students in a specific program.
594 timeline_entity
= program_entity
.timeline
596 if timeline_helper
.isActivePeriod(timeline_entity
, 'student_signup'):
597 items
+= [('/student_proposal/list_orgs/%s' % (
598 student_entity
.key().name()),
599 "Submit your Student Proposal", 'any_access')]
600 items
+= [(redirects
.getListSelfRedirect(student_entity
,
601 {'url_name':'student_proposal'}),
602 "List my Student Proposals", 'any_access')]
606 def _getOrganizationEntries(self
, program_entity
, org_admin_entity
,
607 mentor_entity
, params
, id, user
):
608 """Returns a list with menu items for org admins and mentors in a
612 # TODO(ljvderijk) think about adding specific org items like submit review
621 admin
= decorators
.view(view
.admin
)
622 assign_slots
= decorators
.view(view
.assignSlots
)
623 assigned_proposals
= decorators
.view(view
.assignedProposals
)
624 create
= decorators
.view(view
.create
)
625 delete
= decorators
.view(view
.delete
)
626 edit
= decorators
.view(view
.edit
)
627 list = decorators
.view(view
.list)
628 public
= decorators
.view(view
.public
)
629 export
= decorators
.view(view
.export
)
630 show_duplicates
= decorators
.view(view
.showDuplicates
)
631 slots
= decorators
.view(view
.slots
)
632 home
= decorators
.view(view
.home
)
633 pick
= decorators
.view(view
.pick
)