1 # This file is part of Indico.
2 # Copyright (C) 2002 - 2015 European Organization for Nuclear Research (CERN).
4 # Indico is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License as
6 # published by the Free Software Foundation; either version 3 of the
7 # License, or (at your option) any later version.
9 # Indico is distributed in the hope that it will be useful, but
10 # WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 # General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with Indico; if not, see <http://www.gnu.org/licenses/>.
17 from __future__
import unicode_literals
20 from collections
import OrderedDict
21 from datetime
import timedelta
22 from tempfile
import NamedTemporaryFile
23 from zipfile
import ZipFile
25 from flask
import session
, flash
26 from markupsafe
import escape
27 from sqlalchemy
import cast
, Date
29 from indico
.core
.config
import Config
30 from indico
.core
.db
.sqlalchemy
.links
import LinkType
31 from indico
.util
.date_time
import format_date
32 from indico
.util
.i18n
import _
33 from indico
.util
.fs
import secure_filename
34 from indico
.util
.string
import to_unicode
, natural_sort_key
35 from indico
.util
.tasks
import delete_file
36 from indico
.web
.flask
.util
import send_file
37 from indico
.web
.forms
.base
import FormDefaults
38 from indico
.modules
.attachments
.forms
import AttachmentPackageForm
39 from indico
.modules
.attachments
.models
.attachments
import Attachment
, AttachmentFile
, AttachmentType
40 from indico
.modules
.attachments
.models
.folders
import AttachmentFolder
41 from MaKaC
.conference
import SubContribution
44 def _get_start_dt(obj
):
45 if isinstance(obj
, SubContribution
):
46 return obj
.getContribution().getAdjustedStartDate()
48 return obj
.getAdjustedStartDate()
51 class AttachmentPackageGeneratorMixin
:
53 def _filter_attachments(self
, filter_data
):
54 added_since
= filter_data
.get('added_since', None)
55 filter_type
= filter_data
['filter_type']
58 if filter_type
== 'all':
59 attachments
= self
._get
_all
_attachments
(added_since
)
60 elif filter_type
== 'sessions':
61 attachments
= self
._filter
_by
_sessions
(filter_data
.get('sessions', []), added_since
)
62 elif filter_type
== 'contributions':
63 attachments
= self
._filter
_by
_contributions
(filter_data
.get('contributions', []), added_since
)
64 elif filter_type
== 'dates':
65 attachments
= self
._filter
_by
_dates
(filter_data
.get('dates', []))
67 return self
._filter
_protected
(attachments
)
69 def _filter_protected(self
, attachments
):
70 return [attachment
for attachment
in attachments
if attachment
.can_access(session
.user
)]
72 def _get_all_attachments(self
, added_since
):
73 query
= self
._build
_base
_query
(added_since
)
75 def _check_scheduled(attachment
):
76 obj
= attachment
.folder
.linked_object
77 return obj
is not None and _get_start_dt(obj
) is not None
79 return filter(_check_scheduled
, query
)
81 def _build_base_query(self
, added_since
=None):
82 query
= Attachment
.find(Attachment
.type == AttachmentType
.file, ~AttachmentFolder
.is_deleted
,
83 ~Attachment
.is_deleted
, AttachmentFolder
.event_id
== int(self
._conf
.getId()),
84 _join
=AttachmentFolder
)
85 if added_since
is not None:
86 query
= query
.join(Attachment
.file).filter(cast(AttachmentFile
.created_dt
, Date
) >= added_since
)
89 def _filter_by_sessions(self
, session_ids
, added_since
):
90 session_ids
= set(session_ids
)
91 query
= self
._build
_base
_query
(added_since
).filter(AttachmentFolder
.link_type
.in_([LinkType
.session
,
92 LinkType
.contribution
,
93 LinkType
.subcontribution
]))
95 def _check_session(attachment
):
96 obj
= attachment
.folder
.linked_object
97 return obj
is not None and obj
.getSession() and obj
.getSession().getId() in session_ids
99 return filter(_check_session
, query
)
101 def _filter_by_contributions(self
, contribution_ids
, added_since
):
102 query
= self
._build
_base
_query
(added_since
).filter(AttachmentFolder
.contribution_id
.in_(contribution_ids
),
103 AttachmentFolder
.link_type
.in_([LinkType
.contribution
,
104 LinkType
.subcontribution
]))
106 def _check_scheduled(attachment
):
107 obj
= attachment
.folder
.linked_object
108 return obj
is not None and _get_start_dt(obj
) is not None
110 return filter(_check_scheduled
, query
)
112 def _filter_by_dates(self
, dates
):
115 def _check_date(attachment
):
116 obj
= attachment
.folder
.linked_object
119 start_dt
= _get_start_dt(obj
)
122 return unicode(start_dt
.date()) in dates
124 return filter(_check_date
, self
._build
_base
_query
())
126 def _generate_zip_file(self
, attachments
):
127 temp_file
= NamedTemporaryFile(suffix
='indico.tmp', dir=Config
.getInstance().getTempDir())
128 with
ZipFile(temp_file
.name
, 'w', allowZip64
=True) as zip_handler
:
130 for attachment
in attachments
:
131 name
= self
._prepare
_folder
_structure
(attachment
)
133 with attachment
.file.storage
.get_local_path(attachment
.file.storage_file_id
) as filepath
:
134 zip_handler
.write(filepath
, name
)
136 # Delete the temporary file after some time. Even for a large file we don't
137 # need a higher delay since the webserver will keep it open anyway until it's
138 # done sending it to the client.
139 delete_file
.apply_async(args
=[temp_file
.name
], countdown
=3600)
140 temp_file
.delete
= False
141 return send_file('material-{}.zip'.format(self
._conf
.id), temp_file
.name
, 'application/zip', inline
=False)
143 def _prepare_folder_structure(self
, attachment
):
144 event_dir
= secure_filename(self
._conf
.getTitle(), None)
145 segments
= [event_dir
] if event_dir
else []
146 segments
.extend(self
._get
_base
_path
(attachment
))
147 if not attachment
.folder
.is_default
:
148 segments
.append(secure_filename(attachment
.folder
.title
, unicode(attachment
.folder
.id)))
149 segments
.append(attachment
.file.filename
)
150 path
= os
.path
.join(*filter(None, segments
))
151 while path
in self
.used
:
152 # prepend the id if there's a path collision
153 segments
[-1] = '{}-{}'.format(attachment
.id, segments
[-1])
154 path
= os
.path
.join(*filter(None, segments
))
157 def _get_base_path(self
, attachment
):
158 obj
= linked_object
= attachment
.folder
.linked_object
160 while obj
!= self
._conf
:
161 owner
= obj
.getOwner()
162 if isinstance(obj
, SubContribution
):
163 start_date
= owner
.getAdjustedStartDate()
165 start_date
= obj
.getAdjustedStartDate()
167 if start_date
is not None:
168 paths
.append(secure_filename(start_date
.strftime('%H%M_{}').format(obj
.getTitle()), ''))
170 paths
.append(secure_filename(obj
.getTitle(), unicode(obj
.getId())))
173 if isinstance(linked_object
, SubContribution
):
174 linked_obj_start_date
= linked_object
.getOwner().getAdjustedStartDate()
176 linked_obj_start_date
= linked_object
.getAdjustedStartDate()
178 if attachment
.folder
.linked_object
!= self
._conf
and linked_obj_start_date
is not None:
179 paths
.append(secure_filename(linked_obj_start_date
.strftime('%Y%m%d_%A'), ''))
181 return reversed(paths
)
184 class AttachmentPackageMixin(AttachmentPackageGeneratorMixin
):
188 form
= self
._prepare
_form
()
189 if form
.validate_on_submit():
190 attachments
= self
._filter
_attachments
(form
.data
)
192 return self
._generate
_zip
_file
(attachments
)
194 flash(_('There are no materials matching your criteria.'), 'warning')
196 return self
.wp
.render_template('generate_package.html', self
._conf
, form
=form
)
198 def _prepare_form(self
):
199 form
= AttachmentPackageForm(obj
=FormDefaults(filter_type
='all'))
200 form
.dates
.choices
= list(self
._iter
_event
_days
())
201 filter_types
= OrderedDict()
202 filter_types
['all'] = _('Everything')
203 filter_types
['sessions'] = _('Specific sessions')
204 filter_types
['contributions'] = _('Specific contributions')
205 filter_types
['dates'] = _('Specific days')
207 form
.sessions
.choices
= self
._load
_session
_data
()
208 if not form
.sessions
.choices
:
209 del filter_types
['sessions']
212 form
.contributions
.choices
= self
._load
_contribution
_data
()
213 if not form
.contributions
.choices
:
214 del filter_types
['contributions']
215 del form
.contributions
217 form
.filter_type
.choices
= filter_types
.items()
220 def _load_session_data(self
):
221 return [(session
.getId(), escape(to_unicode(session
.getTitle()))) for session
in self
._conf
.getSessionList()]
223 def _load_contribution_data(self
):
224 def _format_contrib(contrib
):
225 if contrib
.getSession() is None:
226 return to_unicode(contrib
.getTitle())
228 return _('{contrib} (in session "{session}")').format(
229 session
=to_unicode(contrib
.getSession().getTitle()),
230 contrib
=to_unicode(contrib
.getTitle())
233 contribs
= sorted([contrib
for contrib
in self
._conf
.getContributionList() if contrib
.getStartDate()],
234 key
=lambda c
: natural_sort_key(c
.getTitle()))
235 return [(contrib
.getId(), escape(_format_contrib(contrib
))) for contrib
in contribs
]
237 def _iter_event_days(self
):
238 duration
= (self
._conf
.getAdjustedEndDate() - self
._conf
.getAdjustedStartDate()).days
239 for offset
in xrange(duration
+ 1):
240 day
= (self
._conf
.getAdjustedStartDate() + timedelta(days
=offset
)).date()
241 yield day
.isoformat(), format_date(day
, 'short')