Skip to content
Snippets Groups Projects
Commit 4e4e96d5 authored by srosse's avatar srosse
Browse files

OO-5166: pre-upload slides for BigBlueButton meetings

parent c118a4f6
No related branches found
No related tags found
No related merge requests found
Showing
with 987 additions and 11 deletions
......@@ -26,6 +26,7 @@ import java.util.List;
import org.olat.core.id.Identity;
import org.olat.core.id.Roles;
import org.olat.core.util.UserSession;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.group.BusinessGroup;
import org.olat.modules.bigbluebutton.manager.BigBlueButtonUriBuilder;
import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
......@@ -103,6 +104,18 @@ public interface BigBlueButtonManager {
public BigBlueButtonMeeting getMeeting(BigBlueButtonMeeting meeting);
public VFSContainer getSlidesContainer(BigBlueButtonMeeting meeting);
/**
* The method will create a meeting and uploaded the slides to the
* BigBlueButton server but it will only doing it during the leading
* time.
*
* @param meetingKey The meeting primary key
* @return true if slides were effectively uploaded
*/
public boolean preloadSlides(Long meetingKey);
/**
* Return the first meeting which matches the specified identifier
* as the meeting's identifier or readable identifier.
......
......@@ -134,6 +134,14 @@ public interface BigBlueButtonMeeting extends ModifiedInfo, CreateInfo {
public void setPassword(String password);
/**
* This is the relative path to the container for the slides. Generated
* only if needed.
*
* @return
*/
public String getDirectory();
/**
* If not a permanent meeting, the meetings starts at this date. Participants
......
......@@ -20,6 +20,7 @@
package org.olat.modules.bigbluebutton;
import java.net.URI;
import java.util.Set;
import javax.ws.rs.core.UriBuilder;
......@@ -55,7 +56,13 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO
private static final String PROP_ADHOC_MEETING = "vc.bigbluebutton.adhoc.meeting";
private static final String PROP_USER_BANDWIDTH_REQUIREMENT = "vc.bigbluebutton.user.bandwidth.requirement";
private static final String PROP_RECORDING_HANDLER_ID = "vc.bigbluebutton.recording.handler.id";
private static final String PROP_MAX_UPLOAD_SIZE = "vc.bigbluebutton.max.upload.size";
public static final Set<String> SLIDES_MIME_TYPES = Set.of("image/gif", "image/jpg", "image/jpeg", "image/png", "application/pdf",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
@Value("${vc.bigbluebutton.enabled}")
private boolean enabled;
......@@ -78,6 +85,9 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO
private String secret;
@Value("${vc.bigbluebutton.shared.secret}")
private String sharedSecret;
@Value("${vc.bigbluebutton.max.upload.size:100}")
private Integer maxUploadSize;
@Value("${vc.bigbluebutton.permanent.meeting:false}")
private String permanentMeetingEnabled;
......@@ -123,6 +133,11 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO
groupsEnabled = getStringPropertyValue(PROP_GROUP_ENABLED, groupsEnabled);
coursesEnabled = getStringPropertyValue(PROP_COURSE_ENABLED, coursesEnabled);
appointmentsEnabled = getStringPropertyValue(PROP_APPOINTMENTS_ENABLED, appointmentsEnabled);
String maxUploadSizeObj = getStringPropertyValue(PROP_MAX_UPLOAD_SIZE, maxUploadSize.toString());
if(StringHelper.containsNonWhitespace(maxUploadSizeObj)) {
maxUploadSize = Integer.valueOf(maxUploadSizeObj);
}
String bandwidthReqObj = getStringPropertyValue(PROP_USER_BANDWIDTH_REQUIREMENT, true);
if(StringHelper.containsNonWhitespace(bandwidthReqObj)) {
......@@ -283,6 +298,20 @@ public class BigBlueButtonModule extends AbstractSpringModule implements ConfigO
setStringProperty(PROP_SHARED_SECRET, sharedSecret, true);
}
/**
*
* @return The max. size of the slides to upload to BigBlueButton servers
* in MB.
*/
public Integer getMaxUploadSize() {
return maxUploadSize;
}
public void setMaxUploadSize(Integer maxUploadSize) {
this.maxUploadSize = maxUploadSize;
setStringProperty(PROP_MAX_UPLOAD_SIZE, maxUploadSize.toString(), true);
}
public Double getUserBandwidhtRequirement() {
return userBandwidhtRequirement;
}
......
......@@ -20,6 +20,7 @@
package org.olat.modules.bigbluebutton.manager;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
......@@ -34,9 +35,13 @@ import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.logging.log4j.Logger;
......@@ -47,7 +52,13 @@ import org.olat.commons.calendar.model.KalendarEvent;
import org.olat.commons.calendar.model.KalendarEventLink;
import org.olat.commons.calendar.ui.components.KalendarRenderWrapper;
import org.olat.core.commons.persistence.DB;
import org.olat.core.commons.services.taskexecutor.Task;
import org.olat.core.commons.services.taskexecutor.TaskExecutorManager;
import org.olat.core.dispatcher.mapper.MapperService;
import org.olat.core.dispatcher.mapper.manager.MapperKey;
import org.olat.core.helpers.Settings;
import org.olat.core.id.Identity;
import org.olat.core.id.OLATResourceable;
import org.olat.core.id.Roles;
import org.olat.core.id.User;
import org.olat.core.id.context.BusinessControlFactory;
......@@ -56,6 +67,11 @@ import org.olat.core.util.CodeHelper;
import org.olat.core.util.StringHelper;
import org.olat.core.util.UserSession;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.filters.VFSLeafButSystemFilter;
import org.olat.course.CourseFactory;
import org.olat.course.ICourse;
import org.olat.group.BusinessGroup;
......@@ -90,6 +106,8 @@ import org.olat.repository.RepositoryEntryRef;
import org.olat.repository.RepositoryEntrySecurity;
import org.olat.repository.RepositoryManager;
import org.olat.repository.manager.RepositoryEntryDAO;
import org.olat.resource.OLATResource;
import org.olat.resource.OLATResourceManager;
import org.olat.user.UserDataDeletable;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -108,12 +126,19 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
DeletableGroupData, RepositoryEntryDataDeletable, UserDataDeletable, InitializingBean {
private static final Logger log = Tracing.createLoggerFor(BigBlueButtonManagerImpl.class);
private static final String TASK_MEETING_RESNAME = BigBlueButtonMeeting.class.getSimpleName();
@Autowired
private DB dbInstance;
@Autowired
private MapperService mapperService;
@Autowired
private CalendarManager calendarManager;
@Autowired
private TaskExecutorManager taskManager;
@Autowired
private OLATResourceManager resourceManager;
@Autowired
private RepositoryManager repositoryManager;
@Autowired
private RepositoryEntryDAO repositoryEntryDao;
......@@ -128,6 +153,8 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
@Autowired
private BigBlueButtonAttendeeDAO bigBlueButtonAttendeeDao;
@Autowired
private BigBlueButtonSlidesStorage bigBlueButtonSlidesStorage;
@Autowired
private BigBlueButtonMeetingTemplateDAO bigBlueButtonMeetingTemplateDao;
@Autowired
private BigBlueButtonRecordingReferenceDAO bigBlueButtonRecordingReferenceDao;
......@@ -386,6 +413,41 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
return null;
}
@Override
public VFSContainer getSlidesContainer(BigBlueButtonMeeting meeting) {
VFSContainer container;
if(StringHelper.containsNonWhitespace(meeting.getDirectory())) {
container = bigBlueButtonSlidesStorage.getStorage(meeting);
} else {
container = bigBlueButtonSlidesStorage.createStorage(meeting);
}
return container;
}
@Override
public boolean preloadSlides(Long meetingKey) {
BigBlueButtonMeeting meeting = bigBlueButtonMeetingDao.loadByKey(meetingKey);
if(meeting == null) {
return false;
}
boolean loaded = false;
Date now = new Date();
Date start = meeting.getStartDate();
Date startWithLeadingTime = meeting.getStartWithLeadTime();
if(startWithLeadingTime.compareTo(now) <= 0 && start.compareTo(now) >= 0) {
List<VFSLeaf> slides = getSlides(meeting);
if(!slides.isEmpty()) {
BigBlueButtonErrors errors = new BigBlueButtonErrors();
meeting = getMeetingWithServer(meeting);
createBigBlueButtonMeeting(meeting, errors);
loaded = !errors.hasErrors();
log.info(Tracing.M_AUDIT, "Slides preloaded: {}", meeting);
}
}
return loaded;
}
@Override
public boolean isIdentifierInUse(String identifier, BigBlueButtonMeeting reference) {
if(StringHelper.containsNonWhitespace(identifier)) {
......@@ -397,7 +459,23 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
@Override
public BigBlueButtonMeeting updateMeeting(BigBlueButtonMeeting meeting) {
updateCalendarEvent(meeting);
return bigBlueButtonMeetingDao.updateMeeting(meeting);
meeting = bigBlueButtonMeetingDao.updateMeeting(meeting);
if(StringHelper.containsNonWhitespace(meeting.getDirectory())) {
OLATResource resource = resourceManager.findResourceable(meeting.getKey(), TASK_MEETING_RESNAME);
if(resource == null) {
OLATResourceable res = OresHelper.createOLATResourceableInstance(TASK_MEETING_RESNAME, meeting.getKey());
resource = resourceManager.createAndPersistOLATResourceInstance(res);
SlidesPreloaderTask loader = new SlidesPreloaderTask(meeting.getKey());
taskManager.execute(loader, null, resource, null, meeting.getStartWithLeadTime());
} else {
List<Task> currentTasks = taskManager.getTasks(resource);
if(!currentTasks.isEmpty()) {
SlidesPreloaderTask loader = new SlidesPreloaderTask(meeting.getKey());
taskManager.updateAndReturn(currentTasks.get(0), loader, null, meeting.getStartWithLeadTime());
}
}
}
return meeting;
}
@Override
......@@ -460,12 +538,28 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
bigBlueButtonMeetingDeletionHandlers.forEach(h -> h.onBeforeDelete(reloadedMeeting));
removeCalendarEvent(reloadedMeeting);
deleteRecordings(meeting, errors);
deleteSlides(meeting);
bigBlueButtonAttendeeDao.deleteAttendee(reloadedMeeting);
bigBlueButtonMeetingDao.deleteMeeting(reloadedMeeting);
}
return false;
}
private void deleteSlides(BigBlueButtonMeeting meeting) {
if(StringHelper.containsNonWhitespace(meeting.getDirectory())) {
VFSContainer slidesContainer = bigBlueButtonSlidesStorage.getStorage(meeting);
if(slidesContainer != null && slidesContainer.exists()) {
slidesContainer.deleteSilently();
}
OLATResource resource = resourceManager.findResourceable(meeting.getKey(), TASK_MEETING_RESNAME);
if(resource != null) {
taskManager.delete(resource);
resourceManager.deleteOLATResource(resource);
}
}
}
public BigBlueButtonServer getAvailableServer() {
List<BigBlueButtonServer> servers = getServers();
List<BigBlueButtonServer> availableServers = servers.stream()
......@@ -948,10 +1042,52 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
// metadata
getRecordingsHandler().appendMetadata(uriBuilder, meeting);
// slides
if(StringHelper.containsNonWhitespace(meeting.getDirectory())) {
VFSContainer slidesContainer = bigBlueButtonSlidesStorage.getStorage(meeting);
List<VFSLeaf> slides = getSlides(meeting);
if(!slides.isEmpty()) {
MapperKey mapperKey = mapperService.register(null, meeting.getMeetingId(), new SlidesContainerMapper(slidesContainer), 360);
String url = Settings.createServerURI() + mapperKey.getUrl() + "/slides/";
String slidesXml = slidesDocument(url, slides);
uriBuilder.xmlPayload(slidesXml);
}
}
Document doc = sendRequest(uriBuilder, errors);
return BigBlueButtonUtils.checkSuccess(doc, errors);
}
private String slidesDocument(String url, List<VFSLeaf> slides) {
StringBuilder sb = new StringBuilder();
sb.append("<?xml version='1.0' encoding='UTF-8'?>")
.append("<modules><module name='presentation'>");
for(VFSLeaf slide:slides) {
sb.append("<document url='").append(url).append(slide.getName()).append("' />");
}
sb.append("</module></modules>");
return sb.toString();
}
private List<VFSLeaf> getSlides(BigBlueButtonMeeting meeting) {
List<VFSLeaf> slides = new ArrayList<>();
if(StringHelper.containsNonWhitespace(meeting.getDirectory())) {
VFSContainer slidesContainer = bigBlueButtonSlidesStorage.getStorage(meeting);
if(slidesContainer != null && slidesContainer.exists()) {
List<VFSItem> items = slidesContainer.getItems(new VFSLeafButSystemFilter());
for(VFSItem item:items) {
if(item instanceof VFSLeaf) {
slides.add((VFSLeaf)item);
}
}
}
}
return slides;
}
@Override
public BigBlueButtonAttendee getAttendee(Identity identity, BigBlueButtonMeeting meeting) {
return bigBlueButtonAttendeeDao.getAttendee(identity, meeting);
......@@ -1032,14 +1168,41 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
@Override
public Document sendRequest(BigBlueButtonUriBuilder builder, BigBlueButtonErrors errors) {
dbInstance.commit();
if(StringHelper.containsNonWhitespace(builder.getXmlPayload())) {
return sendPostRequest(builder, errors);
}
return sendGetRequest(builder, errors);
}
private Document sendPostRequest(BigBlueButtonUriBuilder builder, BigBlueButtonErrors errors) {
URI uri = builder.build();
HttpPost post = new HttpPost(uri);
String payload = builder.getXmlPayload();
post.addHeader("Content-Language", "en-US");
ContentType cType = ContentType.create("text/xml", StandardCharsets.UTF_8);
HttpEntity myEntity = new StringEntity(payload, cType);
post.setEntity(myEntity);
RequestConfig requestConfig = getRequestConfiguration();
try(CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.disableAutomaticRetries()
.build();
CloseableHttpResponse response = httpClient.execute(post)) {
int statusCode = response.getStatusLine().getStatusCode();
log.debug("Status code of: {} {}", uri, statusCode);
return BigBlueButtonUtils.getDocumentFromEntity(response.getEntity());
} catch(Exception e) {
errors.append(new BigBlueButtonError(BigBlueButtonErrorCodes.unkown));
log.error("Cannot send: {}", uri, e);
return null;
}
}
private Document sendGetRequest(BigBlueButtonUriBuilder builder, BigBlueButtonErrors errors) {
URI uri = builder.build();
HttpGet get = new HttpGet(uri);
RequestConfig requestConfig = RequestConfig.copy(RequestConfig.DEFAULT)
.setConnectTimeout(bigBlueButtonModule.getHttpConnectTimeout())
.setConnectionRequestTimeout(bigBlueButtonModule.getHttpConnectRequestTimeout())
.setSocketTimeout(bigBlueButtonModule.getHttpSocketTimeout())
.build();
RequestConfig requestConfig = getRequestConfiguration();
try(CloseableHttpClient httpClient = HttpClientBuilder.create()
.setDefaultRequestConfig(requestConfig)
.disableAutomaticRetries()
......@@ -1055,6 +1218,14 @@ public class BigBlueButtonManagerImpl implements BigBlueButtonManager,
}
}
private RequestConfig getRequestConfiguration() {
return RequestConfig.copy(RequestConfig.DEFAULT)
.setConnectTimeout(bigBlueButtonModule.getHttpConnectTimeout())
.setConnectionRequestTimeout(bigBlueButtonModule.getHttpConnectRequestTimeout())
.setSocketTimeout(bigBlueButtonModule.getHttpSocketTimeout())
.build();
}
private static class ServerLoadComparator implements Comparator<BigBlueButtonServerInfos> {
@Override
......
......@@ -79,6 +79,7 @@ public class BigBlueButtonMeetingDAO {
meeting.setCreator(creator);
dbInstance.getCurrentEntityManager().persist(meeting);
return meeting;
}
......
/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.modules.bigbluebutton.manager;
import java.io.File;
import org.olat.core.util.StringHelper;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSManager;
import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
import org.olat.modules.bigbluebutton.model.BigBlueButtonMeetingImpl;
import org.springframework.stereotype.Service;
/**
*
* Initial date: 22 déc. 2020<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
@Service
public class BigBlueButtonSlidesStorage {
private static final String DIRECTORY = "bigbluebutton";
private VFSContainer rootContainer;
public VFSContainer getRootContainer() {
if(rootContainer == null) {
rootContainer = VFSManager.olatRootContainer(File.separator + DIRECTORY, null);
}
return rootContainer;
}
public VFSContainer getStorage(BigBlueButtonMeeting meeting) {
VFSItem dir = getRootContainer().resolve(meeting.getDirectory());
return dir instanceof VFSContainer ? (VFSContainer)dir : null;
}
public VFSContainer createStorage(BigBlueButtonMeeting meeting) {
StringBuilder sb = new StringBuilder();
if(meeting.getBusinessGroup() != null) {
VFSManager.getOrCreateContainer(getRootContainer(), sb.toString());
sb.append("group").append(File.separator).append(meeting.getBusinessGroup().getKey());
} else if(meeting.getEntry() != null) {
sb.append("course").append(File.separator).append(meeting.getEntry().getKey()).append(File.separator);
if(StringHelper.containsNonWhitespace(meeting.getSubIdent())) {
sb.append(meeting.getSubIdent());
} else {
sb.append("tool");
}
}
sb.append(File.separator).append(meeting.getKey());
VFSContainer cont = VFSManager.olatRootContainer(File.separator + DIRECTORY + File.separator + sb.toString());
((BigBlueButtonMeetingImpl)meeting).setDirectory(sb.toString());
return cont;
}
}
......@@ -42,6 +42,7 @@ public class BigBlueButtonUriBuilder {
private String scheme;
private int port;
private String startPath;
private String xmlPayload;
private final String sharedSecret;
......@@ -64,6 +65,11 @@ public class BigBlueButtonUriBuilder {
return new BigBlueButtonUriBuilder(uri, sharedSecret);
}
public BigBlueButtonUriBuilder xmlPayload(String xml) {
xmlPayload = xml;
return this;
}
public BigBlueButtonUriBuilder operation(String operation) {
this.operation = operation;
return this;
......@@ -133,6 +139,10 @@ public class BigBlueButtonUriBuilder {
}
}
public String getXmlPayload() {
return xmlPayload;
}
public static String urlEncode(String s) {
try {
return URLEncoder.encode(s, "UTF-8");
......
/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.modules.bigbluebutton.manager;
import javax.servlet.http.HttpServletRequest;
import org.olat.core.dispatcher.mapper.Mapper;
import org.olat.core.gui.media.ForbiddenMediaResource;
import org.olat.core.gui.media.MediaResource;
import org.olat.core.gui.media.NotFoundMediaResource;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSMediaResource;
/**
*
* Initial date: 23 déc. 2020<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class SlidesContainerMapper implements Mapper {
public static final String DOWNLOAD_PREFIX = "/slides/";
private final VFSContainer container;
private final VFSContainer tempContainer;
public SlidesContainerMapper(VFSContainer container) {
this(null, container);
}
public SlidesContainerMapper(VFSContainer tempContainer, VFSContainer container) {
this.container = container;
this.tempContainer = tempContainer;
}
@Override
public MediaResource handle(String relPath, HttpServletRequest request) {
MediaResource resource = null;
if(relPath.startsWith(DOWNLOAD_PREFIX)) {
String filename = relPath.substring(DOWNLOAD_PREFIX.length());
VFSItem slide = null;
if(tempContainer != null) {
slide = tempContainer.resolve(filename);
}
if(slide == null && container != null) {
slide = container.resolve(filename);
}
if(slide instanceof VFSLeaf) {
resource = new VFSMediaResource((VFSLeaf)slide);
} else {
resource = new NotFoundMediaResource();
}
} else {
resource = new ForbiddenMediaResource();
}
return resource;
}
}
/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.modules.bigbluebutton.manager;
import org.olat.core.CoreSpringFactory;
import org.olat.core.commons.services.taskexecutor.LongRunnable;
import org.olat.modules.bigbluebutton.BigBlueButtonManager;
/**
*
* Initial date: 23 déc. 2020<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class SlidesPreloaderTask implements LongRunnable {
private static final long serialVersionUID = -1753449764226330508L;
private Long meetingKey;
public SlidesPreloaderTask(Long meetingKey) {
this.meetingKey = meetingKey;
}
@Override
public void run() {
CoreSpringFactory.getImpl(BigBlueButtonManager.class)
.preloadSlides(meetingKey);
}
}
......@@ -107,6 +107,9 @@ public class BigBlueButtonMeetingImpl implements Persistable, BigBlueButtonMeeti
@Column(name="b_layout", nullable=false, insertable=true, updatable=true)
private String layout;
@Column(name="b_directory", nullable=true, insertable=true, updatable=true)
private String directory;
@Column(name="b_meeting_id", nullable=false, insertable=true, updatable=false)
private String meetingId;
@Column(name="b_attendee_pw", nullable=false, insertable=true, updatable=false)
......@@ -228,8 +231,14 @@ public class BigBlueButtonMeetingImpl implements Persistable, BigBlueButtonMeeti
public void setWelcome(String welcome) {
this.welcome = welcome;
}
public String getDirectory() {
return directory;
}
public void setDirectory(String directory) {
this.directory = directory;
}
public String getLayout() {
return layout;
......
......@@ -29,6 +29,7 @@ import org.olat.core.gui.components.form.flexible.elements.FlexiTableElement;
import org.olat.core.gui.components.form.flexible.elements.FormLink;
import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
import org.olat.core.gui.components.form.flexible.elements.SingleSelection;
import org.olat.core.gui.components.form.flexible.elements.TextElement;
import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
import org.olat.core.gui.components.form.flexible.impl.FormEvent;
import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
......@@ -42,6 +43,7 @@ import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.util.StringHelper;
import org.olat.modules.bigbluebutton.BigBlueButtonManager;
import org.olat.modules.bigbluebutton.BigBlueButtonModule;
import org.olat.modules.bigbluebutton.BigBlueButtonRecordingsHandler;
......@@ -65,6 +67,7 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
private MultipleSelectionElement enabledForEl;
private MultipleSelectionElement permanentForEl;
private SingleSelection recordingsHandlerEl;
private TextElement slidesUploadLimitEl;
private FormLink addServerButton;
private FlexiTableElement serversTableEl;
......@@ -140,6 +143,11 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
} else {
recordingsHandlerEl.select(BigBlueButtonNativeRecordingsHandler.NATIVE_RECORDING_HANDLER_ID, true);
}
Integer maxSize = bigBlueButtonModule.getMaxUploadSize();
String maxSizeStr = maxSize == null ? null : maxSize.toString();
slidesUploadLimitEl = uifactory.addTextElement("slides.upload.limit", 8, maxSizeStr, formLayout);
slidesUploadLimitEl.setMandatory(true);
//buttons save - check
FormLayoutContainer buttonLayout = FormLayoutContainer.createButtonLayout("save", getTranslator());
......@@ -185,6 +193,19 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
}
}
slidesUploadLimitEl.clearError();
if(StringHelper.containsNonWhitespace(slidesUploadLimitEl.getValue())) {
try {
Integer.parseInt(slidesUploadLimitEl.getValue());
} catch (NumberFormatException e) {
slidesUploadLimitEl.setErrorKey("form.error.nointeger", null);
allOk &= false;
}
} else {
slidesUploadLimitEl.setErrorKey("form.legende.mandatory", null);
allOk &= false;
}
return allOk;
}
......@@ -241,6 +262,7 @@ public class BigBlueButtonConfigurationController extends FormBasicController {
bigBlueButtonModule.setGroupsEnabled(enabledForEl.isSelected(2));
bigBlueButtonModule.setPermanentMeetingEnabled(permanentForEl.isAtLeastSelected(1));
bigBlueButtonModule.setRecordingHandlerId(recordingsHandlerEl.getSelectedKey());
bigBlueButtonModule.setMaxUploadSize(Integer.valueOf(slidesUploadLimitEl.getValue()));
}
CollaborationToolsFactory.getInstance().initAvailableTools();
}
......
......@@ -46,6 +46,7 @@ import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.gui.control.generic.closablewrapper.CloseableCalloutWindowController;
import org.olat.core.gui.control.generic.closablewrapper.CloseableModalController;
import org.olat.core.gui.control.generic.modal.DialogBoxController;
import org.olat.core.gui.control.generic.modal.DialogBoxUIFactory;
import org.olat.core.gui.control.winmgr.CommandFactory;
......@@ -57,6 +58,10 @@ import org.olat.core.util.UserSession;
import org.olat.core.util.coordinate.CoordinatorManager;
import org.olat.core.util.event.GenericEventListener;
import org.olat.core.util.resource.OresHelper;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.filters.VFSLeafButSystemFilter;
import org.olat.modules.bigbluebutton.BigBlueButtonAttendee;
import org.olat.modules.bigbluebutton.BigBlueButtonAttendeeRoles;
import org.olat.modules.bigbluebutton.BigBlueButtonDispatcher;
......@@ -66,6 +71,7 @@ import org.olat.modules.bigbluebutton.BigBlueButtonModule;
import org.olat.modules.bigbluebutton.BigBlueButtonRecording;
import org.olat.modules.bigbluebutton.BigBlueButtonRecordingReference;
import org.olat.modules.bigbluebutton.BigBlueButtonRecordingsPublishedRoles;
import org.olat.modules.bigbluebutton.manager.SlidesContainerMapper;
import org.olat.modules.bigbluebutton.model.BigBlueButtonErrors;
import org.olat.modules.bigbluebutton.model.BigBlueButtonRecordingWithReference;
import org.olat.modules.bigbluebutton.ui.BigBlueButtonRecordingTableModel.BRecordingsCols;
......@@ -84,16 +90,22 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
private final boolean administrator;
private BigBlueButtonMeeting meeting;
private int count = 0;
private final boolean guest;
private final boolean moderatorStartMeeting;
private final OLATResourceable meetingOres;
private FormLink joinButton;
private FormLink uploadButton;
private FormLink guestJoinButton;
private MultipleSelectionElement acknowledgeRecordingEl;
private FlexiTableElement tableEl;
private BigBlueButtonRecordingTableModel recordingTableModel;
private SlidesContainerMapper slidesMapper;
private CloseableModalController cmc;
private SlideUploadController uploadSlideCtrl;
private PublishRecordingController publishCtrl;
private DialogBoxController confirmDeleteRecordingDialog;
private CloseableCalloutWindowController publishCalloutCtrl;
......@@ -162,6 +174,11 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
layoutCont.contextPut("externalPassword", password);
}
if(administrator || moderator) {
loadSlides(layoutCont);
uploadButton = uifactory.addFormLink("meeting.slides.upload", formLayout, Link.BUTTON_SMALL);
}
if(StringHelper.containsNonWhitespace(meeting.getMainPresenter())) {
layoutCont.contextPut("mainPresenter", meeting.getMainPresenter());
}
......@@ -185,6 +202,38 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
acknowledgeKeyValue.keys(), acknowledgeKeyValue.values());
}
private void loadSlides(FormLayoutContainer layoutCont) {
List<SlideWrapper> documentWrappers = new ArrayList<>();
if(StringHelper.containsNonWhitespace(meeting.getDirectory())) {
VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting);
if(slidesMapper == null) {
slidesMapper = new SlidesContainerMapper(slidesContainer);
String mapperUri = registerCacheableMapper(null, "BigBlueButtonSlides::" + meeting.getKey(), slidesMapper);
layoutCont.contextPut("mapperUri", mapperUri);
}
if(slidesContainer != null && slidesContainer.exists()) {
boolean slidesEditable = isSlidesEditable();
List<VFSItem> items = slidesContainer.getItems(new VFSLeafButSystemFilter());
for(VFSItem item:items) {
if(item instanceof VFSLeaf) {
VFSLeaf slide = (VFSLeaf)item;
SlideWrapper wrapper = new SlideWrapper(slide, false);
if(slidesEditable) {
FormLink deleteButton = uifactory
.addFormLink("delete_" + (++count), "delete", "delete", null, layoutCont, Link.BUTTON_XSMALL);
deleteButton.setUserObject(wrapper);
wrapper.setDeleteButton(deleteButton);
}
documentWrappers.add(wrapper);
}
}
}
}
layoutCont.contextPut("documents", documentWrappers);
}
private void initRecordings(FormItemContainer formLayout) {
FlexiTableColumnModel columnsModel = FlexiTableDataModelFactory.createFlexiTableColumnModel();
columnsModel.addFlexiColumnModel(new DefaultFlexiColumnModel(BRecordingsCols.name));
......@@ -273,6 +322,17 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
return !((start != null && start.compareTo(now) >= 0) || (end != null && end.compareTo(now) <= 0));
}
private boolean isSlidesEditable() {
if(meeting == null) return false;
if(meeting.isPermanent()) {
return true;
}
Date now = new Date();
Date start = meeting.getStartDate();
return start != null && start.compareTo(now) > 0;
}
private void reloadButtonsAndStatus() {
meeting = bigBlueButtonManager.getMeeting(meeting);
updateButtonsAndStatus();
......@@ -300,6 +360,11 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
guestJoinButton.setVisible(accessible && !disabled && guest);
}
guestJoinButton.setEnabled(!readOnly && accessible && !disabled && guest);
if(uploadButton != null) {
boolean slidesEditable = isSlidesEditable();
uploadButton.setVisible(slidesEditable);
}
if(accessible && !disabled) {
boolean running = bigBlueButtonManager.isMeetingRunning(meeting);
......@@ -364,7 +429,14 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
}
publishCalloutCtrl.deactivate();
cleanUp();
} else if(publishCalloutCtrl == source) {
} else if(uploadSlideCtrl == source) {
if(event == Event.DONE_EVENT || event == Event.CHANGED_EVENT) {
meeting = bigBlueButtonManager.getMeeting(meeting);
loadSlides(flc);
}
cmc.deactivate();
cleanUp();
} else if(publishCalloutCtrl == source || cmc == source) {
cleanUp();
}
super.event(ureq, source, event);
......@@ -373,10 +445,14 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
private void cleanUp() {
removeAsListenerAndDispose(confirmDeleteRecordingDialog);
removeAsListenerAndDispose(publishCalloutCtrl);
removeAsListenerAndDispose(uploadSlideCtrl);
removeAsListenerAndDispose(publishCtrl);
removeAsListenerAndDispose(cmc);
confirmDeleteRecordingDialog = null;
publishCalloutCtrl = null;
uploadSlideCtrl = null;
publishCtrl = null;
cmc = null;
}
@Override
......@@ -401,6 +477,8 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
doJoin(ureq);
} else if(this.guestJoinButton == source) {
doGuestJoin(ureq);
} else if(uploadButton == source) {
doUploadSlides(ureq);
} else if(tableEl == source) {
if(event instanceof SelectionEvent) {
SelectionEvent se = (SelectionEvent)event;
......@@ -414,6 +492,9 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
FormLink link = (FormLink)source;
if("publish".equals(link.getCmd()) && link.getUserObject() instanceof BigBlueButtonRecordingRow) {
doPublish(ureq, link, (BigBlueButtonRecordingRow)link.getUserObject());
} else if("delete".equals(link.getCmd()) && link.getUserObject() instanceof SlideWrapper) {
doDeleteSlide((SlideWrapper)link.getUserObject());
loadSlides(flc);
}
}
super.formInnerEvent(ureq, source, event);
......@@ -424,6 +505,25 @@ public class BigBlueButtonMeetingController extends FormBasicController implemen
//
}
private void doUploadSlides(UserRequest ureq) {
uploadSlideCtrl = new SlideUploadController(ureq, getWindowControl(), meeting);
listenTo(uploadSlideCtrl);
String title = translate("meeting.slides.upload");
cmc = new CloseableModalController(getWindowControl(), translate("close"), uploadSlideCtrl.getInitialComponent(), true, title);
listenTo(cmc);
cmc.activate();
}
private void doDeleteSlide(SlideWrapper slide) {
VFSLeaf document = slide.getDocument();
VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting);
VFSItem reloadedDocument = slidesContainer.resolve(document.getName());
if(reloadedDocument != null && reloadedDocument.exists()) {
reloadedDocument.delete();
}
}
private void doPublish(UserRequest ureq, FormLink link, BigBlueButtonRecordingRow row) {
publishCtrl = new PublishRecordingController(ureq, getWindowControl(), row);
listenTo(publishCtrl);
......
......@@ -22,14 +22,19 @@ package org.olat.modules.bigbluebutton.ui;
import static org.olat.modules.bigbluebutton.ui.BigBlueButtonUIHelper.getSelectedTemplate;
import static org.olat.modules.bigbluebutton.ui.BigBlueButtonUIHelper.isWebcamLayoutAvailable;
import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.form.flexible.FormItem;
import org.olat.core.gui.components.form.flexible.FormItemContainer;
import org.olat.core.gui.components.form.flexible.elements.DateChooser;
import org.olat.core.gui.components.form.flexible.elements.FileElement;
import org.olat.core.gui.components.form.flexible.elements.FormLink;
import org.olat.core.gui.components.form.flexible.elements.MultipleSelectionElement;
import org.olat.core.gui.components.form.flexible.elements.SingleSelection;
......@@ -37,6 +42,7 @@ import org.olat.core.gui.components.form.flexible.elements.TextElement;
import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
import org.olat.core.gui.components.form.flexible.impl.FormEvent;
import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
import org.olat.core.gui.components.link.Link;
import org.olat.core.gui.components.util.KeyValues;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
......@@ -45,6 +51,14 @@ import org.olat.core.gui.control.generic.closablewrapper.CloseableModalControlle
import org.olat.core.id.Identity;
import org.olat.core.util.CodeHelper;
import org.olat.core.util.StringHelper;
import org.olat.core.util.ValidationStatus;
import org.olat.core.util.WebappHelper;
import org.olat.core.util.vfs.LocalFolderImpl;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.VFSManager;
import org.olat.core.util.vfs.filters.VFSSystemItemFilter;
import org.olat.group.BusinessGroup;
import org.olat.modules.bigbluebutton.BigBlueButtonDispatcher;
import org.olat.modules.bigbluebutton.BigBlueButtonManager;
......@@ -55,6 +69,7 @@ import org.olat.modules.bigbluebutton.BigBlueButtonModule;
import org.olat.modules.bigbluebutton.BigBlueButtonRecordingsPublishingEnum;
import org.olat.modules.bigbluebutton.BigBlueButtonServer;
import org.olat.modules.bigbluebutton.BigBlueButtonTemplatePermissions;
import org.olat.modules.bigbluebutton.manager.SlidesContainerMapper;
import org.olat.repository.RepositoryEntry;
import org.olat.user.UserManager;
import org.springframework.beans.factory.annotation.Autowired;
......@@ -88,6 +103,8 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
private TextElement externalLinkEl;
private MultipleSelectionElement passwordEnableEl;
private TextElement passwordEl;
private FileElement uploadSlidesEl;
private FormLayoutContainer slidesCont;
private final Mode mode;
private final String subIdent;
......@@ -97,6 +114,12 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
private BigBlueButtonMeeting meeting;
private final List<BigBlueButtonTemplatePermissions> permissions;
private List<BigBlueButtonMeetingTemplate> templates;
private SlidesContainerMapper slidesMapper;
private final String mapperUri;
private VFSContainer slidesContainer;
private VFSContainer temporaryContainer;
private List<SlideWrapper> documentWrappers = new ArrayList<>();
private int count = 0;
private final boolean running;
private final boolean editable;
......@@ -129,8 +152,13 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
this.businessGroup = businessGroup;
this.permissions = permissions;
templates = bigBlueButtonManager.getTemplates();
temporaryContainer = new LocalFolderImpl(new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID()));
slidesMapper = new SlidesContainerMapper(temporaryContainer);
mapperUri = registerCacheableMapper(null, null, slidesMapper);
initForm(ureq);
reloadSlides();
}
public EditBigBlueButtonMeetingController(UserRequest ureq, WindowControl wControl,
......@@ -149,8 +177,14 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
editable = isEditable(meeting, ureq);
editableInternal = isEditableInternal(meeting, ureq);
administrator = ureq.getUserSession().getRoles().isAdministrator();
slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting);
temporaryContainer = new LocalFolderImpl(new File(WebappHelper.getTmpDir(), CodeHelper.getUniqueID()));
slidesMapper = new SlidesContainerMapper(temporaryContainer, slidesContainer);
mapperUri = registerCacheableMapper(null, null, slidesMapper);
initForm(ureq);
reloadSlides();
}
private boolean isEditable(BigBlueButtonMeeting m, UserRequest ureq) {
......@@ -209,6 +243,18 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
mainPresenterEl.setElementCssClass("o_sel_bbb_edit_meeting_presenter");
mainPresenterEl.setEnabled(editable || editableInternal);
// slides
String page = velocity_root + "/meeting_slides.html";
slidesCont = FormLayoutContainer.createCustomFormLayout("meeting.slides.container", getTranslator(), page);
slidesCont.setLabel("meeting.slides", null);
slidesCont.contextPut("mapperUri", mapperUri);
formLayout.add(slidesCont);
uploadSlidesEl = uifactory.addFileElement(getWindowControl(), "meeting.slides.upload", "meeting.slides", formLayout);
uploadSlidesEl.addActionListener(FormEvent.ONCHANGE);
uploadSlidesEl.setVisible(editable || editableInternal);
uploadSlidesEl.limitToMimeType(BigBlueButtonModule.SLIDES_MIME_TYPES, "error.slides.type", null);
Long selectedTemplateKey = meeting == null || meeting.getTemplate() == null
? null : meeting.getTemplate().getKey();
KeyValues templatesKeyValues = new KeyValues();
......@@ -385,9 +431,58 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
listenTo(cmc);
}
private void reloadSlides() {
Map<String,SlideWrapper> docsMap = documentWrappers.stream()
.collect(Collectors.toMap(SlideWrapper::getFilename, doc -> doc));
List<SlideWrapper> wrappers = new ArrayList<>();
if(temporaryContainer != null && temporaryContainer.exists()) {
reloadSlides(temporaryContainer, true, wrappers, docsMap);
}
if(slidesContainer != null && slidesContainer.exists()) {
reloadSlides(slidesContainer, false, wrappers, docsMap);
}
slidesCont.contextPut("documents", wrappers);
slidesCont.setVisible(!wrappers.isEmpty());
if(uploadSlidesEl != null && uploadSlidesEl.isVisible()) {
if(wrappers.isEmpty()) {
uploadSlidesEl.setLabel("meeting.slides.upload", null);
slidesCont.setDirty(true);
} else {
uploadSlidesEl.setLabel(null, null);
}
}
}
private void reloadSlides(VFSContainer container, boolean temporary, List<SlideWrapper> wrappers, Map<String,SlideWrapper> docsMap) {
List<VFSItem> documents = container.getItems(new VFSSystemItemFilter());
for (VFSItem document : documents) {
if(document instanceof VFSLeaf) {
SlideWrapper wrapper = docsMap.get(document.getName());
if(wrapper == null) {
wrapper = new SlideWrapper((VFSLeaf)document, temporary);
documentWrappers.add(wrapper);
if(editable || editableInternal) {
FormLink deleteButton = uifactory
.addFormLink("delete_" + (++count), "delete", "delete", null, slidesCont, Link.BUTTON_XSMALL);
deleteButton.setUserObject(wrapper);
wrapper.setDeleteButton(deleteButton);
}
}
if(!wrapper.isDeleted()) {
wrappers.add(wrapper);
}
}
}
}
@Override
protected void doDispose() {
//
if(temporaryContainer != null) {
temporaryContainer.deleteSilently();
}
}
@Override
......@@ -465,10 +560,31 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
allOk &= false;
}
allOk &= validateSlidesSize();
return allOk;
}
private boolean validateSlidesSize() {
boolean allOk = true;
Integer maxSizeInMb = bigBlueButtonModule.getMaxUploadSize();
slidesCont.clearError();
if(maxSizeInMb != null && maxSizeInMb.intValue() > 0) {
long total = 0l;
for(SlideWrapper doc:documentWrappers) {
if(!doc.isDeleted()) {
total += doc.getDocument().getSize();
}
}
if(total > (maxSizeInMb.intValue() * 1000 * 1000)) {
slidesCont.setErrorKey("error.slides.size", new String[] { maxSizeInMb.toString() });
allOk &= false;
}
}
return allOk;
}
@Override
protected void event(UserRequest ureq, Controller source, Event event) {
......@@ -502,6 +618,19 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
BigBlueButtonUIHelper.validateReadableIdentifier(externalLinkEl, meeting);
} else if (serverEl == source) {
serverChangeWarning();
} else if(uploadSlidesEl == source) {
if(uploadSlidesEl.getUploadFile() != null && StringHelper.containsNonWhitespace(uploadSlidesEl.getUploadFileName())) {
doUploadSlide(uploadSlidesEl.getUploadFile(), uploadSlidesEl.getUploadFileName());
reloadSlides();
validateSlidesSize();
uploadSlidesEl.reset();
}
} else if(source instanceof FormLink) {
FormLink link = (FormLink)source;
if("delete".equals(link.getCmd()) && link.getUserObject() instanceof SlideWrapper) {
doDeleteSlide((SlideWrapper)link.getUserObject());
reloadSlides();
}
}
super.formInnerEvent(ureq, source, event);
}
......@@ -587,11 +716,48 @@ public class EditBigBlueButtonMeetingController extends FormBasicController {
BigBlueButtonServer server = bigBlueButtonManager.getServer(Long.valueOf(selectedServerKey));
meeting.setServer(server);
}
// copy the slides, eventually update the directory field
doCopySlides();
meeting = bigBlueButtonManager.updateMeeting(meeting);
fireEvent(ureq, Event.DONE_EVENT);
}
private void doCopySlides() {
if(documentWrappers.isEmpty()) return;
VFSContainer storage = bigBlueButtonManager.getSlidesContainer(meeting);
for(SlideWrapper doc:documentWrappers) {
if(doc.isDeleted()) {
doc.getDocument().deleteSilently();
} else if(doc.isTemporary()) {
VFSLeaf target = storage.createChildLeaf(doc.getFilename());
VFSManager.copyContent(doc.getDocument(), target, true);
}
}
}
private void doUploadSlide(File file, String filename) {
List<ValidationStatus> validationResults = new ArrayList<>();
uploadSlidesEl.validate(validationResults);
if(validationResults.isEmpty()) {
VFSLeaf newSlide = VFSManager.resolveOrCreateLeafFromPath(temporaryContainer, filename);
VFSManager.copyContent(file, newSlide);
for(SlideWrapper doc:documentWrappers) {
if(filename.equals(doc.getFilename())) {
doc.setDeleted(false);
}
}
}
}
private void doDeleteSlide(SlideWrapper slide) {
slide.setDeleted(true);
slidesCont.setDirty(true);
}
@Override
protected void formCancelled(UserRequest ureq) {
......
/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.modules.bigbluebutton.ui;
import java.util.ArrayList;
import java.util.List;
import org.olat.core.commons.persistence.DB;
import org.olat.core.gui.UserRequest;
import org.olat.core.gui.components.form.flexible.FormItemContainer;
import org.olat.core.gui.components.form.flexible.elements.FileElement;
import org.olat.core.gui.components.form.flexible.impl.FormBasicController;
import org.olat.core.gui.components.form.flexible.impl.FormEvent;
import org.olat.core.gui.components.form.flexible.impl.FormLayoutContainer;
import org.olat.core.gui.control.Controller;
import org.olat.core.gui.control.Event;
import org.olat.core.gui.control.WindowControl;
import org.olat.core.util.ValidationStatus;
import org.olat.core.util.vfs.VFSContainer;
import org.olat.core.util.vfs.VFSItem;
import org.olat.core.util.vfs.VFSLeaf;
import org.olat.core.util.vfs.filters.VFSSystemItemFilter;
import org.olat.modules.bigbluebutton.BigBlueButtonManager;
import org.olat.modules.bigbluebutton.BigBlueButtonMeeting;
import org.olat.modules.bigbluebutton.BigBlueButtonModule;
import org.springframework.beans.factory.annotation.Autowired;
/**
*
* Initial date: 24 déc. 2020<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class SlideUploadController extends FormBasicController {
private FileElement fileEl;
private BigBlueButtonMeeting meeting;
@Autowired
private DB dbInstance;
@Autowired
private BigBlueButtonModule bigBlueButtonModule;
@Autowired
private BigBlueButtonManager bigBlueButtonManager;
public SlideUploadController(UserRequest ureq, WindowControl wControl, BigBlueButtonMeeting meeting) {
super(ureq, wControl);
this.meeting = meeting;
initForm(ureq);
}
@Override
protected void initForm(FormItemContainer formLayout, Controller listener, UserRequest ureq) {
formLayout.setElementCssClass("o_sel_course_gta_upload_form");
fileEl = uifactory.addFileElement(getWindowControl(), "file", "meeting.slides", formLayout);
fileEl.setMandatory(true);
fileEl.addActionListener(FormEvent.ONCHANGE);
fileEl.limitToMimeType(BigBlueButtonModule.SLIDES_MIME_TYPES, "error.slides.type", null);
FormLayoutContainer buttonCont = FormLayoutContainer.createButtonLayout("buttons", getTranslator());
buttonCont.setRootForm(mainForm);
formLayout.add(buttonCont);
uifactory.addFormCancelButton("cancel", buttonCont, ureq, getWindowControl());
uifactory.addFormSubmitButton("save", buttonCont);
}
@Override
protected void doDispose() {
//
}
@Override
protected boolean validateFormLogic(UserRequest ureq) {
boolean allOk = super.validateFormLogic(ureq);
fileEl.clearError();
List<ValidationStatus> validationResults = new ArrayList<>();
fileEl.validate(validationResults);
if(validationResults.isEmpty()) {
Integer maxSizeInMb = bigBlueButtonModule.getMaxUploadSize();
if(maxSizeInMb != null && maxSizeInMb.intValue() > 0) {
long total = fileEl.getUploadSize();
VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting);
List<VFSItem> documents = slidesContainer.getItems(new VFSSystemItemFilter());
for(VFSItem doc:documents) {
if(doc instanceof VFSLeaf) {
total += ((VFSLeaf)doc).getSize();
}
}
if(total > (maxSizeInMb.intValue() * 1000 * 1000)) {
fileEl.setErrorKey("error.slides.size", new String[] { maxSizeInMb.toString() });
allOk &= false;
}
}
} else {
allOk &= false;
}
return allOk;
}
@Override
protected void formOK(UserRequest ureq) {
VFSContainer slidesContainer = bigBlueButtonManager.getSlidesContainer(meeting);
fileEl.moveUploadFileTo(slidesContainer);
meeting = bigBlueButtonManager.updateMeeting(meeting);
dbInstance.commit();
fireEvent(ureq, Event.DONE_EVENT);
}
@Override
protected void formCancelled(UserRequest ureq) {
fireEvent(ureq, Event.CANCELLED_EVENT);
}
}
\ No newline at end of file
/**
* <a href="http://www.openolat.org">
* OpenOLAT - Online Learning and Training</a><br>
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); <br>
* you may not use this file except in compliance with the License.<br>
* You may obtain a copy of the License at the
* <a href="http://www.apache.org/licenses/LICENSE-2.0">Apache homepage</a>
* <p>
* Unless required by applicable law or agreed to in writing,<br>
* software distributed under the License is distributed on an "AS IS" BASIS, <br>
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. <br>
* See the License for the specific language governing permissions and <br>
* limitations under the License.
* <p>
* Initial code contributed and copyrighted by<br>
* frentix GmbH, http://www.frentix.com
* <p>
*/
package org.olat.modules.bigbluebutton.ui;
import org.olat.core.gui.components.form.flexible.elements.FormLink;
import org.olat.core.util.Formatter;
import org.olat.core.util.vfs.VFSLeaf;
/**
*
* Initial date: 23 déc. 2020<br>
* @author srosse, stephane.rosse@frentix.com, http://www.frentix.com
*
*/
public class SlideWrapper {
private final VFSLeaf document;
private FormLink deleteButton;
private boolean deleted = false;
private boolean temporary = false;
public SlideWrapper(VFSLeaf document, boolean temporary) {
this.document = document;
this.temporary = temporary;
}
public String getFilename() {
return document.getName();
}
public boolean isTemporary() {
return temporary;
}
public String getLabel() {
return document.getName() + " (" + Formatter.formatBytes(document.getSize()) + ")";
}
public boolean isDeleted() {
return deleted;
}
public void setDeleted(boolean deleted) {
this.deleted = deleted;
}
public VFSLeaf getDocument() {
return document;
}
public FormLink getDeleteButton() {
return deleteButton;
}
public void setDeleteButton(FormLink deleteButton) {
this.deleteButton = deleteButton;
deleteButton.setUserObject(this);
}
}
......@@ -30,6 +30,18 @@
</div>
#end
#if($r.isNotEmpty($documents))
<label forid="meeting-slides"><i class="o_icon o_filetype_file"> </i> $r.translate("meeting.slides")</label>
<ul id="meeting-slides" class="list-unstyled o_slides">
#foreach($document in $documents)
<li><a href="$mapperUri/slides/$r.encodeUrl($document.filename)" target="_blank"><i class="o_icon o_icon-fw $r.getFiletypeIconCss($document.filename)"> </i> $r.escapeHtml($document.label)</a> #if($r.visible($document.deleteButton))$r.render($document.deleteButton)#end</li>
#end
</ul>
#end
#if($r.available("meeting.slides.upload"))
$r.render("meeting.slides.upload")
#end
#if($r.isTrue($ended))
<div class="o_block_large o_warning">$r.translate("meeting.ended")</div>
#end
......
#if($r.isNotEmpty($documents))
<ul class="form-static list-unstyled o_slides">
#foreach($document in $documents)
#if($r.isFalse($document.deleted))
<li><a href="$mapperUri/slides/$r.encodeUrl($document.filename)" target="_blank"><i class="o_icon o_icon-fw $r.getFiletypeIconCss($document.filename)"> </i> $r.escapeHtml($document.label)</a> #if($r.visible($document.deleteButton))$r.render($document.deleteButton)#end</li>
#end
#end
</ul>
#end
\ No newline at end of file
......@@ -50,6 +50,8 @@ error.end.past=Der Online-Termin kann nicht in der Vergangenheit geplant werden.
error.first.date.in.past=Der erste Termin kann sich nicht in der Vergangenheit befinden.
error.identifier.in.use=Name ist schon verwendet. Bitte einen anderen w\u00e4hlen.
error.identifier.url.not.valid=Die URL ist nicht g\u00fcltig. Bitte Sonderzeichen wie $, ?, Leerschl\u00e4ge entfernen.
error.slides.size=Total Gr\u00f6sse den Pr\u00e4sentationsfolien muss kleiner als {0}MB sein.
error.slides.type=Pr\u00e4sentationsfolien sind limitiert zu Office und PDF Dokumenten und Bilder.
error.password=Passwort ist falsch.
error.prefix=Ein Fehler ist aufgetreten\:
error.same.day=Sie haben schon ein Meeting an diesem Tag geplant.
......@@ -104,6 +106,8 @@ meeting.recurring.start=Start wiederkehrendes Datum
meeting.resource=Kontext
meeting.server=Bevorzugter Server
meeting.server.auto=Automatisch (beste Performance)
meeting.slides=Pr\u00e4sentationsfolien
meeting.slides.upload=Pr\u00e4sentationsfolien hochladen
meeting.start=Beginn
meeting.start.button=Online-Termin starten
meeting.template=Raumvorlage
......@@ -149,6 +153,7 @@ servers.title=Server
server.status.available=Verf\u00fcgbar
server.status.offline=Scheint offline zu sein
server.status.disabled=Abgeschaltet
slides.upload.limit=Max. Grösse den Pr\u00e4sentationsfolien (in MB)
table.header.available=Verf\u00fcgbarkeit
table.header.breakout.meetings=\# Breakout
table.header.breakout.recording.meetings=\# Breakout Recording
......
......@@ -56,6 +56,8 @@ error.same.day=You already have a meeting planed at this date.
error.server.exists=A server with this URL already exists.
error.server.raw={1} <small>Key {0}</small>
error.serverDisabled=Server is currently not available.
error.slides.size=Total size of the slides needs to be smaller than {0}MB.
error.slides.type=Slides are limited to office and PDF documents and images.
error.start.after.end=The end date must not be before the start date.
error.too.long.time=Time is too long. It is limited to {0} minutes.
error.url.invalid=Invalid server URL
......@@ -104,6 +106,8 @@ meeting.recurring.start=Start recurring date
meeting.resource=Context
meeting.server=Preferred server
meeting.server.auto=Automatically (best performance)
meeting.slides=Slides
meeting.slides.upload=Upload slides
meeting.start=Start date
meeting.start.button=Start the online-meeting
meeting.template=Room-template
......@@ -149,6 +153,7 @@ server.status.available=Available
server.status.offline=Seems to be offline
server.status.disabled=Disabled
servers.title=Servers
slides.upload.limit=Max. size of slides (in MB)
table.header.available=Availability
table.header.breakout.meetings=\# Breakout
table.header.breakout.recording.meetings=\# Breakout Recording
......
-- BigBlueButton
alter table o_bbb_meeting add column b_password varchar(64) default null;
alter table o_bbb_meeting add column b_directory varchar(64) default null;
alter table o_bbb_meeting add constraint bbb_dir_idx unique (b_directory);
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment