Fixed a very obvious bug in slots()
[Melange.git] / app / soc / views / models / program.py
blob1bcde6870fbb2ec200b1ac7263f9f5b7c0e57db1
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 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.
64 """
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.
70 Params:
71 params: a dict with params for this View
72 """
74 rights = access.Checker(params)
75 rights['any_access'] = ['allow']
76 rights['show'] = ['allow']
77 rights['create'] = [('checkSeeded', ['checkHasActiveRoleForScope',
78 host_logic.logic])]
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']
86 new_params = {}
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']
100 patterns = []
101 patterns += [
102 (r'^%(url_name)s/(?P<access_type>assign_slots)/%(key_fields)s$',
103 'soc.views.models.%(module_name)s.assign_slots',
104 '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),
127 reference_fields = [
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')),
138 result = {}
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(),
147 required=True)
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.
173 Args:
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
183 filter = {
184 'scope': program,
185 'status': 'active',
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')
201 orgs = {}
202 applications = {}
203 mentors = {}
205 for org in organizations:
206 filter = {
207 'org': org,
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
219 iterative = False
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)
227 data = []
229 for link_id, count in result.iteritems():
230 org = orgs[link_id]
231 data.append({
232 'link_id': link_id,
233 'slots': count,
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)
257 filter = {
258 'scope': program,
259 'status': 'active',
262 content = lists.getListContent(request, org_params, filter=filter)
263 contents = [content]
265 return_url = "http://%(host)s%(index)s" % {
266 'host' : os.environ['HTTP_HOST'],
267 'index': redirects.getSlotsRedirect(program, params)
270 context = {
271 'total_slots': program.slots,
272 'uses_json': True,
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,
298 'slots >': 0,
299 'status': 'active'}
301 query = org_logic.logic.getQueryForFields(fields)
303 to_json = {
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, {})
337 try:
338 limit = max(0, int(get_dict['limit']))
339 offset = max(0, int(get_dict['offset']))
340 except ValueError:
341 return self.json(request, {})
343 program_entity = program_logic.logic.getFromKeyFieldsOr404(kwargs)
345 fields = {'scope': program_entity,
346 'slots >': 0,
347 'status': 'active'}
349 org_entities = org_logic.logic.getForFields(fields, limit=limit, offset=offset)
351 orgs_data = {}
352 proposals_data = {}
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,
360 'status': 'active',
361 'user': org.founder}
363 org_admin = org_admin_logic.logic.getForFields(fields, unique=True)
365 if org_admin:
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)
374 test = query.count()
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
380 continue
382 # store information about the org
383 orgs_data[org.key().name()] = org_data
385 fields = {'org': org,
386 'mentor !=': None,
387 'status': 'pending'}
388 order = ['-score']
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)
394 proposal_data = {}
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)
423 if not entity:
424 # there is no existing entity so create a new timeline
425 fields['timeline'] = self._createTimelineForType(fields)
426 else:
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)
444 return timeline
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.
454 Args:
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)
465 menus = []
467 rights.setCurrentUser(id, user)
469 for entity in entities:
470 items = []
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)
477 try:
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:
505 pass
507 items = sidebar.getSidebarMenu(id, user, items, params=params)
508 if not items:
509 continue
511 menu = {}
512 menu['heading'] = entity.short_name
513 menu['items'] = items
514 menu['group'] = 'Programs'
515 menus.append(menu)
517 return menus
519 def _getTimeDependentEntries(self, program_entity, params, id, user):
520 """Returns a list with time dependent menu items.
522 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
529 items += [
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'):
534 filter = {
535 'applicant': user,
536 'scope': program_entity,
539 if org_app_logic.logic.getForFields(filter, unique=True):
540 # add the 'List my Organization Applications' link
541 items += [
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,
549 'status': 'active'}
550 student_entity = student_logic.logic.getForFields(filter, unique=True)
552 if student_entity:
553 items += self._getStudentEntries(program_entity, student_entity,
554 params, id, user)
556 # get mentor and org_admin entity for this user and program
557 filter = {'user': user,
558 'program': program_entity,
559 'status': 'active'}
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')]
585 return items
587 def _getStudentEntries(self, program_entity, student_entity,
588 params, id, user):
589 """Returns a list with menu items for students in a specific program.
592 items = []
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')]
604 return items
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
609 specific program.
612 # TODO(ljvderijk) think about adding specific org items like submit review
614 items = []
616 return items
619 view = View()
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)