What we decided to do was to delete the app store because the people we manage have a habit of installing games and we made everything downloadable via Self Service. The thing I like about Self Service is that I don't dictate what is on "their" iPads. We have around 40 apps in Self Service (using VPP) and if the they only uses 5, it is a waste of space to install 40 apps on their device. We used a script that we found on JAMF nation that leverages the JSS API and creates static mobile device groups based on class enrollment (from a CSV file- maybe from your student information system?). It is very handy.
All in all, I would delete the app store, we tried to keep it on the iPad for years but, they didn't have enough self control. Self Service has been wonderful. We recommend using the web clip because it is much easier than having the app on the device.
Good Luck!
#!/usr/bin/env python# Limitations:# - Only one device per student is supported# - Classes may not contain a + or / character as the API cannot parse these at this time# Settings
jssAddr = "https://casper.yourcompany.org:8443"# JSS server address with or without https:// and port
jssUser = "JSSLogin"# JSS login username (API privileges must be set)
jssPass = "Password"# JSS login password
csvPath = "/users/Admin/desktop/jssscript/StudentsByCourse.csv"# Path and file name for the input CSV file# Importsfrom urllib2 import urlopen, URLError, HTTPError, Request
from xml.dom import minidom
from xml.sax import make_parser, ContentHandler
from sys import exc_info, argv, stdout, stdin
from getpass import getpass
import base64
import csv
import re
defmain():global jssAddr
global jssUser
global jssPass
global csvPath
updateMDGroups = JSSUpdateMobileDeviceGroups()
studentList = updateMDGroups.getCSVData(csvPath)
updateMDGroups.updateGroups(jssAddr, jssUser, jssPass, studentList)
classJSSUpdateMobileDeviceGroups:def__init__(self):
self.numMobileDevicesUpdated = 0
self.deviceMap = dict()
defgetCSVData(self, csvPath):# Read in CSV
csvinput = open(csvPath)
reader = csv.reader(csvinput)
return reader
defupdateGroups(self, jssAddr, jssUser, jssPass, studentList):# Cache mobile device ID to student username mapping
self.grabMobileDeviceData(jssAddr, jssUser, jssPass)
# Assign student device to the classes (groups)
url = jssAddr + "/JSSResource/mobiledevicegroups"
grabber = CasperGroupPUTHandler(jssUser, jssPass)
for studentLine in studentList:
if studentLine[0] != "Student ID":
self.handleStudentDeviceAssignment(grabber, url, studentLine)
print"Successfully updated %d devices in the JSS." % self.numMobileDevicesUpdated
defgrabMobileDeviceData(self, jssAddr, jssUser, jssPass):
url = jssAddr + "/JSSResource/mobiledevices"
grabber = CasperDeviceXMLGrabber(jssUser, jssPass)
grabber.parseXML(url, CasperMobileDeviceListHandler(grabber, self.deviceMap))
defhandleStudentDeviceAssignment(self, grabber, url, studentLine):# Create mobile device studentLine XML... if studentLine[0] in self.deviceMap:
if"/"in studentLine[1] or"+"in studentLine[1]:
print"Error: User: %s, Class: %s, Error: Class contains forward slash or ends in plus character" % (studentLine[0], studentLine[1])
else:
studentDeviceID = self.deviceMap[studentLine[0]]
newGroupAssignment = self.createNewGroupElement(studentDeviceID, studentLine[1])
self.handleGroupPUT(grabber, url, studentLine[1], newGroupAssignment)
else:
print"Error: User: %s, Class: %s, Error: Could not find a mobile device match for student username" % (studentLine[0], studentLine[1])
defhandleGroupPUT(self, grabber, url, className, newGroupAssignment):# PUT new XML
apiClassURLRAW = url + "/name/" + className
apiClassURL = apiClassURLRAW.replace (' ', '+')
apiClassName = className.replace ('+', '')
apiClassName = apiClassName.replace (' ', '+')
###########UNCOMMENT NEXT TWO LINES FOR DEBUG MODE##############print "PUT-ing URL %s: " % (apiClassURL)#print newGroupAssignment.toprettyxml()
putStatus = grabber.openXMLStream("%s/name/%s" % (url, apiClassName), newGroupAssignment)
if putStatus isNone:
self.numMobileDevicesUpdated += 1defcreateNewGroupElement(self, studentDeviceID, groupName):global eventValues
newGroupAssignment = minidom.Document()
group = self.appendEmptyElement(newGroupAssignment, newGroupAssignment, "mobile_device_group")
self.appendNewTextElement(newGroupAssignment, group, "name", groupName)
groupAdditions = self.appendEmptyElement(newGroupAssignment, group, "mobile_device_additions")
deviceElement = self.appendEmptyElement(newGroupAssignment, groupAdditions, "mobile_device")
self.appendNewTextElement(newGroupAssignment, deviceElement, "id", studentDeviceID)
return newGroupAssignment
defappendEmptyElement(self, doc, section, newElementTag):
newElement = doc.createElement(newElementTag)
section.appendChild(newElement)
return newElement
defappendNewTextElement(self, doc, section, newElementTag, newElementValue):
newElement = self.appendEmptyElement(doc, section, newElementTag)
newValueElement = doc.createTextNode(newElementValue)
newElement.appendChild(newValueElement)
return newElement
classCasperDeviceXMLGrabber:def__init__(self, jssUser, jssPass):
self.jssUser = jssUser
self.jssPass = jssPass
defparseXML(self, url, handler):
req = Request(url)
base64string = base64.encodestring('%s:%s' % (self.jssUser, self.jssPass))[:-1]
authheader = "Basic %s" % base64string
req.add_header("Authorization", authheader)
try:
MobileDeviceList = urlopen(req)
except (URLError, HTTPError) as urlError: # Catch errors related to urlopen()print"Error when opening URL: " + urlError.__str__()
exit(1)
except: # Catch any unexpected problems and bail out of the programprint"Unexpected error:", exc_info()[0]
exit(1)
parser = make_parser()
parser.setContentHandler(handler)
parser.parse(MobileDeviceList)
classCasperGroupPUTHandler:def__init__(self, jssUser, jssPass):
self.jssUser = jssUser
self.jssPass = jssPass
defparseXML(self, url):return self.openXMLStream(url, None)
defopenXMLStream(self, url, xmlout):try:
if xmlout isNone:
req = Request(url)
else:
req = Request(url, data=xmlout.toxml())
req.add_header('Content-Type', 'text/xml')
req.get_method = lambda: 'PUT'
base64string = base64.encodestring('%s:%s' % (self.jssUser, self.jssPass))[:-1]
authheader = "Basic %s" % base64string
req.add_header("Authorization", authheader)
xmldata = urlopen(req)
if xmlout isNone:
xmldoc = minidom.parse(xmldata)
else:
xmldoc = Nonereturn xmldoc
except (URLError, HTTPError) as urlError: # Catch errors related to urlopen()if urlError.code == 404:
if xmlout isNone:
req = Request(url)
else:
req = Request(url, data=xmlout.toxml())
req.add_header('Content-Type', 'text/xml')
req.get_method = lambda: 'POST'
base64string = base64.encodestring('%s:%s' % (self.jssUser, self.jssPass))[:-1]
authheader = "Basic %s" % base64string
req.add_header("Authorization", authheader)
xmldata = urlopen(req)
if xmlout isNone:
xmldoc = minidom.parse(xmldata)
else:
xmldoc = Nonereturn xmldoc
print"Error when opening URL %s - %s " % (url, urlError.__str__())
return"Error"except: # Catch any unexpected problems and bail out of the programprint"Unexpected error:", exc_info()[0]
exit(1)
# This class is used to parse the /mobiledevices list to get all of the ids and usernamesclassCasperMobileDeviceListHandler(ContentHandler):def__init__(self, grabber, deviceMap):
ContentHandler.__init__(self)
self.grabber = grabber
self.deviceMap = deviceMap
self.currentID = ""
self.inID = False
self.inSize = False
self.inUsername = FalsedefstartElement(self, tag, attributes):if tag == "id":
self.inID = Trueelif tag == "username":
self.inUsername = Trueelif tag == "size":
self.inSize = TruedefendElement(self, tag):
self.inID = False
self.inSize = False
self.inUsername = Falseif tag == "mobiledevices":
print"Finished collecting mobile devices for lookup"defcharacters(self, data):if self.inID:
self.currentID = data
elif self.inUsername:
if data != "":
self.deviceMap[data] = self.currentID
elif self.inSize:
self.numDevices = data
print"Collecting data for " + data + " Mobile Device(s)..."if __name__ == "__main__":
main()
We use 3 different kinds of cookies. You can choose which cookies you want to accept. We need basic cookies to make this site work, therefore these are the minimum you can select. Learn more about our cookies.