admin.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. # Copyright 2013 The Distro Tracker Developers
  2. # See the COPYRIGHT file at the top-level directory of this distribution and
  3. # at http://deb.li/DTAuthors
  4. #
  5. # This file is part of Distro Tracker. It is subject to the license terms
  6. # in the LICENSE file found in the top-level directory of this
  7. # distribution and at http://deb.li/DTLicense. No part of Distro Tracker,
  8. # including this file, may be copied, modified, propagated, or distributed
  9. # except according to the terms contained in the LICENSE file.
  10. """
  11. Settings for the admin panel for the models defined in the
  12. :mod:`distro_tracker.core` app.
  13. """
  14. from __future__ import unicode_literals
  15. from django.contrib import admin
  16. from django import forms
  17. from .models import Repository
  18. from django.core.exceptions import ValidationError
  19. from django.core.validators import URLValidator
  20. from distro_tracker.core.models import Architecture
  21. from distro_tracker.core.models import RepositoryFlag
  22. from distro_tracker.core.models import RepositoryRelation
  23. from distro_tracker.core.retrieve_data import retrieve_repository_info
  24. from distro_tracker.core.retrieve_data import InvalidRepositoryException
  25. import requests
  26. def validate_sources_list_entry(value):
  27. """
  28. A custom validator for the sources.list entry form field.
  29. Makes sure that it follows the correct syntax and that the specified Web
  30. resource is available.
  31. :param value: The value of the sources.list entry which needs to be
  32. validated
  33. :raises ValidationError: Giving the validation failure message.
  34. """
  35. split = value.split(None, 3)
  36. if len(split) < 3:
  37. raise ValidationError("Invalid syntax: all parts not found.")
  38. repository_type, url, name = split[:3]
  39. if repository_type not in ('deb', 'deb-src'):
  40. raise ValidationError(
  41. "Invalid syntax: the line must start with deb or deb-src")
  42. url_validator = URLValidator()
  43. try:
  44. url_validator(url)
  45. except ValidationError:
  46. raise ValidationError("Invalid repository URL")
  47. # Check whether a Release file even exists.
  48. if url.endswith('/'):
  49. url = url.rstrip('/')
  50. try:
  51. response = requests.head(Repository.release_file_url(url, name),
  52. allow_redirects=True)
  53. except requests.exceptions.Timeout:
  54. raise ValidationError(
  55. "Invalid repository: Could not connect to {url}."
  56. " Request timed out.".format(url=url))
  57. except requests.exceptions.ConnectionError:
  58. raise ValidationError(
  59. "Invalid repository: Could not connect to {url} due to a network"
  60. " problem. The URL may not exist or is refusing to receive"
  61. " connections.".format(url=url))
  62. except requests.exceptions.HTTPError:
  63. raise ValidationError(
  64. "Invalid repository:"
  65. " Received an invalid HTTP response from {url}.".format(url=url))
  66. except:
  67. raise ValidationError(
  68. "Invalid repository: Could not connect to {url}".format(url=url))
  69. if response.status_code != 200:
  70. raise ValidationError(
  71. "Invalid repository: No Release file found. "
  72. "received a {status_code} HTTP response code.".format(
  73. status_code=response.status_code))
  74. class RepositoryAdminForm(forms.ModelForm):
  75. """
  76. A custom :class:`ModelForm <django.forms.ModelForm>` used for creating and
  77. modifying :class:`Repository <distro_tracker.core.models.Repository>` model
  78. instances.
  79. The class adds the ability to enter only a sources.list entry describing
  80. the repository and other properties of the repository are automatically
  81. filled in by using the ``Release`` file of the repository.
  82. """
  83. #: The additional form field which allows entring the sources.list entry
  84. sources_list_entry = forms.CharField(
  85. required=False,
  86. help_text="You can enter a sources.list formatted entry and have the"
  87. " rest of the fields automatically filled by using the "
  88. "Release file of the repository.",
  89. max_length=200,
  90. widget=forms.TextInput(attrs={
  91. 'size': 100,
  92. }),
  93. validators=[
  94. validate_sources_list_entry,
  95. ]
  96. )
  97. flags = forms.MultipleChoiceField(
  98. required=False,
  99. widget=forms.CheckboxSelectMultiple, choices=RepositoryFlag.FLAG_NAMES)
  100. class Meta:
  101. model = Repository
  102. exclude = (
  103. 'position',
  104. 'source_packages',
  105. )
  106. def __init__(self, *args, **kwargs):
  107. # Inject initial data for flags field
  108. initial = kwargs.get('initial', {})
  109. instance = kwargs.get('instance', None)
  110. if instance is None:
  111. flags = RepositoryFlag.FLAG_DEFAULT_VALUES
  112. else:
  113. flags = instance.get_flags()
  114. initial['flags'] = [
  115. flag_name for flag_name in flags if flags[flag_name]
  116. ]
  117. kwargs['initial'] = initial
  118. super(RepositoryAdminForm, self).__init__(*args, **kwargs)
  119. # Fields can't be required if we want to support different methods of
  120. # setting their value through the same form (sources.list and directly)
  121. # The clean method makes sure that they are set in the end.
  122. # So, save originally required fields in order to check them later.
  123. self.original_required_fields = []
  124. for name, field in self.fields.items():
  125. if field.required:
  126. field.required = False
  127. self.original_required_fields.append(name)
  128. # These fields are always required
  129. self.fields['name'].required = True
  130. self.fields['shorthand'].required = True
  131. def clean(self, *args, **kwargs):
  132. """
  133. Overrides the :meth:`clean <django.forms.ModelForm.clean>` method of the
  134. parent class to allow validating the form based on the sources.list
  135. entry, not only the model fields.
  136. """
  137. self.cleaned_data = super(RepositoryAdminForm, self).clean(*args,
  138. **kwargs)
  139. if 'sources_list_entry' not in self.cleaned_data:
  140. # Sources list entry was given to the form but it failed
  141. # validation.
  142. return self.cleaned_data
  143. # Check if the entry was not even given
  144. if not self.cleaned_data['sources_list_entry']:
  145. # If not, all the fields required by the model must be found
  146. # instead
  147. for name in self.original_required_fields:
  148. self.fields[name].required = True
  149. self._clean_fields()
  150. else:
  151. # If it was given, need to make sure now that the Relase file
  152. # contains usable data.
  153. try:
  154. repository_info = retrieve_repository_info(
  155. self.cleaned_data['sources_list_entry'])
  156. except InvalidRepositoryException:
  157. raise ValidationError("The Release file was invalid.")
  158. # Use the data to construct a Repository object.
  159. self.cleaned_data.update(repository_info)
  160. # Architectures have to be matched with their primary keys
  161. self.cleaned_data['architectures'] = [
  162. Architecture.objects.get(name=architecture_name).pk
  163. for architecture_name in self.cleaned_data['architectures']
  164. if Architecture.objects.filter(name=architecture_name).exists()
  165. ]
  166. return self.cleaned_data
  167. class RepositoryAdmin(admin.ModelAdmin):
  168. """
  169. Actual configuration for the
  170. :class:`Repository <distro_tracker.core.models.Repository>`
  171. admin panel.
  172. """
  173. class Media:
  174. """
  175. Add extra Javascript resources to the page in order to support
  176. drag-and-drop repository position modification.
  177. """
  178. js = (
  179. 'js/jquery.min.js',
  180. 'js/jquery-ui.min.js',
  181. 'js/admin-list-reorder.js',
  182. )
  183. form = RepositoryAdminForm
  184. # Sections the form in multiple parts
  185. fieldsets = [
  186. (None, {
  187. 'fields': [
  188. 'name',
  189. 'shorthand',
  190. ]
  191. }),
  192. ('sources.list entry', {
  193. 'fields': [
  194. 'sources_list_entry',
  195. ]
  196. }),
  197. ('Repository information', {
  198. 'fields': [
  199. 'uri',
  200. 'public_uri',
  201. 'codename',
  202. 'suite',
  203. 'components',
  204. 'architectures',
  205. 'default',
  206. 'optional',
  207. 'binary',
  208. 'source',
  209. ]
  210. }),
  211. ('Repository flags', {
  212. 'fields': [
  213. 'flags',
  214. ]
  215. }),
  216. ]
  217. #: Gives a list of fields which should be displayed as columns in the
  218. #: list of existing
  219. #: :class:`Repository <distro_tracker.core.models.Repository>` instances.
  220. list_display = (
  221. 'name',
  222. 'shorthand',
  223. 'codename',
  224. 'uri',
  225. 'components_string',
  226. 'architectures_string',
  227. 'default',
  228. 'optional',
  229. 'binary',
  230. 'source',
  231. 'position',
  232. 'flags_string',
  233. )
  234. ordering = (
  235. 'position',
  236. )
  237. list_editable = (
  238. 'position',
  239. )
  240. def save_model(self, request, obj, form, change):
  241. if not change and obj.position == 0:
  242. obj.position = Repository.objects.count() + 1
  243. obj.save()
  244. if 'flags' not in form.cleaned_data:
  245. return
  246. for flag in RepositoryFlag.FLAG_DEFAULT_VALUES:
  247. value = flag in form.cleaned_data['flags']
  248. try:
  249. repo_flag = obj.flags.get(name=flag)
  250. repo_flag.value = value
  251. repo_flag.save()
  252. except RepositoryFlag.DoesNotExist:
  253. obj.flags.create(name=flag, value=value)
  254. def components_string(self, obj):
  255. """
  256. Helper method for displaying Repository objects.
  257. Turns the components list into a display-friendly string.
  258. :param obj: The repository whose components are to be formatted
  259. :type obj: :class:`Repository <distro_tracker.core.models.Repository>`
  260. """
  261. return ' '.join(obj.components)
  262. components_string.short_description = 'components'
  263. def architectures_string(self, obj):
  264. """
  265. Helper method for displaying Repository objects.
  266. Turns the architectures list into a display-friendly string.
  267. :param obj: The repository whose architectures are to be formatted
  268. :type obj: :class:`Repository <distro_tracker.core.models.Repository>`
  269. """
  270. return ' '.join(map(str, obj.architectures.all()))
  271. architectures_string.short_description = 'architectures'
  272. def flags_string(self, obj):
  273. return ' '.join("{}={}".format(x.name, x.value)
  274. for x in obj.flags.all())
  275. flags_string.short_description = 'Flags'
  276. class RepositoryRelationAdmin(admin.ModelAdmin):
  277. """
  278. Actual configuration for the
  279. :class:`Repository <distro_tracker.core.models.RepositoryRelation>`
  280. admin panel.
  281. """
  282. list_display = (
  283. 'repository',
  284. 'name',
  285. 'target_repository',
  286. )
  287. admin.site.register(Repository, RepositoryAdmin)
  288. admin.site.register(RepositoryRelation, RepositoryRelationAdmin)