Returning a JSON response on successfully storing the found duplicates.
[Melange.git] / app / soc / views / models / program.py
blobca0566795242712524959685b04bc01908c79d20
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.cache.logic
59 import soc.logic.models.program
60 import soc.models.work
63 class View(presence.View):
64 """View methods for the Program model.
65 """
67 DEF_ACCEPTED_ORGS_MSG_FMT = ugettext("These organizations have"
68 " been accepted into %(name)s, but they have not yet completed"
69 " their organization profile. You can still learn more about"
70 " each organization by visiting the links below.")
72 DEF_CREATED_ORGS_MSG_FMT = ugettext("These organizations have been"
73 " accepted into %(name)s and have completed their organization"
74 " profiles. You can learn more about each organization by"
75 " visiting the links below.")
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'] = ['checkIsDeveloper']
93 rights['slots'] = ['checkIsDeveloper']
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 @decorators.merge_params
194 @decorators.check_access
195 def acceptedOrgs(self, request, access_type,
196 page_name=None, params=None, filter=None, **kwargs):
197 """See base.View.list.
200 contents = []
201 logic = params['logic']
203 program_entity = logic.getFromKeyFieldsOr404(kwargs)
205 fmt = {'name': program_entity.name}
206 description = self.DEF_ACCEPTED_ORGS_MSG_FMT % fmt
208 filter = {
209 'status': 'accepted',
210 'scope': program_entity,
213 from soc.views.models import org_app as org_app_view
214 aa_params = org_app_view.view.getParams().copy() # accepted applications
216 # define the list redirect action to show the notification
217 del aa_params['list_key_order']
218 aa_params['list_action'] = (redirects.getPublicRedirect, aa_params)
219 aa_params['list_description'] = description
221 aa_list = lists.getListContent(request, aa_params, filter, idx=0,
222 need_content=True)
224 if aa_list:
225 contents.append(aa_list)
227 description = self.DEF_CREATED_ORGS_MSG_FMT % fmt
229 filter['status'] = ['new', 'active']
231 from soc.views.models.organization import view as org_view
232 ao_params = org_view.getParams().copy() # active orgs
233 ao_logic = ao_params['logic']
235 order = ['name']
237 if aa_list:
238 fun = self._getData
239 else:
240 # only cache if all profiles are created
241 fun = soc.cache.logic.cache(self._getData)
242 entities = fun(ao_logic.getModel(), filter, order, ao_logic)
244 ao_list = dicts.rename(ao_params, ao_params['list_params'])
245 ao_list['action'] = (redirects.getPublicRedirect, ao_params)
246 ao_list['description'] = description
247 ao_list['pagination'] = 'soc/list/no_pagination.html'
248 ao_list['data'] = entities
250 contents.append(ao_list)
252 params = params.copy()
253 params['list_msg'] = program_entity.accepted_orgs_msg
255 return self._list(request, params, contents, page_name)
257 @decorators.merge_params
258 @decorators.check_access
259 def slots(self, request, acces_type, page_name=None, params=None, **kwargs):
260 """Returns a JSON object with all orgs allocation.
262 Args:
263 request: the standard Django HTTP request object
264 access_type : the name of the access type which should be checked
265 page_name: the page name displayed in templates as page and header title
266 params: a dict with params for this View, not used
269 program = program_logic.logic.getFromKeyFieldsOr404(kwargs)
270 slots = program.slots
272 filter = {
273 'scope': program,
274 'status': 'active',
277 query = org_logic.logic.getQueryForFields(filter=filter)
278 organizations = org_logic.logic.getAll(query)
280 locked_slots = adjusted_slots = {}
282 if request.method == 'POST' and 'result' in request.POST:
283 result = request.POST['result']
285 from_json = simplejson.loads(result)
287 locked_slots = dicts.groupDictBy(from_json, 'locked', 'slots')
288 adjusted_slots = dicts.groupDictBy(from_json, 'adjustment')
290 orgs = {}
291 applications = {}
292 mentors = {}
294 for org in organizations:
295 filter = {
296 'org': org,
297 'status': ['new', 'pending']
299 orgs[org.link_id] = org
300 query = student_proposal_logic.logic.getQueryForFields(filter=filter)
301 proposals = student_proposal_logic.logic.getAll(query)
302 applications[org.link_id] = len(proposals)
303 mentors[org.link_id] = len([i for i in proposals if i.mentor != None])
305 # TODO: Use configuration variables here
306 max_slots_per_org = 40
307 min_slots_per_org = 2
308 iterative = False
310 allocator = allocations.Allocator(orgs.keys(), applications, mentors,
311 slots, max_slots_per_org,
312 min_slots_per_org, iterative)
314 result = allocator.allocate(locked_slots, adjusted_slots)
316 data = []
318 for link_id, count in result.iteritems():
319 org = orgs[link_id]
320 data.append({
321 'link_id': link_id,
322 'slots': count,
323 'locked': locked_slots.get(link_id, 0),
324 'adjustment': adjusted_slots.get(link_id, 0),
327 return self.json(request, data)
329 @decorators.merge_params
330 @decorators.check_access
331 def assignSlots(self, request, access_type, page_name=None,
332 params=None, **kwargs):
333 """View that allows to assign slots to orgs.
336 from soc.views.models import organization as organization_view
338 org_params = organization_view.view.getParams().copy()
339 org_params['list_template'] = 'soc/program/allocation/allocation.html'
340 org_params['list_heading'] = 'soc/program/allocation/heading.html'
341 org_params['list_row'] = 'soc/program/allocation/row.html'
342 org_params['list_pagination'] = 'soc/list/no_pagination.html'
344 program = program_logic.logic.getFromKeyFieldsOr404(kwargs)
346 filter = {
347 'scope': program,
348 'status': 'active',
351 content = lists.getListContent(request, org_params, filter=filter)
352 contents = [content]
354 return_url = "http://%(host)s%(index)s" % {
355 'host' : os.environ['HTTP_HOST'],
356 'index': redirects.getSlotsRedirect(program, params)
359 context = {
360 'total_slots': program.slots,
361 'uses_json': True,
362 'uses_slot_allocator': True,
363 'return_url': return_url,
366 return self._list(request, org_params, contents, page_name, context)
368 @decorators.merge_params
369 @decorators.check_access
370 def showDuplicates(self, request, access_type, page_name=None,
371 params=None, **kwargs):
372 """View in which a host can see which students have been assigned multiple slots.
374 For params see base.view.Public().
377 from django.utils import simplejson
379 from soc.logic.models.proposal_duplicates import logic as duplicates_logic
381 program_entity = program_logic.logic.getFromKeyFieldsOr404(kwargs)
383 if request.POST and request.POST.get('result'):
384 # store result in the datastore
385 fields = {'link_id': program_entity.link_id,
386 'scope': program_entity,
387 'scope_path': program_entity.key().name(),
388 'json_representation' : request.POST['result']
390 key_name = duplicates_logic.getKeyNameFromFields(fields)
391 duplicates_logic.updateOrCreateFromKeyName(fields, key_name)
393 response = simplejson.dumps({'status': 'done'})
394 return http.HttpResponse(response)
396 context = helper.responses.getUniversalContext(request)
397 helper.responses.useJavaScript(context, params['js_uses_all'])
398 context['uses_duplicates'] = True
399 context['uses_json'] = True
400 context['page_name'] = page_name
402 # get all orgs for this program who are active and have slots assigned
403 fields = {'scope': program_entity,
404 'slots >': 0,
405 'status': 'active'}
407 query = org_logic.logic.getQueryForFields(fields)
409 to_json = {
410 'nr_of_orgs': query.count(),
411 'program_key': program_entity.key().name()}
412 json = simplejson.dumps(to_json)
413 context['info'] = json
414 context['offset_length'] = 10
416 fields = {'link_id': program_entity.link_id,
417 'scope': program_entity}
418 duplicates = duplicates_logic.getForFields(fields, unique=True)
420 if duplicates:
421 # we have stored information
422 context['duplicate_cache_content'] = duplicates.json_representation
423 context['date_of_calculation'] = duplicates.calculated_on
424 else:
425 # no information stored
426 context['duplicate_cache_content'] = simplejson.dumps({})
428 template = 'soc/program/show_duplicates.html'
430 return helper.responses.respond(request, template=template, context=context)
432 @decorators.merge_params
433 @decorators.check_access
434 def assignedProposals(self, request, access_type, page_name=None,
435 params=None, filter=None, **kwargs):
436 """Returns a JSON dict containing all the proposals that would have
437 a slot assigned for a specific set of orgs.
439 The request.GET limit and offset determines how many and which
440 organizations should be returned.
442 For params see base.View.public().
444 Returns: JSON object with a collection of orgs and proposals. Containing
445 identification information and contact information.
448 get_dict = request.GET
450 if not (get_dict.get('limit') and get_dict.get('offset')):
451 return self.json(request, {})
453 try:
454 limit = max(0, int(get_dict['limit']))
455 offset = max(0, int(get_dict['offset']))
456 except ValueError:
457 return self.json(request, {})
459 program_entity = program_logic.logic.getFromKeyFieldsOr404(kwargs)
461 fields = {'scope': program_entity,
462 'slots >': 0,
463 'status': 'active'}
465 org_entities = org_logic.logic.getForFields(fields, limit=limit, offset=offset)
467 orgs_data = {}
468 proposals_data = []
470 # for each org get the proposals who will be assigned a slot
471 for org in org_entities:
473 org_data = {'name': org.name}
475 fields = {'scope': org,
476 'status': 'active',
477 'user': org.founder}
479 org_admin = org_admin_logic.logic.getForFields(fields, unique=True)
481 if org_admin:
482 org_data['admin_name'] = org_admin.name()
483 org_data['admin_email'] = org_admin.email
485 # check if there are already slots taken by this org
486 fields = {'org': org,
487 'status': 'accepted'}
489 query = student_proposal_logic.logic.getQueryForFields(fields)
491 slots_left_to_assign = max(0, org.slots - query.count())
493 if slots_left_to_assign == 0:
494 # no slots left so next org
495 continue
497 # store information about the org
498 orgs_data[org.key().name()] = org_data
500 fields = {'org': org,
501 'mentor !=': None,
502 'status': 'pending'}
503 order = ['-score']
505 # get the the number of proposals that would be assigned a slot
506 student_proposal_entities = student_proposal_logic.logic.getForFields(
507 fields, limit=slots_left_to_assign, order=order)
509 # store each proposal in the dictionary
510 for proposal in student_proposal_entities:
511 student_entity = proposal.scope
513 proposals_data.append(
514 {'key_name': proposal.key().name(),
515 'proposal_title': proposal.title,
516 'student_key': student_entity.key().name(),
517 'student_name': student_entity.name(),
518 'student_contact': student_entity.email,
519 'org_key': org.key().name()
522 # return all the data in JSON format
523 data = {'orgs': orgs_data,
524 'proposals': proposals_data}
526 return self.json(request, data)
528 def _editPost(self, request, entity, fields):
529 """See base._editPost().
532 super(View, self)._editPost(request, entity, fields)
534 if not entity:
535 # there is no existing entity so create a new timeline
536 fields['timeline'] = self._createTimelineForType(fields)
537 else:
538 # use the timeline from the entity
539 fields['timeline'] = entity.timeline
541 def _createTimelineForType(self, fields):
542 """Creates and stores a timeline model for the given type of program.
545 workflow = fields['workflow']
547 timeline_logic = program_logic.logic.TIMELINE_LOGIC[workflow]
549 properties = timeline_logic.getKeyFieldsFromFields(fields)
550 key_name = timeline_logic.getKeyNameFromFields(properties)
552 properties['scope'] = fields['scope']
554 timeline = timeline_logic.updateOrCreateFromKeyName(properties, key_name)
555 return timeline
557 @decorators.merge_params
558 def getExtraMenus(self, id, user, params=None):
559 """Returns the extra menu's for this view.
561 A menu item is generated for each program that is currently
562 running. The public page for each program is added as menu item,
563 as well as all public documents for that program.
565 Args:
566 params: a dict with params for this View.
569 logic = params['logic']
570 rights = params['rights']
572 # only get all invisible and visible programs
573 fields = {'status': ['invisible', 'visible']}
574 entities = logic.getForFields(fields)
576 menus = []
578 rights.setCurrentUser(id, user)
580 for entity in entities:
581 items = []
583 if entity.status == 'visible':
584 # show the documents for this program, even for not logged in users
585 items += document_view.view.getMenusForScope(entity, params)
586 items += self._getTimeDependentEntries(entity, params, id, user)
588 try:
589 # check if the current user is a host for this program
590 rights.doCachedCheck('checkIsHostForProgram',
591 {'scope_path': entity.scope_path,
592 'link_id': entity.link_id}, [])
594 if entity.status == 'invisible':
595 # still add the document links so hosts can see how it looks like
596 items += document_view.view.getMenusForScope(entity, params)
597 items += self._getTimeDependentEntries(entity, params, id, user)
599 items += [(redirects.getReviewOverviewRedirect(
600 entity, {'url_name': 'org_app'}),
601 "Review Organization Applications", 'any_access')]
602 # add link to edit Program Profile
603 items += [(redirects.getEditRedirect(entity, params),
604 'Edit Program Profile','any_access')]
605 # add link to Assign Slots
606 items += [(redirects.getAssignSlotsRedirect(entity, params),
607 'Assign Slots','any_access')]
608 # add link to edit Program Timeline
609 items += [(redirects.getEditRedirect(entity, {'url_name': 'timeline'}),
610 "Edit Program Timeline", 'any_access')]
611 # add link to create a new Program Document
612 items += [(redirects.getCreateDocumentRedirect(entity, 'program'),
613 "Create a New Document", 'any_access')]
614 # add link to list all Program Document
615 items += [(redirects.getListDocumentsRedirect(entity, 'program'),
616 "List Documents", 'any_access')]
618 except out_of_band.Error:
619 pass
621 items = sidebar.getSidebarMenu(id, user, items, params=params)
622 if not items:
623 continue
625 menu = {}
626 menu['heading'] = entity.short_name
627 menu['items'] = items
628 menu['group'] = 'Programs'
629 menus.append(menu)
631 return menus
633 def _getTimeDependentEntries(self, program_entity, params, id, user):
634 """Returns a list with time dependent menu items.
636 items = []
638 #TODO(ljvderijk) Add more timeline dependent entries
639 timeline_entity = program_entity.timeline
641 if timeline_helper.isActivePeriod(timeline_entity, 'org_signup'):
642 # add the organization signup link
643 items += [
644 (redirects.getApplyRedirect(program_entity, {'url_name': 'org_app'}),
645 "Apply to become an Organization", 'any_access')]
647 if user and timeline_helper.isAfterEvent(timeline_entity, 'org_signup_start'):
648 filter = {
649 'applicant': user,
650 'scope': program_entity,
653 if org_app_logic.logic.getForFields(filter, unique=True):
654 # add the 'List my Organization Applications' link
655 items += [
656 (redirects.getListSelfRedirect(program_entity,
657 {'url_name' : 'org_app'}),
658 "List My Organization Applications", 'any_access')]
660 # get the student entity for this user and program
661 filter = {'user': user,
662 'scope': program_entity,
663 'status': 'active'}
664 student_entity = student_logic.logic.getForFields(filter, unique=True)
666 if student_entity:
667 items += self._getStudentEntries(program_entity, student_entity,
668 params, id, user)
670 # get mentor and org_admin entity for this user and program
671 filter = {'user': user,
672 'program': program_entity,
673 'status': 'active'}
674 mentor_entity = mentor_logic.logic.getForFields(filter, unique=True)
675 org_admin_entity = org_admin_logic.logic.getForFields(filter, unique=True)
677 if mentor_entity or org_admin_entity:
678 items += self._getOrganizationEntries(program_entity, org_admin_entity,
679 mentor_entity, params, id, user)
681 if user and not (student_entity or mentor_entity or org_admin_entity):
682 if timeline_helper.isActivePeriod(timeline_entity, 'student_signup'):
683 # this user does not have a role yet for this program
684 items += [('/student/apply/%s' % (program_entity.key().name()),
685 "Register as a Student", 'any_access')]
687 deadline = 'accepted_organization_announced_deadline'
689 if timeline_helper.isAfterEvent(timeline_entity, deadline):
690 url = redirects.getAcceptedOrgsRedirect(program_entity, params)
691 # add a link to list all the organizations
692 items += [(url, "List participating Organizations", 'any_access')]
694 if not student_entity:
695 # add apply to become a mentor link
696 items += [('/org/apply_mentor/%s' % (program_entity.key().name()),
697 "Apply to become a Mentor", 'any_access')]
699 return items
701 def _getStudentEntries(self, program_entity, student_entity,
702 params, id, user):
703 """Returns a list with menu items for students in a specific program.
706 items = []
708 timeline_entity = program_entity.timeline
710 if timeline_helper.isActivePeriod(timeline_entity, 'student_signup'):
711 items += [('/student_proposal/list_orgs/%s' % (
712 student_entity.key().name()),
713 "Submit your Student Proposal", 'any_access')]
714 items += [(redirects.getListSelfRedirect(student_entity,
715 {'url_name':'student_proposal'}),
716 "List my Student Proposals", 'any_access')]
718 items += [(redirects.getEditRedirect(student_entity,
719 {'url_name': 'student'}),
720 "Edit my Student Profile", 'any_access')]
722 return items
724 def _getOrganizationEntries(self, program_entity, org_admin_entity,
725 mentor_entity, params, id, user):
726 """Returns a list with menu items for org admins and mentors in a
727 specific program.
730 # TODO(ljvderijk) think about adding specific org items like submit review
732 items = []
734 return items
737 view = View()
739 accepted_orgs = decorators.view(view.acceptedOrgs)
740 admin = decorators.view(view.admin)
741 assign_slots = decorators.view(view.assignSlots)
742 assigned_proposals = decorators.view(view.assignedProposals)
743 create = decorators.view(view.create)
744 delete = decorators.view(view.delete)
745 edit = decorators.view(view.edit)
746 list = decorators.view(view.list)
747 public = decorators.view(view.public)
748 export = decorators.view(view.export)
749 show_duplicates = decorators.view(view.showDuplicates)
750 slots = decorators.view(view.slots)
751 home = decorators.view(view.home)
752 pick = decorators.view(view.pick)