utils.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. from collections import defaultdict
  2. from itertools import chain
  3. from operator import attrgetter, itemgetter
  4. import re
  5. from django.core.serializers.json import DjangoJSONEncoder
  6. from django.db import connection
  7. from django.db.models import Count, Max, F
  8. from django.db.models.query import QuerySet
  9. from django.contrib.auth.models import User
  10. from main.models import Package, PackageFile, Arch, Repo
  11. from main.utils import (database_vendor,
  12. groupby_preserve_order, PackageStandin)
  13. from .models import (PackageGroup, PackageRelation,
  14. License, Depend, Conflict, Provision, Replacement,
  15. SignoffSpecification, Signoff, fake_signoff_spec)
  16. VERSION_RE = re.compile(r'^((\d+):)?(.+)-([^-]+)$')
  17. def parse_version(version):
  18. match = VERSION_RE.match(version)
  19. if not match:
  20. return None, None, 0
  21. ver = match.group(3)
  22. rel = match.group(4)
  23. if match.group(2):
  24. epoch = int(match.group(2))
  25. else:
  26. epoch = 0
  27. return ver, rel, epoch
  28. def get_group_info(include_arches=None):
  29. raw_groups = PackageGroup.objects.values_list(
  30. 'name', 'pkg__arch__name').order_by('name').annotate(
  31. cnt=Count('pkg'), last_update=Max('pkg__last_update'))
  32. # now for post_processing. we need to seperate things out and add
  33. # the count in for 'any' to all of the other architectures.
  34. group_mapping = {}
  35. for grp in raw_groups:
  36. arch_groups = group_mapping.setdefault(grp[1], {})
  37. arch_groups[grp[0]] = {'name': grp[0], 'arch': grp[1],
  38. 'count': grp[2], 'last_update': grp[3]}
  39. # we want to promote the count of 'any' packages in groups to the
  40. # other architectures, and also add any 'any'-only groups
  41. if 'any' in group_mapping:
  42. any_groups = group_mapping['any']
  43. del group_mapping['any']
  44. for arch, arch_groups in group_mapping.iteritems():
  45. for grp in any_groups.itervalues():
  46. if grp['name'] in arch_groups:
  47. found = arch_groups[grp['name']]
  48. found['count'] += grp['count']
  49. if grp['last_update'] > found['last_update']:
  50. found['last_update'] = grp['last_update']
  51. else:
  52. new_g = grp.copy()
  53. # override the arch to not be 'any'
  54. new_g['arch'] = arch
  55. arch_groups[grp['name']] = new_g
  56. # now transform it back into a sorted list, including only the specified
  57. # architectures if we got a list
  58. groups = []
  59. for key, val in group_mapping.iteritems():
  60. if not include_arches or key in include_arches:
  61. groups.extend(val.itervalues())
  62. return sorted(groups, key=itemgetter('name', 'arch'))
  63. def get_split_packages_info():
  64. '''Return info on split packages that do not have an actual package name
  65. matching the split pkgbase.'''
  66. pkgnames = Package.objects.values('pkgname')
  67. split_pkgs = Package.objects.exclude(pkgname=F('pkgbase')).exclude(
  68. pkgbase__in=pkgnames).values('pkgbase', 'repo', 'arch').annotate(
  69. last_update=Max('last_update')).order_by().distinct()
  70. all_arches = Arch.objects.in_bulk({s['arch'] for s in split_pkgs})
  71. all_repos = Repo.objects.in_bulk({s['repo'] for s in split_pkgs})
  72. for split in split_pkgs:
  73. split['arch'] = all_arches[split['arch']]
  74. split['repo'] = all_repos[split['repo']]
  75. return split_pkgs
  76. class Difference(object):
  77. def __init__(self, pkgname, repo, pkg_a, pkg_b):
  78. self.pkgname = pkgname
  79. self.repo = repo
  80. self.pkg_a = pkg_a
  81. self.pkg_b = pkg_b
  82. def classes(self):
  83. '''A list of CSS classes that should be applied to this row in any
  84. generated HTML. Useful for sorting, filtering, etc. Contains whether
  85. this difference is in both architectures or the sole architecture it
  86. belongs to, as well as the repo name.'''
  87. css_classes = [self.repo.name.lower()]
  88. if self.pkg_a and self.pkg_b:
  89. css_classes.append('both')
  90. elif self.pkg_a:
  91. css_classes.append(self.pkg_a.arch.name)
  92. elif self.pkg_b:
  93. css_classes.append(self.pkg_b.arch.name)
  94. return ' '.join(css_classes)
  95. def __key(self):
  96. return (self.pkgname, hash(self.repo),
  97. hash(self.pkg_a), hash(self.pkg_b))
  98. def __eq__(self, other):
  99. return self.__key() == other.__key()
  100. def __hash__(self):
  101. return hash(self.__key())
  102. def get_differences_info(arch_a, arch_b):
  103. # This is a monster. Join packages against itself, looking for packages in
  104. # our non-'any' architectures only, and not having a corresponding package
  105. # entry in the other table (or having one with a different pkgver). We will
  106. # then go and fetch all of these packages from the database and display
  107. # them later using normal ORM models.
  108. sql = """
  109. SELECT p.id, q.id
  110. FROM packages p
  111. LEFT JOIN packages q
  112. ON (
  113. p.pkgname = q.pkgname
  114. AND p.repo_id = q.repo_id
  115. AND p.arch_id != q.arch_id
  116. AND p.id != q.id
  117. )
  118. WHERE p.arch_id IN (%s, %s)
  119. AND (
  120. q.arch_id IN (%s, %s)
  121. OR q.id IS NULL
  122. )
  123. AND (
  124. q.id IS NULL
  125. OR p.pkgver != q.pkgver
  126. OR p.pkgrel != q.pkgrel
  127. OR p.epoch != q.epoch
  128. )
  129. """
  130. cursor = connection.cursor()
  131. cursor.execute(sql, [arch_a.id, arch_b.id, arch_a.id, arch_b.id])
  132. results = cursor.fetchall()
  133. # column A will always have a value, column B might be NULL
  134. to_fetch = {row[0] for row in results}
  135. # fetch all of the necessary packages
  136. pkgs = Package.objects.normal().in_bulk(to_fetch)
  137. # now build a set containing differences
  138. differences = set()
  139. for row in results:
  140. pkg_a = pkgs.get(row[0])
  141. pkg_b = pkgs.get(row[1])
  142. # We want arch_a to always appear first
  143. # pkg_a should never be None
  144. if pkg_a.arch == arch_a:
  145. item = Difference(pkg_a.pkgname, pkg_a.repo, pkg_a, pkg_b)
  146. else:
  147. # pkg_b can be None in this case, so be careful
  148. name = pkg_a.pkgname if pkg_a else pkg_b.pkgname
  149. repo = pkg_a.repo if pkg_a else pkg_b.repo
  150. item = Difference(name, repo, pkg_b, pkg_a)
  151. differences.add(item)
  152. # now sort our list by repository, package name
  153. key_func = attrgetter('repo.name', 'pkgname')
  154. differences = sorted(differences, key=key_func)
  155. return differences
  156. def multilib_differences():
  157. # Query for checking multilib out of date-ness
  158. if database_vendor(Package) == 'sqlite':
  159. pkgname_sql = """
  160. CASE WHEN ml.pkgname LIKE %s
  161. THEN SUBSTR(ml.pkgname, 7)
  162. WHEN ml.pkgname LIKE %s
  163. THEN SUBSTR(ml.pkgname, 1, LENGTH(ml.pkgname) - 9)
  164. ELSE
  165. ml.pkgname
  166. END
  167. """
  168. else:
  169. pkgname_sql = """
  170. CASE WHEN ml.pkgname LIKE %s
  171. THEN SUBSTRING(ml.pkgname, 7)
  172. WHEN ml.pkgname LIKE %s
  173. THEN SUBSTRING(ml.pkgname FROM 1 FOR CHAR_LENGTH(ml.pkgname) - 9)
  174. ELSE
  175. ml.pkgname
  176. END
  177. """
  178. sql = """
  179. SELECT ml.id, reg.id
  180. FROM packages ml
  181. JOIN packages reg
  182. ON (
  183. reg.pkgname = (""" + pkgname_sql + """)
  184. AND reg.pkgver != ml.pkgver
  185. )
  186. JOIN repos r ON reg.repo_id = r.id
  187. WHERE ml.repo_id = %s
  188. AND r.testing = %s
  189. AND r.staging = %s
  190. AND reg.arch_id = %s
  191. ORDER BY ml.last_update
  192. """
  193. multilib = Repo.objects.get(name__iexact='multilib')
  194. i686 = Arch.objects.get(name='i686')
  195. params = ['lib32-%', '%-multilib', multilib.id, False, False, i686.id]
  196. cursor = connection.cursor()
  197. cursor.execute(sql, params)
  198. results = cursor.fetchall()
  199. # fetch all of the necessary packages
  200. to_fetch = set(chain.from_iterable(results))
  201. pkgs = Package.objects.normal().in_bulk(to_fetch)
  202. return [(pkgs[ml], pkgs[reg]) for ml, reg in results]
  203. def get_wrong_permissions():
  204. sql = """
  205. SELECT DISTINCT id
  206. FROM (
  207. SELECT pr.id, p.repo_id, pr.user_id
  208. FROM packages p
  209. JOIN packages_packagerelation pr ON p.pkgbase = pr.pkgbase
  210. WHERE pr.type = %s
  211. ) mp
  212. LEFT JOIN (
  213. SELECT user_id, repo_id FROM user_profiles_allowed_repos ar
  214. INNER JOIN user_profiles up ON ar.userprofile_id = up.id
  215. ) ur
  216. ON mp.user_id = ur.user_id AND mp.repo_id = ur.repo_id
  217. WHERE ur.user_id IS NULL;
  218. """
  219. cursor = connection.cursor()
  220. cursor.execute(sql, [PackageRelation.MAINTAINER])
  221. to_fetch = [row[0] for row in cursor.fetchall()]
  222. relations = PackageRelation.objects.select_related(
  223. 'user', 'user__userprofile').filter(
  224. id__in=to_fetch)
  225. return relations
  226. def attach_maintainers(packages):
  227. '''Given a queryset or something resembling it of package objects, find all
  228. the maintainers and attach them to the packages to prevent N+1 query
  229. cascading.'''
  230. if isinstance(packages, QuerySet):
  231. pkgbases = packages.values('pkgbase')
  232. else:
  233. packages = list(packages)
  234. pkgbases = {p.pkgbase for p in packages if p is not None}
  235. rels = PackageRelation.objects.filter(type=PackageRelation.MAINTAINER,
  236. pkgbase__in=pkgbases).values_list(
  237. 'pkgbase', 'user_id').order_by().distinct()
  238. # get all the user objects we will need
  239. user_ids = {rel[1] for rel in rels}
  240. users = User.objects.in_bulk(user_ids)
  241. # now build a pkgbase -> [maintainers...] map
  242. maintainers = defaultdict(list)
  243. for rel in rels:
  244. maintainers[rel[0]].append(users[rel[1]])
  245. annotated = []
  246. # and finally, attach the maintainer lists on the original packages
  247. for package in packages:
  248. if package is None:
  249. continue
  250. package.maintainers = maintainers[package.pkgbase]
  251. annotated.append(package)
  252. return annotated
  253. def approved_by_signoffs(signoffs, spec):
  254. if signoffs:
  255. good_signoffs = sum(1 for s in signoffs if not s.revoked)
  256. return good_signoffs >= spec.required
  257. return False
  258. class PackageSignoffGroup(object):
  259. '''Encompasses all packages in testing with the same pkgbase.'''
  260. def __init__(self, packages):
  261. if len(packages) == 0:
  262. raise Exception
  263. self.packages = packages
  264. self.user = None
  265. self.target_repo = None
  266. self.signoffs = set()
  267. self.default_spec = True
  268. first = packages[0]
  269. self.pkgbase = first.pkgbase
  270. self.arch = first.arch
  271. self.repo = first.repo
  272. self.version = ''
  273. self.last_update = first.last_update
  274. self.packager = first.packager
  275. self.maintainers = first.maintainers
  276. self.specification = fake_signoff_spec(first.arch)
  277. version = first.full_version
  278. if all(version == pkg.full_version for pkg in packages):
  279. self.version = version
  280. @property
  281. def package(self):
  282. '''Try and return a relevant single package object representing this
  283. group. Start by seeing if there is only one package, then look for the
  284. matching package by name, finally falling back to a standin package
  285. object.'''
  286. if len(self.packages) == 1:
  287. return self.packages[0]
  288. same_pkgs = [p for p in self.packages if p.pkgname == p.pkgbase]
  289. if same_pkgs:
  290. return same_pkgs[0]
  291. return PackageStandin(self.packages[0])
  292. def find_signoffs(self, all_signoffs):
  293. '''Look through a list of Signoff objects for ones matching this
  294. particular group and store them on the object.'''
  295. for s in all_signoffs:
  296. if s.pkgbase != self.pkgbase:
  297. continue
  298. if self.version and not s.full_version == self.version:
  299. continue
  300. if s.arch_id == self.arch.id and s.repo_id == self.repo.id:
  301. self.signoffs.add(s)
  302. def find_specification(self, specifications):
  303. for spec in specifications:
  304. if spec.pkgbase != self.pkgbase:
  305. continue
  306. if self.version and not spec.full_version == self.version:
  307. continue
  308. if spec.arch_id == self.arch.id and spec.repo_id == self.repo.id:
  309. self.specification = spec
  310. self.default_spec = False
  311. return
  312. def approved(self):
  313. return approved_by_signoffs(self.signoffs, self.specification)
  314. @property
  315. def completed(self):
  316. return sum(1 for s in self.signoffs if not s.revoked)
  317. @property
  318. def required(self):
  319. return self.specification.required
  320. def user_signed_off(self, user=None):
  321. '''Did a given user signoff on this package? user can be passed as an
  322. argument, or attached to the group object itself so this can be called
  323. from a template.'''
  324. if user is None:
  325. user = self.user
  326. return user in (s.user for s in self.signoffs if not s.revoked)
  327. def __unicode__(self):
  328. return u'%s-%s (%s): %d' % (
  329. self.pkgbase, self.version, self.arch, len(self.signoffs))
  330. def signoffs_id_query(model, repos):
  331. sql = """
  332. SELECT DISTINCT s.id
  333. FROM %s s
  334. JOIN packages p ON (
  335. s.pkgbase = p.pkgbase
  336. AND s.pkgver = p.pkgver
  337. AND s.pkgrel = p.pkgrel
  338. AND s.epoch = p.epoch
  339. AND s.arch_id = p.arch_id
  340. AND s.repo_id = p.repo_id
  341. )
  342. WHERE p.repo_id IN (%s)
  343. AND s.repo_id IN (%s)
  344. """
  345. cursor = connection.cursor()
  346. # query pre-process- fill in table name and placeholders for IN
  347. repo_sql = ','.join(['%s' for _ in repos])
  348. sql = sql % (model._meta.db_table, repo_sql, repo_sql)
  349. repo_ids = [r.pk for r in repos]
  350. # repo_ids are needed twice, so double the array
  351. cursor.execute(sql, repo_ids * 2)
  352. results = cursor.fetchall()
  353. return [row[0] for row in results]
  354. def get_current_signoffs(repos):
  355. '''Returns a list of signoff objects for the given repos.'''
  356. to_fetch = signoffs_id_query(Signoff, repos)
  357. return Signoff.objects.select_related('user').in_bulk(to_fetch).values()
  358. def get_current_specifications(repos):
  359. '''Returns a list of signoff specification objects for the given repos.'''
  360. to_fetch = signoffs_id_query(SignoffSpecification, repos)
  361. return SignoffSpecification.objects.select_related('arch').in_bulk(
  362. to_fetch).values()
  363. def get_target_repo_map(repos):
  364. sql = """
  365. SELECT DISTINCT p1.pkgbase, r.name
  366. FROM packages p1
  367. JOIN repos r ON p1.repo_id = r.id
  368. JOIN packages p2 ON p1.pkgbase = p2.pkgbase
  369. WHERE r.staging = %s
  370. AND r.testing = %s
  371. AND p2.repo_id IN (
  372. """
  373. sql += ','.join(['%s' for _ in repos])
  374. sql += ")"
  375. params = [False, False]
  376. params.extend(r.pk for r in repos)
  377. cursor = connection.cursor()
  378. cursor.execute(sql, params)
  379. return dict(cursor.fetchall())
  380. def get_signoff_groups(repos=None, user=None):
  381. if repos is None:
  382. repos = Repo.objects.filter(testing=True)
  383. repo_ids = [r.pk for r in repos]
  384. test_pkgs = Package.objects.select_related(
  385. 'arch', 'repo', 'packager').filter(repo__in=repo_ids)
  386. packages = test_pkgs.order_by('pkgname')
  387. packages = attach_maintainers(packages)
  388. # Filter by user if asked to do so
  389. if user is not None:
  390. packages = [p for p in packages if user == p.packager
  391. or user in p.maintainers]
  392. # Collect all pkgbase values in testing repos
  393. pkgtorepo = get_target_repo_map(repos)
  394. # Collect all possible signoffs and specifications for these packages
  395. signoffs = get_current_signoffs(repos)
  396. specs = get_current_specifications(repos)
  397. same_pkgbase_key = lambda x: (x.repo.name, x.arch.name, x.pkgbase)
  398. grouped = groupby_preserve_order(packages, same_pkgbase_key)
  399. signoff_groups = []
  400. for group in grouped:
  401. signoff_group = PackageSignoffGroup(group)
  402. signoff_group.target_repo = pkgtorepo.get(signoff_group.pkgbase,
  403. "Unknown")
  404. signoff_group.find_signoffs(signoffs)
  405. signoff_group.find_specification(specs)
  406. signoff_groups.append(signoff_group)
  407. return signoff_groups
  408. class PackageJSONEncoder(DjangoJSONEncoder):
  409. pkg_attributes = ['pkgname', 'pkgbase', 'repo', 'arch', 'pkgver',
  410. 'pkgrel', 'epoch', 'pkgdesc', 'url', 'filename', 'compressed_size',
  411. 'installed_size', 'build_date', 'last_update', 'flag_date',
  412. 'maintainers', 'packager']
  413. pkg_list_attributes = ['groups', 'licenses', 'conflicts',
  414. 'provides', 'replaces', 'depends']
  415. def default(self, obj):
  416. if hasattr(obj, '__iter__'):
  417. # mainly for queryset serialization
  418. return list(obj)
  419. if isinstance(obj, Package):
  420. data = {attr: getattr(obj, attr) for attr in self.pkg_attributes}
  421. for attr in self.pkg_list_attributes:
  422. data[attr] = getattr(obj, attr).all()
  423. return data
  424. if isinstance(obj, PackageFile):
  425. filename = obj.filename or ''
  426. return obj.directory + filename
  427. if isinstance(obj, (Repo, Arch)):
  428. return obj.name.lower()
  429. if isinstance(obj, (PackageGroup, License)):
  430. return obj.name
  431. if isinstance(obj, (Depend, Conflict, Provision, Replacement)):
  432. return unicode(obj)
  433. elif isinstance(obj, User):
  434. return obj.username
  435. return super(PackageJSONEncoder, self).default(obj)
  436. # vim: set ts=4 sw=4 et: