├── .env ├── .gitignore ├── .s2iignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── Pipfile ├── Pipfile.lock ├── README.docker ├── README.md ├── conf ├── bash-completion │ └── softwarecollections ├── cron │ └── softwarecollections ├── httpd │ ├── environment.override.conf │ ├── softwarecollections.conf │ └── softwarecollections.env └── rsyncd │ ├── rsyncd.conf │ └── softwarecollections-rsyncd.service ├── data └── .gitignore ├── example-data.yaml ├── guide-build ├── guide-import ├── guide-source ├── guide-templatize ├── htdocs ├── media │ └── .gitignore ├── repos │ └── .gitignore ├── static │ └── .gitignore └── wsgi.py ├── manage.py ├── packaging-guide-1.tar.gz ├── rel-eng ├── lib │ └── setup_builder.py ├── packages │ ├── .readme │ └── softwarecollections ├── releasers.conf └── tito.props ├── run-devel ├── run-docker-test ├── setup.cfg ├── setup.py ├── softwarecollections-db-setup ├── softwarecollections-services-setup ├── softwarecollections.spec ├── softwarecollections ├── __init__.py ├── apps.py ├── auth │ ├── __init__.py │ ├── apps.py │ ├── backend.py │ └── templatetags │ │ ├── __init__.py │ │ └── auth.py ├── copr.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── makeerrorpages.py │ │ ├── makesuperuser.py │ │ └── savekey.py ├── middleware │ ├── __init__.py │ └── forwarded.py ├── pages │ ├── __init__.py │ └── views.py ├── scls │ ├── __init__.py │ ├── admin.py │ ├── apps.py │ ├── checks.py │ ├── forms.py │ ├── management │ │ ├── __init__.py │ │ └── commands │ │ │ ├── __init__.py │ │ │ ├── sclcreaterepo.py │ │ │ ├── sclotherrepos.py │ │ │ ├── sclprovides.py │ │ │ ├── sclrelated.py │ │ │ ├── sclrpms.py │ │ │ └── sclsync.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_centos_repos.py │ │ ├── 0003_other_repos.py │ │ ├── 0004_other_repos_default_values.py │ │ └── __init__.py │ ├── models.py │ ├── scl-release.spec │ ├── static │ │ └── scls │ │ │ ├── icons │ │ │ ├── centos.png │ │ │ ├── empty.png │ │ │ ├── epel.png │ │ │ ├── fedora.png │ │ │ └── rhel.png │ │ │ └── javascripts │ │ │ └── collection-add-edit.js │ ├── templates │ │ └── scls │ │ │ ├── base.html │ │ │ ├── filter.html │ │ │ ├── form │ │ │ ├── table_option.html │ │ │ └── table_select.html │ │ │ ├── list_all.html │ │ │ ├── list_base.html │ │ │ ├── list_my.html │ │ │ ├── list_tag.html │ │ │ ├── list_user.html │ │ │ ├── pagination.html │ │ │ ├── softwarecollection_acl.html │ │ │ ├── softwarecollection_base.html │ │ │ ├── softwarecollection_complain.html │ │ │ ├── softwarecollection_coprs.html │ │ │ ├── softwarecollection_delete.html │ │ │ ├── softwarecollection_detail.html │ │ │ ├── softwarecollection_edit.html │ │ │ ├── softwarecollection_new.html │ │ │ ├── softwarecollection_preview.html │ │ │ ├── softwarecollection_repos.html │ │ │ ├── softwarecollection_review_req.html │ │ │ ├── softwarecollection_sub_base.html │ │ │ ├── softwarecollection_sync_req.html │ │ │ ├── softwarecollection_toolbar.html │ │ │ ├── stars.html │ │ │ └── submenu.html │ ├── templatetags │ │ ├── policy_name.py │ │ ├── rating_stars.py │ │ └── truncate_tags.py │ ├── tests.py │ ├── urls.py │ ├── validators.py │ └── views.py ├── settings │ ├── __init__.py │ └── env_util.py ├── static │ ├── .gitignore │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff │ ├── images │ │ ├── apple-touch-icon-114x114-precomposed.png │ │ ├── apple-touch-icon-57x57-precomposed.png │ │ ├── apple-touch-icon-72x72-precomposed.png │ │ ├── apple-touch-icon-precomposed.png │ │ ├── blurr4.jpg │ │ ├── coding-environments.png │ │ ├── favicon.ico │ │ ├── logo.png │ │ ├── rpm.png │ │ ├── star.png │ │ └── star_empty.png │ ├── index-footer.html │ ├── index-header.html │ ├── javascripts │ │ ├── application.js │ │ ├── bootstrap.js │ │ └── bootstrap.min.js │ └── stylesheets │ │ ├── bootstrap-theme.css │ │ ├── bootstrap-theme.css.map │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── custom.css │ │ ├── guide.css │ │ └── index.css ├── templates │ ├── 400.html │ ├── 403.html │ ├── 404.html │ ├── 500.html │ ├── admin │ │ └── base_site.html │ ├── base.html │ ├── default.html │ ├── home.html │ ├── menu.html │ ├── pages │ │ ├── en.html │ │ └── en │ │ │ ├── about.html │ │ │ ├── about │ │ │ ├── developers.html │ │ │ ├── distros.html │ │ │ └── users.html │ │ │ ├── docs.html │ │ │ └── docs │ │ │ ├── .gitignore │ │ │ ├── add-to-catalogue.html │ │ │ ├── developers.html │ │ │ ├── distros.html │ │ │ ├── faq.html │ │ │ ├── licensing.html │ │ │ └── users.html │ ├── section_about.html │ ├── section_docs.html │ ├── section_docs_guide.html │ ├── submenu.html │ ├── submenu_about.html │ ├── submenu_docs.html │ └── with_submenu.html ├── templatetags │ └── menu_item.py ├── tests.py ├── urls.py └── wsgi.py ├── tests ├── conftest.py ├── test_management.py ├── test_middleware_forwarded.py ├── test_regression.py ├── test_sanity.py └── test_settings.py └── tox.ini /.env: -------------------------------------------------------------------------------- 1 | SCL_DEBUG=True 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | softwarecollections/localsettings.py 2 | rel-eng/releasers.conf 3 | data/db.sqlite3 4 | *.pyc 5 | *.pyo 6 | softwarecollections-*.tar.gz 7 | packaging-guide 8 | *.swp 9 | venv-*/ 10 | 11 | # added by gitignore-cli 12 | 13 | # Byte-compiled / optimized / DLL files 14 | __pycache__/ 15 | *.py[cod] 16 | *$py.class 17 | 18 | # C extensions 19 | *.so 20 | 21 | # Distribution / packaging 22 | .Python 23 | build/ 24 | develop-eggs/ 25 | dist/ 26 | downloads/ 27 | eggs/ 28 | .eggs/ 29 | lib/ 30 | !rel-eng/lib/ 31 | lib64/ 32 | parts/ 33 | sdist/ 34 | var/ 35 | wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .coverage 55 | .coverage.* 56 | .cache 57 | nosetests.xml 58 | coverage.xml 59 | *.cover 60 | .hypothesis/ 61 | .pytest_cache/ 62 | 63 | # Translations 64 | *.mo 65 | *.pot 66 | 67 | # Django stuff: 68 | *.log 69 | local_settings.py 70 | db.sqlite3 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # celery beat schedule file 96 | celerybeat-schedule 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | pyproject.toml 125 | -------------------------------------------------------------------------------- /.s2iignore: -------------------------------------------------------------------------------- 1 | # Git is needed for version detection 2 | !.git/ 3 | # RPM-only directories 4 | conf/ 5 | data/ 6 | htdocs/ 7 | rel-eng/ 8 | # Unneeded files 9 | .s2iignore 10 | Dockerfile 11 | example-data.yaml 12 | README.docker 13 | run-* 14 | softwarecollections-db-setup 15 | softwarecollections-services-setup 16 | softwarecollections.spec 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | sudo: false 3 | language: python 4 | python: 5 | - "3.6" 6 | # - "3.7" # currently unsupported (old OpenSSL) 7 | env: 8 | - DJANGO="1.10" 9 | - DJANGO="1.11" 10 | - DJANGO="2.1" 11 | install: pip install tox-travis 12 | script: tox 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM fedora:25 2 | 3 | RUN dnf -y install git && dnf clean all 4 | RUN dnf -y install 'dnf-command(copr)' && dnf clean all 5 | RUN dnf -y copr enable jdornak/SoftwareCollections && dnf clean all 6 | # hackishly install dependencies without the actual package 7 | RUN dnf -y install softwarecollections && \ 8 | rpm -e softwarecollections && \ 9 | dnf clean all 10 | 11 | # installing pyyaml so we can load example data in yaml format 12 | RUN dnf -y install python3-PyYAML && dnf clean all 13 | 14 | WORKDIR /srv 15 | EXPOSE 8000 16 | RUN useradd test 17 | 18 | ADD run-devel /usr/bin/ 19 | CMD /usr/bin/run-devel 20 | 21 | ADD . /srv/softwarecollections 22 | RUN chown -R test:test /srv/softwarecollections 23 | USER test 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Jakub Dorňák, Red Hat, Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the {organization} nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.md 3 | recursive-include softwarecollections * 4 | recursive-exclude * *.pyc 5 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | url = "https://pypi.org/simple" 3 | verify_ssl = true 4 | name = "pypi" 5 | 6 | [packages] 7 | softwarecollections = {editable = true, path = "."} 8 | 9 | [dev-packages] 10 | pytest = "*" 11 | pytest-django = "*" 12 | pyyaml = "*" 13 | pytest-runner = "*" 14 | setuptools-scm = "*" 15 | 16 | [requires] 17 | python_version = "3.6" 18 | 19 | [scripts] 20 | manage = "python ./manage.py" 21 | "manage.py" = "python ./manage.py" 22 | warn-manage = "python -Wall::DeprecationWarning -Wignore:'__class__ not set' -Wignore:\"'U' mode\" -Wignore:'the imp module' ./manage.py" 23 | -------------------------------------------------------------------------------- /README.docker: -------------------------------------------------------------------------------- 1 | For development purposes, a Dockerfile is included. To build a Docker container, install and run Docker daemon first: 2 | 3 | dnf -y install docker 4 | systemctl start docker 5 | 6 | - make sure you're in the top level directory (the one that contains Dockerfile) 7 | - cd into this directory and build the image as `sclorgweb` (this will take some time): 8 | 9 | docker build -t sclorgweb . 10 | 11 | - after build is finished, run the resulting image: 12 | 13 | docker run --name scloorgwebtest -P -d -t -v $(pwd):/srv/softwarecollections:z sclorgweb 14 | 15 | - this will print a hash of new container 16 | - get IP address of the new container: 17 | 18 | docker inspect -format='{{.NetworkSettings.IPAddress}}' scloorgwebtest 19 | 20 | - attach to the image to see the output of testing server: 21 | 22 | docker attach scloorgwebtest 23 | 24 | - develop (code is mounted in container, so you can code locally) 25 | - when done developing, stop the container: 26 | 27 | docker stop 28 | 29 | - if you want to prune DB, just remove data/db.sqlite3 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | softwarecollections 2 | =================== 3 | 4 | Software Collections Management Website 5 | 6 | 7 | Installation 8 | ------------ 9 | 10 | Enable yum repository from copr: 11 | 12 | ``` 13 | sudo dnf copr enable jdornak/SoftwareCollections 14 | ``` 15 | 16 | Install package softwarecollections: 17 | 18 | ``` 19 | sudo dnf install softwarecollections 20 | ``` 21 | 22 | 23 | Configuration (production instance) 24 | ----------------------------------- 25 | 26 | Check the configuration in config files: 27 | 28 | ``` 29 | sudo vim /etc/softwarecollections/localsettings 30 | sudo vim /etc/httpd/conf.d/softwarecollections.conf 31 | ``` 32 | 33 | 34 | Development instance 35 | -------------------- 36 | 37 | Follow the **installation steps**. You do not need package 38 | *softwarecollections* itself, but you need all it's requirements. 39 | 40 | Clone the git repository: 41 | 42 | ``` 43 | git clone git@github.com:sclorg/softwarecollections.git 44 | cd softwarecollections 45 | ``` 46 | 47 | Clone the packaging-guide repository 48 | 49 | ``` 50 | git clone git@github.com:pmkovar/packaging-guide.git 51 | ``` 52 | 53 | Import packaging-guide 54 | 55 | ``` 56 | ./guide-build packaging-guide 57 | ./guide-import packaging-guide 58 | ``` 59 | 60 | Create local configuration: 61 | 62 | ``` 63 | cp softwarecollections/localsettings{-development,}.py 64 | ``` 65 | 66 | Create and set up the development environment: 67 | 68 | ``` 69 | pipenv install --dev 70 | ``` 71 | 72 | Initialize development database: 73 | 74 | ``` 75 | pipenv run ./manage.py migrate 76 | ``` 77 | 78 | Run development server: 79 | 80 | ``` 81 | pipenv run ./manage.py runserver 82 | ``` 83 | 84 | Voilà! 85 | 86 | No registration of user is required. 87 | You may simply [login](http://127.0.0.1:8000/login) if You have 88 | [FAS](https://admin.fedoraproject.org/accounts/) account. 89 | 90 | If You want to access the [admin site](http://127.0.0.1:8000/admin/), 91 | first make Yourself a superuser: 92 | 93 | ``` 94 | pipenv run ./manage.py makesuperuser $USER 95 | ``` 96 | 97 | To update your code and database to the last available version run: 98 | 99 | ``` 100 | git pull --rebase 101 | pipenv run ./manage.py migrate 102 | ``` 103 | 104 | Running tests 105 | ------------- 106 | 107 | Tests can be run either in the local development environment, 108 | or in pre-configured set of tests environments. 109 | 110 | In order to run the tests in the development environment, execute: 111 | 112 | ``` 113 | pipenv run pytest 114 | # or, equivalent 115 | pipenv run python setup.py test 116 | ``` 117 | 118 | In order to run the full test matrix, execute: 119 | 120 | ``` 121 | tox 122 | ``` 123 | 124 | RPM build 125 | --------- 126 | 127 | To build RPM you need **tito** package. To release the RPM in Copr, 128 | you need **copr-cli** package. 129 | 130 | ``` 131 | sudo dnf install tito copr-cli 132 | ``` 133 | 134 | To build RPM locally type: 135 | 136 | ``` 137 | tito build --rpm # builds RPM from the latest tagged release 138 | tito build --rpm --test # builds RPM from the latest commit 139 | ``` 140 | 141 | To build RPM in Copr type: 142 | 143 | ``` 144 | tito release copr # submits build from the latest tagged release 145 | tito release copr-test # submits build from the latest commit 146 | ``` 147 | 148 | 149 | Data migration from one server to another one 150 | --------------------------------------------- 151 | 152 | 1. Dump all data on the old system: 153 | 154 | ``` 155 | softwarecollections dumpdata > data.json 156 | ``` 157 | 158 | 2. Move data.json to the new system to location accessible by softwarecollections user. 159 | 160 | ``` 161 | rsync old:data.json /var/scls/data.json 162 | ``` 163 | 164 | 3. Delete automaticaly generated tables and load all data from json file: 165 | 166 | ``` 167 | echo "delete from auth_permission;" | softwarecollections dbshell 168 | echo "delete from django_content_type;" | softwarecollections dbshell 169 | softwarecollections loaddata /var/scls/data.json 170 | ``` 171 | 172 | 173 | Voilà! 174 | 175 | 176 | Help 177 | ---- 178 | 179 | If this is Your first time working with Django application, read through the 180 | [Django Tutorial](https://docs.djangoproject.com/en/1.9/intro/tutorial01/). 181 | 182 | For the detailed information about all aspect of using Django see the 183 | [Django Documentation](https://docs.djangoproject.com/en/1.9/). 184 | 185 | -------------------------------------------------------------------------------- /conf/bash-completion/softwarecollections: -------------------------------------------------------------------------------- 1 | # This bash script adds tab-completion feature to softwarecollections command. 2 | # It is only available for root, because other users can not see the secret key. 3 | 4 | if [ $(id -u) -eq 0 ]; then 5 | 6 | _django_completion() 7 | { 8 | COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \ 9 | COMP_CWORD=$COMP_CWORD \ 10 | DJANGO_AUTO_COMPLETE=1 $1 ) ) 11 | } 12 | complete -F _django_completion -o default softwarecollections 13 | 14 | fi 15 | 16 | -------------------------------------------------------------------------------- /conf/cron/softwarecollections: -------------------------------------------------------------------------------- 1 | ### Sync SCLs with copr repos every 10 minutes 2 | #*/10 * * * * root if [ ! -e /run/sclsync ]; then touch /run/sclsync; softwarecollections sclsync; softwarecollections sclrelated; rm /run/sclsync; fi 3 | 4 | ### This rebuilds error pages with the current year in the footer 5 | #0 0 1 1 * root /usr/bin/softwarecollections makeerrorpages 6 | 7 | ### Enable temporarily disabled cron entries 8 | #0 3 * * * root sed -i -r 's/^#([^#])/\1/' /etc/cron.d/softwarecollections 9 | -------------------------------------------------------------------------------- /conf/httpd/environment.override.conf: -------------------------------------------------------------------------------- 1 | # Run HTTPD with modified environment 2 | [Service] 3 | # softwarecollections require utf-8 support 4 | Environment=LC_ALL=C.utf-8 5 | Environment=LANG=C.utf-8 6 | # Load configuration from env file 7 | EnvironmentFile=/etc/sysconfig/softwarecollections.env 8 | 9 | # vim:set filetype=systemd: 10 | -------------------------------------------------------------------------------- /conf/httpd/softwarecollections.conf: -------------------------------------------------------------------------------- 1 | # vim: ft=apache 2 | 3 | 4 | 5 | ServerName www.softwarecollections.org 6 | ServerAdmin admin@softwarecollections.org 7 | 8 | DocumentRoot /var/scls/htdocs 9 | Alias /static /var/scls/htdocs/static 10 | Alias /media /var/scls/htdocs/media 11 | Alias /repos /var/scls/htdocs/repos 12 | WSGIScriptAlias / /var/scls/htdocs/wsgi.py 13 | ErrorDocument 400 /media/400.html 14 | ErrorDocument 403 /media/403.html 15 | ErrorDocument 404 /media/404.html 16 | ErrorDocument 500 /media/500.html 17 | 18 | WSGIDaemonProcess softwarecollections.org user=softwarecollections group=softwarecollections processes=5 threads=15 display-name=%{GROUP} 19 | WSGIProcessGroup softwarecollections.org 20 | 21 | AddIcon /static/images/rpm.png *.rpm 22 | 23 | AddOutputFilterByType DEFLATE "application/atom+xml" \ 24 | "application/javascript" \ 25 | "application/json" \ 26 | "application/ld+json" \ 27 | "application/manifest+json" \ 28 | "application/rss+xml" \ 29 | "application/vnd.geo+json" \ 30 | "application/vnd.ms-fontobject" \ 31 | "application/x-font-ttf" \ 32 | "application/x-web-app-manifest+json" \ 33 | "application/xhtml+xml" \ 34 | "application/xml" \ 35 | "font/opentype" \ 36 | "image/svg+xml" \ 37 | "image/x-icon" \ 38 | "text/cache-manifest" \ 39 | "text/css" \ 40 | "text/html" \ 41 | "text/plain" \ 42 | "text/vtt" \ 43 | "text/x-component" \ 44 | "text/xml" 45 | 46 | 47 | Order allow,deny 48 | Allow from all 49 | Require all granted 50 | 51 | 52 | # Allow directory indexes for each scl root 53 | 54 | Options +Indexes 55 | IndexStyleSheet /static/stylesheets/index.css 56 | IndexHeadInsert "\ 57 | \ 58 | \ 59 | \ 60 | \ 61 | \ 62 | \ 63 | \ 64 | \ 65 | \ 66 | " 67 | HeaderName /static/index-header.html 68 | ReadmeName /static/index-footer.html 69 | IndexOptions FancyIndexing 70 | IndexOptions FoldersFirst 71 | IndexOptions HTMLTable 72 | IndexOptions IconsAreLinks 73 | IndexOptions NameWidth=* 74 | IndexOptions SuppressDescription 75 | IndexOptions VersionSort 76 | 77 | 78 | ErrorLog logs/softwarecollections_error_log 79 | TransferLog logs/softwarecollections_access_log 80 | LogLevel warn 81 | 82 | # SSL Configuration 83 | SSLEngine on 84 | SSLProtocol all -SSLv3 85 | SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA 86 | SSLCertificateFile /etc/pki/tls/certs/softwarecollections.org.crt 87 | SSLCertificateKeyFile /etc/pki/tls/private/softwarecollections.org.key 88 | SSLCertificateChainFile /etc/pki/tls/certs/softwarecollections.org.CA.crt 89 | 90 | BrowserMatch "MSIE [2-5]" \ 91 | nokeepalive ssl-unclean-shutdown \ 92 | downgrade-1.0 force-response-1.0 93 | 94 | 95 | 96 | 97 | ServerName softwarecollections.org 98 | ServerAlias * 99 | ServerAdmin admin@softwarecollections.org 100 | Redirect / https://www.softwarecollections.org/ 101 | 102 | 103 | 104 | ServerName softwarecollections.org 105 | ServerAlias * 106 | ServerAdmin admin@softwarecollections.org 107 | Redirect / https://www.softwarecollections.org/ 108 | 109 | # SSL Configuration 110 | SSLEngine on 111 | SSLProtocol all -SSLv3 112 | SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA 113 | SSLCertificateFile /etc/pki/tls/certs/softwarecollections.org.crt 114 | SSLCertificateKeyFile /etc/pki/tls/private/softwarecollections.org.key 115 | SSLCertificateChainFile /etc/pki/tls/certs/softwarecollections.org.CA.crt 116 | 117 | BrowserMatch "MSIE [2-5]" \ 118 | nokeepalive ssl-unclean-shutdown \ 119 | downgrade-1.0 force-response-1.0 120 | 121 | 122 | -------------------------------------------------------------------------------- /conf/httpd/softwarecollections.env: -------------------------------------------------------------------------------- 1 | # Canonical production environment 2 | SCL_BASE_DIR=/var/scls 3 | 4 | SCL_DBDEBUG=false 5 | SCL_DEBUG=false 6 | SCL_SECRET_KEY_FILE=/var/scls/secret_key 7 | 8 | SCL_ADMINS="Jan Staněk " 9 | SCL_ALLOWED_HOSTS=www.softwarecollections.org 10 | 11 | SCL_CACHE_URL=memcached://127.0.0.1:11211 12 | SCL_DATABASE_URL=postgresql://softwarecollections@%2fvar%2fscls%2fdb:5432/softwarecollections 13 | -------------------------------------------------------------------------------- /conf/rsyncd/rsyncd.conf: -------------------------------------------------------------------------------- 1 | # /etc/rsyncd: configuration file for rsync daemon mode 2 | 3 | # See rsyncd.conf man page for more options. 4 | 5 | [repos] 6 | path = /var/scls/htdocs/repos 7 | comment = repos of softwarecollections.org 8 | 9 | uid = nobody 10 | gid = nobody 11 | use chroot = yes 12 | max connections = 4 13 | exclude = .* 14 | ignore nonreadable = yes 15 | dont compress = *.gz *.tgz *.zip *.z *.Z *.rpm *.deb *.bz2 16 | 17 | -------------------------------------------------------------------------------- /conf/rsyncd/softwarecollections-rsyncd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=rsync daemon for softwarecollections.org 3 | ConditionPathExists=/etc/softwarecollections/rsyncd.conf 4 | Conflicts=rsyncd.service 5 | 6 | [Service] 7 | ExecStart=/usr/bin/rsync --daemon --no-detach --config=/etc/softwarecollections/rsyncd.conf 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /guide-build: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "$1/publican.cfg" ]; then 4 | echo >&2 "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | # change dir to guide repo 9 | pushd "$1" > /dev/null 10 | 11 | # publican build 12 | publican build --formats=html-single --langs=en-US 13 | 14 | # change dir back 15 | popd > /dev/null 16 | 17 | -------------------------------------------------------------------------------- /guide-import: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "$1/publican.cfg" ]; then 4 | echo >&2 "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | rm -rf softwarecollections/static/guide 9 | mkdir softwarecollections/static/guide 10 | cp -a "$1/tmp/en-US/html-single"/{images,Common_Content} \ 11 | softwarecollections/static/guide/ 12 | 13 | sed -r \ 14 | -e 's@"(Common_Content|images)/@"/static/guide/\1/@g' \ 15 | "$1/tmp/en-US/html-single/index.html" \ 16 | | ./guide-templatize > softwarecollections/templates/pages/en/docs/guide.html 17 | 18 | -------------------------------------------------------------------------------- /guide-source: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ ! -f "$1/publican.cfg" ]; then 4 | echo >&2 "Usage: $0 " 5 | exit 1 6 | fi 7 | 8 | # change dir to guide repo 9 | pushd "$1" > /dev/null 10 | 11 | # get information about source 12 | NAME="packaging-guide" 13 | VERSION="$(egrep ^version: publican.cfg | sed -r 's/version:\s*//')" 14 | 15 | # create archive 16 | git archive --format=tar.gz --prefix=$NAME-$VERSION/ HEAD > ${NAME}-${VERSION}.tar.gz 17 | 18 | # change dir back 19 | popd > /dev/null 20 | 21 | # move source to current directory 22 | mv "$1/${NAME}-${VERSION}.tar.gz" ./ 23 | 24 | -------------------------------------------------------------------------------- /guide-templatize: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import io 4 | import sys 5 | 6 | # read utf-8 content 7 | content = io.TextIOWrapper(sys.stdin.buffer, encoding='utf-8').read() 8 | 9 | # cut header 10 | _, content = content.split('
', 1) 11 | 12 | # cut end 13 | content, _ = content.split('') 14 | 15 | # format output 16 | out = ''' 17 | {% extends "section_docs_guide.html" %}} 18 | 19 | {% block title %}Packaging Guide{% endblock %} 20 | 21 | {% block content %} 22 | 23 | 28 | 29 |

Packaging Guide

30 | 31 |
32 | The Packaging Guide provides an explanation of Software Collections and 33 | details how to build and package them. Developers and system administrators 34 | who have a basic understanding of software packaging with RPM packages, but 35 | who are new to the concept of Software Collections, can use this Guide to 36 | get started with Software Collections. 37 |
38 | 39 |
''' + content + ''' 40 | 41 | {% endblock %}''' 42 | 43 | # print output 44 | io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8').write(out) 45 | 46 | -------------------------------------------------------------------------------- /htdocs/media/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /htdocs/repos/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /htdocs/static/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /htdocs/wsgi.py: -------------------------------------------------------------------------------- 1 | """Link to the softwarecollections web app installed in system.""" 2 | 3 | from softwarecollections.wsgi import application # noqa: F401 4 | -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Manage softwarecollections instance. 3 | 4 | Can be installed and run as management binary for deployment. 5 | """ 6 | 7 | import grp 8 | import os 9 | import pwd 10 | import sys 11 | from collections import deque 12 | from functools import partial 13 | from itertools import filterfalse 14 | from operator import methodcaller 15 | from pathlib import Path 16 | from warnings import warn 17 | 18 | from django.core.management import execute_from_command_line 19 | 20 | #: Paths searched for environment file 21 | ENV_PATHS = [Path.cwd() / ".env", Path("/etc/sysconfig/softwarecollections.env")] 22 | #: Commands which are allowed to be run as root 23 | ALLOW_ROOT = {"collectstatic"} 24 | #: Minimal environment necessary for successful execution 25 | MINIMAL_ENVIRONMENT = { 26 | "DJANGO_SETTINGS_MODULE": "softwarecollections.settings", 27 | "LANG": "C.utf-8", 28 | "LC_CTYPE": "C.utf-8", 29 | } 30 | 31 | 32 | def switch_user(name="softwarecollections"): 33 | """Change the current user to the specified one. 34 | 35 | Keyword arguments: 36 | name: The name (login) of the user to switch to. 37 | Must exists on the system. 38 | """ 39 | 40 | user = pwd.getpwnam(name) 41 | groups = [g.gr_gid for g in grp.getgrall() if user.pw_name in g.gr_mem] 42 | 43 | # Execution context 44 | os.setgid(user.pw_gid) 45 | os.setgroups(groups) 46 | os.setuid(user.pw_uid) 47 | 48 | # Environment 49 | os.environ.update( 50 | USER=user.pw_name, LOGNAME=user.pw_name, HOME=user.pw_dir, SHELL=user.pw_shell 51 | ) 52 | 53 | 54 | def parse_env_file(file): 55 | """Parse environment file in similar way to SystemD unit file. 56 | 57 | Keyword arguments: 58 | file: The file-like object to parse. 59 | 60 | Returns: 61 | A dictionary with the parsed keys. 62 | """ 63 | 64 | def is_comment(line): 65 | return line.startswith(("#", ";")) or "=" not in line 66 | 67 | def concat_lines(line_iter): 68 | """Concatenate backslash-ending lines""" 69 | 70 | buffer = deque() 71 | 72 | for line in line_iter: 73 | if line.endswith("\\"): 74 | buffer.append(line[:-1]) 75 | else: 76 | buffer.append(line) 77 | yield "".join(buffer) 78 | buffer.clear() 79 | 80 | def strip(line): 81 | # First strip whitespace, then any double quotes 82 | return line.strip().strip('"') 83 | 84 | line_iter = map(methodcaller("rstrip", "\n"), file) 85 | line_iter = filter(None, line_iter) # empty lines 86 | line_iter = filterfalse(is_comment, line_iter) 87 | line_iter = concat_lines(line_iter) 88 | 89 | split_iter = map(methodcaller("partition", "="), line_iter) 90 | 91 | return {key: strip(value) for key, _sep, value in split_iter} 92 | 93 | 94 | def load_env_file(candidate_path_list=ENV_PATHS): 95 | """Load configuration from environment file. 96 | 97 | Keyword arguments: 98 | candidate_path_list: List of paths to try to read configuration from; 99 | first readable will be used. 100 | If none exists, a warning will be issued. 101 | """ 102 | 103 | for candidate in candidate_path_list: 104 | if os.access(candidate, os.R_OK): 105 | configuration = candidate 106 | break 107 | else: 108 | warn("No readable environment file found; using default configuration.") 109 | return 110 | 111 | with configuration.open(encoding="utf-8") as file: 112 | items = parse_env_file(file).items() 113 | encoded = map(partial(map, methodcaller("encode", "utf-8")), items) 114 | os.environb.update(encoded) 115 | 116 | 117 | if __name__ == "__main__": 118 | for envvar, value in MINIMAL_ENVIRONMENT.items(): 119 | os.environ.setdefault(envvar, value) 120 | 121 | subcommand = sys.argv[1] if len(sys.argv) >= 2 else None 122 | if os.getuid() == 0 and subcommand not in ALLOW_ROOT: 123 | switch_user() 124 | 125 | if Path(sys.argv[0]).name != "manage.py": 126 | load_env_file() 127 | 128 | execute_from_command_line(sys.argv) 129 | -------------------------------------------------------------------------------- /packaging-guide-1.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/packaging-guide-1.tar.gz -------------------------------------------------------------------------------- /rel-eng/lib/setup_builder.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | from shutil import copytree, rmtree 4 | from tempfile import TemporaryDirectory 5 | 6 | from tito.builder import Builder 7 | from tito.common import run_command, debug, find_spec_file 8 | 9 | 10 | class TemporaryWorktree(TemporaryDirectory): 11 | """Create temporary git checkout to different directory""" 12 | 13 | def __init__(self, *args, commit="HEAD", **kwargs): 14 | """Store identification of desired commit""" 15 | 16 | super().__init__(*args, **kwargs) 17 | 18 | self.commit = commit 19 | self.orig_dir = None 20 | 21 | create = "git worktree add --detach {path} {commit}".format( 22 | path=self.name, commit=self.commit 23 | ) 24 | run_command(create) 25 | 26 | def cleanup(self): 27 | """Remove refence to the worktree""" 28 | 29 | ret = super().cleanup() 30 | run_command("git worktree prune") 31 | 32 | return ret 33 | 34 | def __enter__(self): 35 | """Switch working dir to the worktree. 36 | 37 | Returns: Path to the worktree. 38 | """ 39 | 40 | path = super().__enter__() 41 | 42 | self.orig_dir = Path.cwd() 43 | os.chdir(path) 44 | 45 | return Path(path) 46 | 47 | def __exit__(self, *exc_info): 48 | """Switch working dir to the original one""" 49 | 50 | os.chdir(self.orig_dir) 51 | self.orig_dir = None 52 | 53 | return super().__exit__(*exc_info) 54 | 55 | 56 | class SetupBuilder(Builder): 57 | """Build tgz using setup.py sdist""" 58 | 59 | @staticmethod 60 | def _recreate_tgz(source, target): 61 | """Recreate sdist-archive in a way that is expected by tito. 62 | 63 | Keyword arguments: 64 | source: Path to source archive. 65 | target: Path to expected output archive. 66 | """ 67 | 68 | def strip_ext(name, ext=".tar.gz"): 69 | return name[: -len(ext)] 70 | 71 | names = { 72 | "source": source, 73 | "target": target, 74 | "source_root": strip_ext(source.name), 75 | "target_root": strip_ext(target.name), 76 | } 77 | 78 | # Extract source 79 | run_command("tar -xzf {source}".format_map(names)) 80 | 81 | # Rename root 82 | if names["source_root"] != names["target_root"]: 83 | run_command("mv {source_root} {target_root}".format_map(names)) 84 | 85 | # Compress converted archive 86 | run_command("tar -czf {target} {target_root}".format_map(names)) 87 | 88 | def _setup_sources(self): 89 | """Create .tar.gz for this package. 90 | 91 | Returns: absolute path to the archive. 92 | """ 93 | 94 | self._create_build_dirs() 95 | 96 | target_archive = Path(self.rpmbuild_sourcedir, self.tgz_filename) 97 | 98 | worktree = TemporaryWorktree(prefix="scl.org.build-", commit=self.git_commit_id) 99 | 100 | with worktree as workdir: 101 | debug("Copy source directory to {}".format(self.rpmbuild_gitcopy)) 102 | rmtree(self.rpmbuild_gitcopy) 103 | copytree(workdir, self.rpmbuild_gitcopy) 104 | 105 | debug("Make sdist archive") 106 | run_command("python3 ./setup.py sdist") 107 | 108 | source_archive, = workdir.joinpath("dist").glob("*.tar.gz") 109 | 110 | debug("Recreate sdist archive in {target}".format(target=target_archive)) 111 | self._recreate_tgz(source_archive, target_archive) 112 | 113 | self.spec_file_name = find_spec_file(self.rpmbuild_gitcopy) 114 | self.spec_file = Path(self.rpmbuild_gitcopy, self.spec_file_name) 115 | -------------------------------------------------------------------------------- /rel-eng/packages/.readme: -------------------------------------------------------------------------------- 1 | the rel-eng/packages directory contains metadata files 2 | named after their packages. Each file has the latest tagged 3 | version and the project's relative directory. 4 | -------------------------------------------------------------------------------- /rel-eng/packages/softwarecollections: -------------------------------------------------------------------------------- 1 | 0.19-1 ./ 2 | -------------------------------------------------------------------------------- /rel-eng/releasers.conf: -------------------------------------------------------------------------------- 1 | [copr] 2 | releaser = tito.release.CoprReleaser 3 | project_name = jdornak/SoftwareCollections 4 | 5 | [copr-test] 6 | releaser = tito.release.CoprReleaser 7 | builder.test = 1 8 | project_name = jdornak/SoftwareCollections 9 | 10 | -------------------------------------------------------------------------------- /rel-eng/tito.props: -------------------------------------------------------------------------------- 1 | [buildconfig] 2 | lib_dir = rel-eng/lib 3 | builder = setup_builder.SetupBuilder 4 | tagger = tito.tagger.VersionTagger 5 | changelog_do_not_remove_cherrypick = 0 6 | changelog_format = %s (%ae) 7 | tag_format = v{version} 8 | -------------------------------------------------------------------------------- /run-devel: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | cd /srv/softwarecollections 6 | cp softwarecollections/localsettings{-development,}.py 7 | test -e data/db.sqlite3 && dbexists=0 || dbexists=1 8 | ./manage.py migrate --noinput 9 | if [ $dbexists -ne 0 ]; then 10 | echo "from django.contrib.auth.models import User 11 | User.objects.filter(email='admin@example.com').delete() 12 | User.objects.create_superuser('admin@example.com', 'test', 'test')" | python3 manage.py shell 13 | # echo -e "test\ntest@test.com\ntest\ntest\n" | ./manage.py createsuperuser 14 | ./manage.py makesuperuser admin@example.com 15 | fi 16 | chmod a+w data/db.sqlite3 17 | 18 | # start memcached on background 19 | echo "Starting memcached ..." 20 | memcached & 21 | 22 | ./manage.py loaddata example-data.yaml 23 | 24 | # show nice IP that can be used in browser with some delay 25 | # and in separate process, because python runs in frontend 26 | ( 27 | set +x 28 | sleep 2 29 | echo "========================================================" 30 | echo "App started at http://$(hostname --ip-address):8000" 31 | echo "========================================================" 32 | ) & 33 | 34 | ./manage.py runserver 0.0.0.0:8000 35 | 36 | -------------------------------------------------------------------------------- /run-docker-test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | RESULT=1 6 | 7 | function cleanup() { 8 | docker stop scloorgwebtest 9 | docker rm scloorgwebtest 10 | [ $RESULT -eq 0 ] && echo "Test successfull." || echo "Test failed." 11 | exit $RESULT 12 | } 13 | trap cleanup EXIT SIGINT 14 | 15 | yum -y install docker 16 | systemctl start docker 17 | 18 | docker build -t sclorgweb . 19 | docker run --name scloorgwebtest -P -d -t sclorgweb 20 | webip=$(docker inspect --format='{{.NetworkSettings.IPAddress}}' scloorgwebtest) 21 | 22 | sleep 10 23 | 24 | curl http://$webip:8000/en/scls/hhorak/rpmquality/ | grep 'check-content-instructions' 25 | curl http://$webip:8000/en/scls/hhorak/rpmquality/ | grep 'check-content-description' 26 | curl http://$webip:8000/en/scls/hhorak/rpmquality/ | grep 'yum install centos-release-scl' 27 | 28 | RESULT=0 29 | 30 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | # Run pytest when invoking `setup.py test` 3 | test=pytest 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # encoding: utf-8 3 | 4 | import os 5 | 6 | from setuptools import find_packages, setup 7 | 8 | REPO_DIR = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | METADATA = { 11 | "name": "softwarecollections", 12 | "author": "Jakub Dorňák", 13 | "author_email": "jakub.dornak@misli.cz", 14 | "maintainer": "Jan Staněk", 15 | "maintainer_email": "jstanek@redhat.com", 16 | "url": "https://github.com/sclorg/softwarecollections", 17 | } 18 | 19 | # Basic/common package dependencies 20 | REQUIRES = [ 21 | "dj-database-url", 22 | "django~=2.2.10", 23 | "django-fas", 24 | "django-markdown2", 25 | "django-sekizai", 26 | "django-simple-captcha", 27 | "django-tagging", 28 | "flock", 29 | "gunicorn[eventlet]", 30 | "eventlet<0.30.3", # https://github.com/benoitc/gunicorn/pull/2581 31 | "psycopg2<2.9", # https://code.djangoproject.com/ticket/32856 32 | "py3dns", # pylibravatar missing dependency workaround 33 | "pylibravatar", 34 | "python3-memcached", 35 | "python3-openid", 36 | "requests", 37 | "whitenoise", 38 | ] 39 | 40 | # Extra dependencies for testing 41 | TEST_REQUIRES = ["pytest", "pytest-django", "pyyaml"] 42 | 43 | 44 | def rpm_compat_version(): 45 | """Constructs RPM-compatible version identifier""" 46 | 47 | def rpm_version_scheme(version): 48 | if version.exact: 49 | return version.format_with("{tag}") 50 | else: 51 | return version.format_with("{tag}.dev{distance}") 52 | 53 | return {"version_scheme": rpm_version_scheme} 54 | 55 | 56 | with open(os.path.join(REPO_DIR, "README.md"), encoding="utf-8") as readme: 57 | long_description = readme.read() 58 | 59 | setup( 60 | **METADATA, 61 | use_scm_version=rpm_compat_version, 62 | long_description=long_description, 63 | long_description_content_type="text/markdown", 64 | packages=find_packages(), 65 | include_package_data=True, 66 | python_requires=">=3", 67 | setup_requires=["pytest-runner", "setuptools_scm"], 68 | install_requires=REQUIRES, 69 | tests_require=TEST_REQUIRES, 70 | ) 71 | -------------------------------------------------------------------------------- /softwarecollections-db-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | NAME=softwarecollections 4 | DATADIR=/var/lib/pgsql/$NAME 5 | SOCKETDIR=/var/scls/db 6 | PORT=5432 7 | SOCKET=$SOCKETDIR/.s.PGSQL.$PORT 8 | UNIT="postgresql@${NAME}" 9 | SERVICE="${UNIT}.service" 10 | 11 | log() { 12 | if [ -t 1 ]; then 13 | echo -e "\033[1;36m$1\033[0m" 14 | else 15 | echo "$1" 16 | fi 17 | } 18 | 19 | if ! test -d /etc/systemd/system/$SERVICE.d; then 20 | log "Create systemd unit" 21 | postgresql-new-systemd-unit --unit "$UNIT" --datadir "$DATADIR" 22 | 23 | log "Create datadir" 24 | postgresql-setup --initdb --unit "$UNIT" --port $PORT 25 | 26 | log "Configure database" 27 | sed -r -i \ 28 | -e "s|^#* *listen_addresses.*|listen_addresses = ''|" \ 29 | -e "s|^#* *unix_socket_directories.*|unix_socket_directories = '$SOCKETDIR'|" \ 30 | $DATADIR/postgresql.conf 31 | sed -r -i 's/(local +all +all +).*/\1trust/' $DATADIR/pg_hba.conf 32 | fi 33 | 34 | if ! systemctl is-enabled -q $SERVICE; then 35 | log "Enable $SERVICE" 36 | systemctl enable $SERVICE 37 | fi 38 | 39 | if ! systemctl is-active -q $SERVICE; then 40 | log "Start $SERVICE" 41 | systemctl start $SERVICE || \ 42 | systemctl status $SERVICE 43 | fi 44 | 45 | if test -S $SOCKET && ! echo | su - softwarecollections -c "psql -h $SOCKETDIR -p $PORT -U softwarecollections" 2>/dev/null; then 46 | log "Create database user" 47 | su - postgres -c "createuser -h $SOCKETDIR -p $PORT softwarecollections" 48 | fi 49 | 50 | if test -S $SOCKET && ! echo | su - softwarecollections -c "psql -h $SOCKETDIR -p $PORT -d softwarecollections" 2>/dev/null; then 51 | log "Create database" 52 | su - postgres -c "createdb -h $SOCKETDIR -p $PORT softwarecollections" 53 | fi 54 | 55 | -------------------------------------------------------------------------------- /softwarecollections-services-setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | log() { 4 | if [ -t 1 ]; then 5 | echo -e "\033[1;36m$1\033[0m" 6 | else 7 | echo "$1" 8 | fi 9 | } 10 | 11 | for SERVICE in {memcached,httpd,sendmail,softwarecollections-rsyncd}.service; do 12 | if ! systemctl is-enabled -q $SERVICE; then 13 | log "Enable $SERVICE" 14 | systemctl enable $SERVICE 15 | fi 16 | if ! systemctl is-active -q $SERVICE; then 17 | log "Start $SERVICE" 18 | systemctl start $SERVICE 19 | fi 20 | done 21 | 22 | -------------------------------------------------------------------------------- /softwarecollections/__init__.py: -------------------------------------------------------------------------------- 1 | # empty 2 | -------------------------------------------------------------------------------- /softwarecollections/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class SclsConfig(AppConfig): 4 | name = 'softwarecollections' 5 | -------------------------------------------------------------------------------- /softwarecollections/auth/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | default_app_config = 'softwarecollections.auth.apps.SoftwarecollectionsAuthConfig' 3 | 4 | -------------------------------------------------------------------------------- /softwarecollections/auth/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class SoftwarecollectionsAuthConfig(AppConfig): 4 | name = 'softwarecollections.auth' 5 | label = 'softwarecollections.auth' 6 | 7 | -------------------------------------------------------------------------------- /softwarecollections/auth/backend.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.backends import ModelBackend 2 | 3 | class PerObjectModelBackend(ModelBackend): 4 | """ 5 | Authentication backend handling per object permissions 6 | """ 7 | 8 | def has_perm(self, user, perm, obj=None): 9 | if not user.is_active: 10 | return False 11 | if obj is None: 12 | return perm in self.get_all_permissions(user) 13 | else: 14 | return hasattr(obj, 'has_perm') and obj.has_perm(user, perm) 15 | 16 | -------------------------------------------------------------------------------- /softwarecollections/auth/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/auth/templatetags/__init__.py -------------------------------------------------------------------------------- /softwarecollections/auth/templatetags/auth.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | class AllowedNode(template.base.Node): 7 | 8 | def __init__(self, *perms, ifnodes=None, elsenodes=None, user=None, obj=None): 9 | self.perms = perms 10 | self.ifnodes = ifnodes 11 | self.elsenodes = elsenodes 12 | self.user = user 13 | self.obj = obj 14 | 15 | def __repr__(self): 16 | return "" 17 | 18 | def render(self, context): 19 | user = self.user.resolve(context) \ 20 | if self.user else context['request'].user 21 | obj = self.obj.resolve(context) \ 22 | if self.obj else None 23 | perms = list(map(lambda perm: perm.resolve(context) \ 24 | if isinstance(perm, template.Variable) else perm, self.perms)) 25 | if user.has_perms(perms, obj=obj): 26 | return self.ifnodes.render(context) 27 | elif self.elsenodes: 28 | return self.elsenodes.render(context) 29 | else: 30 | return '' 31 | 32 | 33 | @register.tag 34 | def allowed(parser, token): 35 | """ 36 | The ``{% allowed %}`` tag evaluates user permissions, and if the result is "true" 37 | (the user has appropriate permissions), the contents of the block are output:: 38 | 39 | {% allowed 'news.add' %} 40 | write 41 | {% notallowed %} 42 | You are not allowed to write news. 43 | {% endallowed %} 44 | 45 | ``allowed`` also accepts user=user argument to check permissions of given user:: 46 | 47 | {% allowed user=u 'news.add' %} 48 | User {{u.get_full_name()}} is allowed to write news. 49 | {% endallowed %} 50 | 51 | and obj=obj to check permissions regarding particular object:: 52 | 53 | {% allowed 'delete' obj=o %} 54 | delete 55 | {% endallowed %} 56 | """ 57 | # {% allowed ... %} 58 | args = [] 59 | kwargs = {} 60 | for arg in token.split_contents()[1:]: 61 | if arg[0] == arg[-1] and arg[0] in '\'"': 62 | args.append(arg[1:-1]) 63 | elif arg.startswith('user='): 64 | kwargs['user'] = template.Variable(arg[5:]) 65 | elif arg.startswith('obj='): 66 | kwargs['obj'] = template.Variable(arg[4:]) 67 | else: 68 | args.append(template.Variable(arg)) 69 | 70 | kwargs['ifnodes'] = parser.parse(('notallowed', 'endallowed')) 71 | token = parser.next_token() 72 | 73 | # {% notallowed %} (optional) 74 | if token.contents == 'notallowed': 75 | kwargs['elsenodes'] = parser.parse(('endallowed',)) 76 | token = parser.next_token() 77 | 78 | # {% endallowed %} 79 | assert token.contents == 'endallowed' 80 | 81 | return AllowedNode(*args, **kwargs) 82 | 83 | -------------------------------------------------------------------------------- /softwarecollections/copr.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | 4 | from django.conf import settings 5 | 6 | COPR_API_URL = getattr(settings, 'COPR_API_URL', 'http://copr-fe.cloud.fedoraproject.org/api') 7 | 8 | 9 | class CoprException(Exception): 10 | pass 11 | 12 | 13 | class CoprProxy: 14 | def __init__(self, copr_url=COPR_API_URL): 15 | self.copr_url = copr_url[-1] == '/' and copr_url[:-1] or copr_url 16 | 17 | def _get(self, path): 18 | response = requests.get(self.copr_url + path) 19 | if response.status_code != 200: 20 | response.raise_for_status() 21 | return json.loads(response.text) 22 | 23 | def coprnames(self, username): 24 | """ return list of copr names """ 25 | try: 26 | data = self._get('/coprs/{}/'.format(username)) 27 | return [copr['name'] for copr in data['repos']] 28 | except Exception as e: 29 | raise CoprException('Failed to get copr names: {}'.format(e)) 30 | 31 | def coprdetail(self, username, coprname): 32 | """ return copr details """ 33 | try: 34 | data = self._get('/coprs/{}/{}/detail/'.format(username, coprname)) 35 | data['detail']['username'] = username 36 | return data['detail'] 37 | except Exception as e: 38 | raise CoprException('Failed to get copr detail: {}'.format(e)) 39 | 40 | -------------------------------------------------------------------------------- /softwarecollections/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/management/__init__.py -------------------------------------------------------------------------------- /softwarecollections/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.utils.module_loading import import_string 3 | from django.core.management.base import BaseCommand 4 | 5 | logging_levels = {3: 'DEBUG', 2: 'INFO', 1: 'WARNING', 0: 'ERROR'} 6 | logging_formats = { 7 | 0: '%(module)s: %(message)s', 8 | 1: '%(levelname)s %(module)s: %(message)s', 9 | 2: '%(levelname)s %(module)s: %(message)s', 10 | 3: '%(asctime)s %(levelname)s %(module)s: %(message)s (pid: %(process)d/%(thread)d)', 11 | } 12 | 13 | class LoggingBaseCommand(BaseCommand): 14 | """ 15 | Django is known to use Python’s builtin logging module to perform system logging 16 | (see https://docs.djangoproject.com/en/1.6/topics/logging/), 17 | however no Django's management command use it. 18 | There is --verbosity (-v) option in Django BaseCommand, but it does not handle it. 19 | Django's commands use command.stdout.write instead of logging. 20 | LoggingBaseCommand configures Python’s builtin logging acording to the verbosity option. 21 | """ 22 | 23 | def configure_logging(self, verbosity): 24 | logging_config_func = import_string(settings.LOGGING_CONFIG) 25 | logging_config_func({ 26 | 'version': 1, 27 | 'disable_existing_loggers': False, 28 | 'formatters': { 29 | 'console': { 30 | 'format': logging_formats[verbosity] 31 | }, 32 | }, 33 | 'handlers': { 34 | 'console':{ 35 | 'level': 'DEBUG', 36 | 'class': 'logging.StreamHandler', 37 | 'formatter': 'console', 38 | 'stream': self.stderr 39 | }, 40 | 'mail_admins': { 41 | 'level': 'ERROR', 42 | 'class': 'django.utils.log.AdminEmailHandler', 43 | }, 44 | 45 | }, 46 | 'loggers': { 47 | '': { 48 | 'handlers': settings.DEBUG and ['console'] or ['console', 'mail_admins'], 49 | 'level': logging_levels[verbosity], 50 | 'propagate': True, 51 | } 52 | } 53 | }) 54 | 55 | -------------------------------------------------------------------------------- /softwarecollections/management/commands/makeerrorpages.py: -------------------------------------------------------------------------------- 1 | from pkg_resources import get_distribution, parse_version 2 | 3 | from django.conf import settings 4 | from django.core.management.base import BaseCommand, CommandError 5 | from django.template.loader import render_to_string 6 | from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError 7 | 8 | # Backward-compatible sekizai context 9 | if get_distribution("django-sekizai").parsed_version >= parse_version("0.10.0"): 10 | from sekizai.context import sekizai 11 | else: 12 | sekizai = dict 13 | 14 | 15 | class Command(BaseCommand): 16 | help = "Generate static error pages." 17 | 18 | def handle(self, *_args, **_options): 19 | context = dict(sekizai(), ADMINS=settings.ADMINS) 20 | 21 | for error_page in "400.html", "403.html", "404.html", "500.html": 22 | try: 23 | path = settings.MEDIA_ROOT / error_page 24 | content = render_to_string(error_page, context=context) 25 | path.write_text(content, encoding="utf-8") 26 | 27 | except (IOError, TemplateDoesNotExist, TemplateSyntaxError) as err: 28 | message = "Cannot generate {error_page}: {err!s}".format_map(locals()) 29 | raise CommandError(message) from err 30 | -------------------------------------------------------------------------------- /softwarecollections/management/commands/makesuperuser.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | import logging 3 | from typing import ClassVar 4 | 5 | from django.contrib.auth import get_user_model 6 | from django.core.management.base import BaseCommand, CommandError 7 | from django.db import DEFAULT_DB_ALIAS 8 | 9 | 10 | class Command(BaseCommand): 11 | help: ClassVar[str] = "Make the current or specified user a superuser." 12 | 13 | requires_system_checks: ClassVar[bool] = False 14 | 15 | log: ClassVar[logging.Logger] = logging.getLogger( 16 | "softwarecollections.management.makesuperuser" 17 | ) 18 | 19 | def add_arguments(self, parser): 20 | parser.add_argument( 21 | "--database", 22 | action="store", 23 | dest="database", 24 | default=DEFAULT_DB_ALIAS, 25 | help='Specifies the database to use. Default is "default".', 26 | ) 27 | parser.add_argument( 28 | "username", 29 | nargs="?", 30 | help="Name of the user to promote to superuser. Defaults to current user.", 31 | ) 32 | 33 | def handle(self, *args, **options): 34 | UserModel = get_user_model() 35 | username = options.get("username") or getpass.getuser() 36 | 37 | try: 38 | user = UserModel._default_manager.using(options.get("database")).get( 39 | **{UserModel.USERNAME_FIELD: username} 40 | ) 41 | except UserModel.DoesNotExist as err: 42 | raise CommandError("user '%s' does not exist" % username) from err 43 | 44 | self.log.info("Making user '%s' a superuser", user) 45 | user.is_staff = True 46 | user.is_superuser = True 47 | user.save() 48 | 49 | return "User '%s' successfully made a superuser" % user 50 | -------------------------------------------------------------------------------- /softwarecollections/management/commands/savekey.py: -------------------------------------------------------------------------------- 1 | """Save secret key to SCL_SECRET_KEY_FILE if set""" 2 | 3 | import logging 4 | import os 5 | from pathlib import Path 6 | 7 | from django.conf import settings 8 | from django.core.management.base import BaseCommand 9 | from django.core.management.utils import get_random_secret_key 10 | from django.utils.encoding import force_bytes 11 | 12 | ENVVAR = "SCL_SECRET_KEY_FILE" 13 | COMMAND = Path(__file__).stem 14 | 15 | 16 | class Command(BaseCommand): 17 | help = "Save secret key to {} if set".format(ENVVAR) 18 | log = logging.getLogger(__name__) 19 | 20 | def handle(self, *_args, **_options): 21 | candidate = os.getenv(ENVVAR, "") 22 | 23 | if not candidate: 24 | self.log.warning("%s not set, skipping", ENVVAR) 25 | return 26 | 27 | path = Path(candidate).resolve() 28 | if path.exists() and path.stat().st_size > 0: 29 | self.log.info("%s is not empty, keeping current content", path) 30 | return 31 | 32 | key = settings.SECRET_KEY or get_random_secret_key() 33 | path.write_bytes(force_bytes(key)) 34 | self.log.debug("Written %s into %s", key, path) 35 | -------------------------------------------------------------------------------- /softwarecollections/middleware/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/middleware/__init__.py -------------------------------------------------------------------------------- /softwarecollections/pages/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/pages/__init__.py -------------------------------------------------------------------------------- /softwarecollections/pages/views.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.http.response import Http404 3 | from django.shortcuts import render 4 | from django.template import TemplateDoesNotExist 5 | 6 | def page(request, path, template_dir=None): 7 | if template_dir: 8 | parts = [template_dir, request.LANGUAGE_CODE] 9 | else: 10 | parts = [request.LANGUAGE_CODE] 11 | if path: 12 | # add parts nice parts of path 13 | parts.extend([part for part in path.strip().split('/') if part not in ('','.','..')]) 14 | 15 | template_name = '/'.join(parts) + '.html' 16 | try: 17 | return render(request, template_name) 18 | except TemplateDoesNotExist as e: 19 | if settings.DEBUG: 20 | raise 21 | else: 22 | raise Http404 23 | -------------------------------------------------------------------------------- /softwarecollections/scls/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/__init__.py -------------------------------------------------------------------------------- /softwarecollections/scls/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.utils.translation import ungettext, ugettext as _ 3 | from .models import SoftwareCollection, OtherRepo, Repo, Score 4 | 5 | class SoftwareCollectionAdmin(admin.ModelAdmin): 6 | list_display = ('slug', 'get_title_tag', 'get_copr_tags', 'review_req', 'approved', 'auto_sync', 'need_sync', 'last_synced', 'last_modified') 7 | list_filter = ('review_req', 'approved', 'maintainer') 8 | ordering = ('slug',) 9 | actions = ('approve', 'request_sync') 10 | filter_horizontal = ('coprs', 'collaborators') 11 | 12 | def approve(self, request, queryset): 13 | rows_updated = queryset.update(review_req = False, approved = True) 14 | self.message_user(request, ungettext( 15 | '%(count)d selected collection was approved', 16 | '%(count)d selected collections were approved', 17 | rows_updated) % {'count': rows_updated} 18 | ) 19 | approve.short_description = _('Approve selected software collections') 20 | 21 | def request_sync(self, request, queryset): 22 | rows_updated = queryset.update(need_sync = True) 23 | self.message_user(request, ungettext( 24 | 'Sync was requested for %(count)d collection', 25 | 'Sync was requested for %(count)d collections', 26 | rows_updated) % {'count': rows_updated} 27 | ) 28 | request_sync.short_description = _('Request sync for selected software collections') 29 | 30 | admin.site.register(OtherRepo) 31 | admin.site.register(SoftwareCollection, SoftwareCollectionAdmin) 32 | admin.site.register(Repo) 33 | admin.site.register(Score) 34 | -------------------------------------------------------------------------------- /softwarecollections/scls/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | class SclsConfig(AppConfig): 4 | name = 'scls' 5 | -------------------------------------------------------------------------------- /softwarecollections/scls/checks.py: -------------------------------------------------------------------------------- 1 | """Liveness/readiness checks for supervisor""" 2 | 3 | from django.core.cache import cache, InvalidCacheBackendError 4 | from django.db import DatabaseError 5 | from django.http import JsonResponse 6 | from django.views.decorators.cache import never_cache 7 | 8 | from .models import SoftwareCollection 9 | 10 | SUCCESS = "OK" # sentinel value indicating successful query 11 | 12 | 13 | def database_check(model=SoftwareCollection): 14 | """Check that the database responds to queries. 15 | 16 | Arguments: 17 | model: The model/table to run the ping query against. 18 | 19 | Returns: 20 | SUCCESS if the database responds, error message in case of errors. 21 | """ 22 | 23 | try: 24 | model.objects.count() 25 | return SUCCESS 26 | except DatabaseError as err: 27 | return str(err) 28 | 29 | 30 | def cache_check(key="scls-ping-test", value=42): 31 | """Check that the cache can be interacted with. 32 | 33 | Arguments: 34 | key: The key to set in the cache. 35 | value: The value to set the key to. 36 | 37 | Returns: 38 | SUCCESS if the cache works, error message in case of errors. 39 | """ 40 | 41 | try: 42 | cache.set(key, value) 43 | cache.get(key) 44 | cache.delete(key) 45 | return SUCCESS 46 | except InvalidCacheBackendError as err: 47 | return str(err) 48 | 49 | 50 | @never_cache 51 | def report_liveness(_request): 52 | """The application is live and serving requests.""" 53 | 54 | return JsonResponse({"live": True}) 55 | 56 | 57 | @never_cache 58 | def report_readiness(_request): 59 | """The app is ready and has access to all resources.""" 60 | 61 | report = {"database": database_check(), "cache": cache_check()} 62 | response = JsonResponse(report) 63 | 64 | if any(status is not SUCCESS for status in report.values()): 65 | response.status_code = 503 # Service Unavailable 66 | response["Retry-After"] = 10 # seconds 67 | 68 | return response 69 | -------------------------------------------------------------------------------- /softwarecollections/scls/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/management/__init__.py -------------------------------------------------------------------------------- /softwarecollections/scls/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/management/commands/__init__.py -------------------------------------------------------------------------------- /softwarecollections/scls/management/commands/sclcreaterepo.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from optparse import make_option 5 | 6 | from django.core.management.base import CommandError 7 | 8 | from multiprocessing import Pool, cpu_count 9 | 10 | from softwarecollections.management.commands import LoggingBaseCommand 11 | from softwarecollections.scls.models import Repo 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def createrepo(args): 18 | repo, timeout = args 19 | 20 | # repo.createrepo() 21 | logger.info('Creating repo {}'.format(repo.slug)) 22 | try: 23 | repo.createrepo(timeout) 24 | except Exception as e: 25 | logger.error('Failed to create repo {}: {}'.format(repo.slug, e)) 26 | return 1 27 | 28 | return 0 29 | 30 | 31 | class Command(LoggingBaseCommand): 32 | option_list = LoggingBaseCommand.option_list + ( 33 | make_option( 34 | '-P', '--max-procs', action='store', dest='max_procs', default=cpu_count(), 35 | help='Run up to MAX_PROCS processes at a time (default {})'.format(cpu_count()) 36 | ), 37 | make_option( 38 | '-t', '--timeout', action='store', dest='timeout', default=None, 39 | help='Timeout in seconds for each run of createrepo', 40 | ), 41 | ) 42 | 43 | args = '[ ... ]' 44 | help = 'Rebuild metadata for all repos. ' \ 45 | 'Optionaly you may specify one or more slug of particular repo to be processed.' 46 | 47 | def handle(self, *args, **options): 48 | self.configure_logging(options['verbosity']) 49 | errors = 0 50 | if args: 51 | repos = [] 52 | for slug in args: 53 | try: 54 | repos.append(Repo.objects.get(slug=slug)) 55 | except Exception as e: 56 | logger.error(str(e)) 57 | errors += 1 58 | else: 59 | repos = Repo.objects.all() 60 | timeout = options['timeout'] and int(options['timeout']) 61 | with Pool(processes=int(options['max_procs'])) as pool: 62 | errors += sum(pool.map( 63 | createrepo, 64 | [(scl, timeout) for scl in repos], 65 | )) 66 | if errors > 0: 67 | raise CommandError('Failed to rebuild release RPMs: {} error(s)'.format(errors)) 68 | 69 | -------------------------------------------------------------------------------- /softwarecollections/scls/management/commands/sclotherrepos.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from optparse import make_option 5 | 6 | from django.core.management.base import CommandError 7 | 8 | from multiprocessing import Pool, cpu_count 9 | 10 | from softwarecollections.management.commands import LoggingBaseCommand 11 | from softwarecollections.scls.models import OtherRepo 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def centos_sync(args): 18 | repo, timeout = args 19 | logger.info('Syncing {}'.format(repo)) 20 | try: 21 | repo.sync(timeout) 22 | except Exception as e: 23 | logger.error('Failed to sync {}: {}'.format(repo, e)) 24 | return 1 25 | return 0 26 | 27 | 28 | class Command(LoggingBaseCommand): 29 | option_list = LoggingBaseCommand.option_list + ( 30 | make_option( 31 | '-P', '--max-procs', action='store', dest='max_procs', default=cpu_count(), 32 | help='Run up to MAX_PROCS processes at a time (default {})'.format(cpu_count()) 33 | ), 34 | make_option( 35 | '-t', '--timeout', action='store', dest='timeout', default=None, 36 | help='Timeout in seconds for each step of centos (repocentos, rpmbuild, createrepo, dump_provides)', 37 | ), 38 | ) 39 | 40 | help = 'Refresh other repos information.' 41 | 42 | def handle(self, *args, **options): 43 | self.configure_logging(options['verbosity']) 44 | timeout = options['timeout'] and int(options['timeout']) 45 | with Pool(processes=int(options['max_procs'])) as pool: 46 | errors = sum(pool.map( 47 | centos_sync, 48 | [(repo, timeout) for repo in OtherRepo.objects.all()], 49 | )) 50 | if errors > 0: 51 | raise CommandError('Failed to sync other repos: {} error(s)'.format(errors)) 52 | 53 | -------------------------------------------------------------------------------- /softwarecollections/scls/management/commands/sclprovides.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from optparse import make_option 5 | 6 | from django.core.management.base import CommandError 7 | 8 | from multiprocessing import Pool, cpu_count 9 | 10 | from softwarecollections.management.commands import LoggingBaseCommand 11 | from softwarecollections.scls.models import SoftwareCollection 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def dump_provides(args): 18 | scl, timeout = args 19 | 20 | # scl.dump_provides() 21 | logger.info('Dumping provides for {}'.format(scl.slug)) 22 | try: 23 | for repo in scl.all_repos: 24 | repo.dump_provides(timeout) 25 | scl.dump_provides(timeout) 26 | except Exception as e: 27 | logger.error('Failed to dump provides for {}: {}'.format(scl.slug, e)) 28 | return 1 29 | 30 | return 0 31 | 32 | 33 | class Command(LoggingBaseCommand): 34 | option_list = LoggingBaseCommand.option_list + ( 35 | make_option( 36 | '-P', '--max-procs', action='store', dest='max_procs', default=cpu_count(), 37 | help='Run up to MAX_PROCS processes at a time (default {})'.format(cpu_count()) 38 | ), 39 | make_option( 40 | '-t', '--timeout', action='store', dest='timeout', default=None, 41 | help='Timeout in seconds for each run of dump_provides', 42 | ), 43 | ) 44 | 45 | args = '[ ... ]' 46 | help = 'Dump provides for all collections. ' \ 47 | 'Optionaly you may specify one or more slug of particular SCLs to be dumped.' 48 | 49 | def handle(self, *args, **options): 50 | self.configure_logging(options['verbosity']) 51 | errors = 0 52 | if args: 53 | scls = [] 54 | for slug in args: 55 | try: 56 | scls.append(SoftwareCollection.objects.get(slug=slug)) 57 | except Exception as e: 58 | logger.error(str(e)) 59 | errors += 1 60 | else: 61 | scls = SoftwareCollection.objects.all() 62 | timeout = options['timeout'] and int(options['timeout']) 63 | with Pool(processes=int(options['max_procs'])) as pool: 64 | errors += sum(pool.map( 65 | dump_provides, 66 | [(scl, timeout) for scl in scls], 67 | )) 68 | if errors > 0: 69 | raise CommandError('Failed to dump provides: {} error(s)'.format(errors)) 70 | 71 | -------------------------------------------------------------------------------- /softwarecollections/scls/management/commands/sclrelated.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from optparse import make_option 5 | 6 | from django.core.management.base import CommandError 7 | 8 | from multiprocessing import Pool, cpu_count 9 | 10 | from softwarecollections.management.commands import LoggingBaseCommand 11 | from softwarecollections.scls.models import SoftwareCollection 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def find_related(args): 18 | scl, timeout = args 19 | 20 | # scl.find_related() 21 | logger.info('Searching relations for {}'.format(scl.slug)) 22 | try: 23 | scl.find_related(timeout) 24 | except Exception as e: 25 | logger.error('Failed to search relations for {}: {}'.format(scl.slug, e)) 26 | return 1 27 | 28 | return 0 29 | 30 | 31 | class Command(LoggingBaseCommand): 32 | option_list = LoggingBaseCommand.option_list + ( 33 | make_option( 34 | '-P', '--max-procs', action='store', dest='max_procs', default=cpu_count(), 35 | help='Run up to MAX_PROCS processes at a time (default {})'.format(cpu_count()) 36 | ), 37 | make_option( 38 | '-t', '--timeout', action='store', dest='timeout', default=None, 39 | help='Timeout in seconds for each run of find_related', 40 | ), 41 | ) 42 | 43 | args = '[ ... ]' 44 | help = 'Find related collections for all collections. ' \ 45 | 'Optionaly you may specify one or more slug of particular SCLs to be processed.' 46 | 47 | def handle(self, *args, **options): 48 | self.configure_logging(options['verbosity']) 49 | errors = 0 50 | if args: 51 | scls = [] 52 | for slug in args: 53 | try: 54 | scls.append(SoftwareCollection.objects.get(slug=slug)) 55 | except Exception as e: 56 | logger.error(str(e)) 57 | errors += 1 58 | else: 59 | scls = SoftwareCollection.objects.all() 60 | timeout = options['timeout'] and int(options['timeout']) 61 | with Pool(processes=int(options['max_procs'])) as pool: 62 | errors += sum(pool.map( 63 | find_related, 64 | [(scl, timeout) for scl in scls], 65 | )) 66 | if errors > 0: 67 | raise CommandError('Failed to find relations: {} error(s)'.format(errors)) 68 | 69 | -------------------------------------------------------------------------------- /softwarecollections/scls/management/commands/sclrpms.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from optparse import make_option 5 | 6 | from django.core.management.base import CommandError 7 | 8 | from multiprocessing import Pool, cpu_count 9 | 10 | from softwarecollections.management.commands import LoggingBaseCommand 11 | from softwarecollections.scls.models import Repo 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def rpmbuild(args): 18 | repo, timeout = args 19 | 20 | # repo.rpmbuild() 21 | logger.info('Building RPM for {}'.format(repo.slug)) 22 | try: 23 | repo.rpmbuild(timeout) 24 | except Exception as e: 25 | logger.error('Failed to build {}: {}'.format(repo.rpmname, e)) 26 | return 1 27 | 28 | # repo.createrepo() 29 | logger.info('Creating repo {}'.format(repo.slug)) 30 | try: 31 | repo.createrepo(timeout) 32 | except Exception as e: 33 | logger.error('Failed to create repo {}: {}'.format(repo.slug, e)) 34 | return 1 35 | 36 | return 0 37 | 38 | 39 | class Command(LoggingBaseCommand): 40 | option_list = LoggingBaseCommand.option_list + ( 41 | make_option( 42 | '-P', '--max-procs', action='store', dest='max_procs', default=cpu_count(), 43 | help='Run up to MAX_PROCS processes at a time (default {})'.format(cpu_count()) 44 | ), 45 | make_option( 46 | '-t', '--timeout', action='store', dest='timeout', default=None, 47 | help='Timeout in seconds for each step (rpmbuild, createrepo)', 48 | ), 49 | ) 50 | 51 | args = '[ ... ]' 52 | help = 'Rebuild all release RPMs. ' \ 53 | 'Optionaly you may specify one or more slug of particular repo to be processed.' 54 | 55 | def handle(self, *args, **options): 56 | self.configure_logging(options['verbosity']) 57 | errors = 0 58 | if args: 59 | repos = [] 60 | for slug in args: 61 | try: 62 | repos.append(Repo.objects.get(slug=slug)) 63 | except Exception as e: 64 | logger.error(str(e)) 65 | errors += 1 66 | else: 67 | repos = Repo.objects.all() 68 | timeout = options['timeout'] and int(options['timeout']) 69 | with Pool(processes=int(options['max_procs'])) as pool: 70 | errors += sum(pool.map( 71 | rpmbuild, 72 | [(scl, timeout) for scl in repos], 73 | )) 74 | if errors > 0: 75 | raise CommandError('Failed to rebuild release RPMs: {} error(s)'.format(errors)) 76 | 77 | -------------------------------------------------------------------------------- /softwarecollections/scls/management/commands/sclsync.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | from optparse import make_option 5 | 6 | from django.core.management.base import CommandError 7 | 8 | from multiprocessing import Pool, cpu_count 9 | 10 | from softwarecollections.management.commands import LoggingBaseCommand 11 | from softwarecollections.scls.models import SoftwareCollection 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def sync(args): 18 | scl, timeout = args 19 | exit_code = 0 20 | 21 | # scl.sync() 22 | logger.info('Syncing {}'.format(scl.slug)) 23 | try: 24 | scl.sync(timeout) 25 | if not scl.auto_sync: 26 | scl.need_sync = False 27 | scl.save() 28 | except Exception as e: 29 | logger.error('Failed to sync {}: {}'.format(scl.slug, e)) 30 | exit_code += 1 31 | 32 | # scl.dump_provides() 33 | logger.info('Dumping provides for {}'.format(scl.slug)) 34 | try: 35 | scl.dump_provides(timeout) 36 | except Exception as e: 37 | logger.error('Failed to dump provides for {}: {}'.format(scl.slug, e)) 38 | exit_code += 1 39 | 40 | return exit_code 41 | 42 | 43 | class Command(LoggingBaseCommand): 44 | option_list = LoggingBaseCommand.option_list + ( 45 | make_option( 46 | '-A', '--all', action='store_true', dest='all', default=False, 47 | help='Sync all collections, regardless the need_sync flag.', 48 | ), 49 | make_option( 50 | '-P', '--max-procs', action='store', dest='max_procs', default=cpu_count(), 51 | help='Run up to MAX_PROCS processes at a time (default {})'.format(cpu_count()) 52 | ), 53 | make_option( 54 | '-t', '--timeout', action='store', dest='timeout', default=None, 55 | help='Timeout in seconds for each step of sync (reposync, rpmbuild, createrepo, dump_provides)', 56 | ), 57 | ) 58 | 59 | args = '[ ... ]' 60 | help = 'Sync all SCLs (marked with need_sync flag) with Copr repos. '\ 61 | 'Optionaly you may specify one or more slug of particular SCLs to be synced.' 62 | 63 | def handle(self, *args, **options): 64 | self.configure_logging(options['verbosity']) 65 | errors = 0 66 | if args: 67 | scls = [] 68 | for slug in args: 69 | try: 70 | scls.append(SoftwareCollection.objects.get(slug=slug)) 71 | except Exception as e: 72 | logger.error(str(e)) 73 | errors += 1 74 | elif options['all']: 75 | scls = SoftwareCollection.objects.all() 76 | else: 77 | scls = SoftwareCollection.objects.filter(need_sync=True) 78 | timeout = options['timeout'] and int(options['timeout']) 79 | with Pool(processes=int(options['max_procs'])) as pool: 80 | errors += sum(pool.map( 81 | sync, 82 | [(scl, timeout) for scl in scls], 83 | )) 84 | if errors > 0: 85 | raise CommandError('Failed to sync SCLs: {} error(s)'.format(errors)) 86 | 87 | -------------------------------------------------------------------------------- /softwarecollections/scls/migrations/0002_centos_repos.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | def create_centos_repos(apps, schema_editor): 8 | 9 | CentOSRepo = apps.get_model('scls', 'CentOSRepo') 10 | 11 | for version in [6, 7]: 12 | for prefix in ['rh', 'sclo']: 13 | repo = CentOSRepo() 14 | repo.arch = 'x86_64' 15 | repo.version = version 16 | repo.prefix = prefix 17 | repo.release_package = 'centos-release-scl' 18 | if prefix == 'rh': 19 | repo.release_package += '-rh' 20 | repo.save() 21 | 22 | 23 | class Migration(migrations.Migration): 24 | 25 | dependencies = [ 26 | ('scls', '0001_initial'), 27 | ] 28 | 29 | operations = [ 30 | migrations.CreateModel( 31 | name='CentOSRepo', 32 | fields=[ 33 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)), 34 | ('version', models.CharField(verbose_name='CentOS version', max_length=20)), 35 | ('arch', models.CharField(verbose_name='Architecture', max_length=20)), 36 | ('prefix', models.CharField(verbose_name='Prefix', max_length=20)), 37 | ('release_package', models.CharField(verbose_name='Release package name', max_length=100)), 38 | ('last_modified', models.DateTimeField(verbose_name='Last modified', editable=False, null=True)), 39 | ('last_synced', models.DateTimeField(verbose_name='Last synced', editable=False, null=True)), 40 | ], 41 | options={ 42 | 'verbose_name': 'CentOS repo', 43 | 'ordering': ('version', 'arch', 'prefix'), 44 | 'verbose_name_plural': 'CentOS repos', 45 | }, 46 | ), 47 | migrations.AlterField( 48 | model_name='softwarecollection', 49 | name='coprs', 50 | field=models.ManyToManyField(to='scls.Copr', verbose_name='Copr projects', blank=True), 51 | ), 52 | migrations.AlterUniqueTogether( 53 | name='centosrepo', 54 | unique_together=set([('version', 'arch', 'prefix')]), 55 | ), 56 | migrations.AddField( 57 | model_name='softwarecollection', 58 | name='centos_repos', 59 | field=models.ManyToManyField(to='scls.CentOSRepo', verbose_name='CentOS repos', blank=True), 60 | ), 61 | migrations.RunPython(create_centos_repos), 62 | ] 63 | -------------------------------------------------------------------------------- /softwarecollections/scls/migrations/0003_other_repos.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | def migrate_centos_repos(apps, schema_editor): 8 | CentOSRepo = apps.get_model('scls', 'CentOSRepo') 9 | OtherRepo = apps.get_model('scls', 'OtherRepo') 10 | SoftwareCollection = apps.get_model('scls', 'SoftwareCollection') 11 | 12 | OTHER_REPOS_BY_CENTOS_REPO_ID = {} 13 | 14 | for centos_repo in CentOSRepo.objects.all(): 15 | other_repo = OtherRepo() 16 | other_repo.name = 'CentOS' 17 | other_repo.version = centos_repo.version 18 | other_repo.variant = centos_repo.prefix 19 | other_repo.arch = centos_repo.arch 20 | other_repo.icon = 'centos' 21 | other_repo.url = 'http://mirror.centos.org/centos/{version}/sclo/{arch}/{prefix}/'.format( 22 | version = centos_repo.version, 23 | arch = centos_repo.arch, 24 | prefix = centos_repo.prefix, 25 | ) 26 | other_repo.command = 'yum install {}'.format(centos_repo.release_package) 27 | other_repo.save() 28 | OTHER_REPOS_BY_CENTOS_REPO_ID[centos_repo.id] = other_repo 29 | 30 | for scl in SoftwareCollection.objects.all(): 31 | for centos_repo in scl.centos_repos.all(): 32 | scl.other_repos.add(OTHER_REPOS_BY_CENTOS_REPO_ID[centos_repo.id]) 33 | 34 | 35 | 36 | class Migration(migrations.Migration): 37 | 38 | dependencies = [ 39 | ('scls', '0002_centos_repos'), 40 | ] 41 | 42 | operations = [ 43 | migrations.CreateModel( 44 | name='OtherRepo', 45 | fields=[ 46 | ('id', models.AutoField(verbose_name='ID', serialize=False, primary_key=True, auto_created=True)), 47 | ('name', models.CharField(max_length=20, verbose_name='Distribution name')), 48 | ('version', models.CharField(max_length=20, verbose_name='Distribution version')), 49 | ('variant', models.CharField(max_length=20, verbose_name='Variant', blank=True, default='')), 50 | ('arch', models.CharField(max_length=20, verbose_name='Architecture')), 51 | ('icon', models.CharField(max_length=20, choices=[('centos', 'centos'), ('rhel', 'rhel'), ('fedora', 'fedora'), ('epel', 'epel')], verbose_name='Icon')), 52 | ('url', models.CharField(max_length=200, verbose_name='URL', blank=True, default='')), 53 | ('command', models.TextField(verbose_name='Command')), 54 | ('last_modified', models.DateTimeField(null=True, editable=False, verbose_name='Last modified')), 55 | ('last_synced', models.DateTimeField(null=True, editable=False, verbose_name='Last synced')), 56 | ], 57 | options={ 58 | 'ordering': ('name', 'version', 'variant', 'arch'), 59 | 'verbose_name': 'Other repo', 60 | 'verbose_name_plural': 'Other repos', 61 | }, 62 | ), 63 | migrations.AlterUniqueTogether( 64 | name='otherrepo', 65 | unique_together=set([('name', 'version', 'variant', 'arch')]), 66 | ), 67 | migrations.AddField( 68 | model_name='softwarecollection', 69 | name='other_repos', 70 | field=models.ManyToManyField(to='scls.OtherRepo', blank=True, verbose_name='Other repos'), 71 | ), 72 | migrations.RunPython(migrate_centos_repos), 73 | migrations.RemoveField( 74 | model_name='softwarecollection', 75 | name='centos_repos', 76 | ), 77 | migrations.DeleteModel( 78 | name='CentOSRepo', 79 | ), 80 | ] 81 | -------------------------------------------------------------------------------- /softwarecollections/scls/migrations/0004_other_repos_default_values.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('scls', '0003_other_repos'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AlterField( 15 | model_name='otherrepo', 16 | name='arch', 17 | field=models.CharField(default='', blank=True, verbose_name='Architecture', max_length=20), 18 | ), 19 | migrations.AlterField( 20 | model_name='otherrepo', 21 | name='command', 22 | field=models.TextField(default='', blank=True, verbose_name='Command'), 23 | ), 24 | migrations.AlterField( 25 | model_name='otherrepo', 26 | name='icon', 27 | field=models.CharField(default='', blank=True, verbose_name='Icon', choices=[('centos', 'centos'), ('epel', 'epel'), ('fedora', 'fedora'), ('rhel', 'rhel')], max_length=20), 28 | ), 29 | migrations.AlterField( 30 | model_name='otherrepo', 31 | name='version', 32 | field=models.CharField(default='', blank=True, verbose_name='Distribution version', max_length=20), 33 | ), 34 | ] 35 | -------------------------------------------------------------------------------- /softwarecollections/scls/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/migrations/__init__.py -------------------------------------------------------------------------------- /softwarecollections/scls/scl-release.spec: -------------------------------------------------------------------------------- 1 | Name: %{pkg_name} 2 | Version: %{pkg_version} 3 | Release: %{pkg_release} 4 | Summary: %{scl_title} Repository Configuration 5 | 6 | Group: System Environment/Base 7 | License: BSD 8 | URL: https://www.softwarecollections.org 9 | BuildArch: noarch 10 | 11 | %description 12 | %{scl_description} 13 | 14 | %prep 15 | echo "Nothing to prep" 16 | 17 | %build 18 | echo "Nothing to build" 19 | 20 | %install 21 | mkdir -p %{buildroot}%{_sysconfdir}/yum.repos.d 22 | cat > %{buildroot}%{_sysconfdir}/yum.repos.d/%{pkg_name}.repo < - 1-2 35 | - added gpgcheck=0 36 | 37 | * Tue Mar 11 2014 Jakub Dorňák - 1-1 38 | - Initial package 39 | -------------------------------------------------------------------------------- /softwarecollections/scls/static/scls/icons/centos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/static/scls/icons/centos.png -------------------------------------------------------------------------------- /softwarecollections/scls/static/scls/icons/empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/static/scls/icons/empty.png -------------------------------------------------------------------------------- /softwarecollections/scls/static/scls/icons/epel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/static/scls/icons/epel.png -------------------------------------------------------------------------------- /softwarecollections/scls/static/scls/icons/fedora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/static/scls/icons/fedora.png -------------------------------------------------------------------------------- /softwarecollections/scls/static/scls/icons/rhel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sclorg/softwarecollections/e4a400072e5ae2f388e1258999f1c2deabc002cd/softwarecollections/scls/static/scls/icons/rhel.png -------------------------------------------------------------------------------- /softwarecollections/scls/static/scls/javascripts/collection-add-edit.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | // we'll use a timer to load repo list whenever user stops typing 3 | // for 0.6 seconds (this number just feels right ;)) 4 | var timer; 5 | // remember last synced copr username, so that we don't refresh 6 | // unnecessarily, e.g. after a blur that follows keyup 7 | var last_synced_username; 8 | var last_sync_successful; 9 | 10 | $("#id_copr_username").on("keyup blur", function() { 11 | // coprnames_url has to be defined in template, since it's processed 12 | // on server-side 13 | if (timer) { 14 | clearTimeout(timer); 15 | } 16 | timer = setTimeout(function() { 17 | var curr_name = $("#id_copr_username").val() 18 | if (last_sync_successful && last_synced_username == curr_name) { 19 | return 20 | } else { 21 | last_sync_successful = false 22 | } 23 | var url = coprnames_url.replace("__copr_username__", curr_name); 24 | var $select = $("#id_copr_name"); 25 | $select.empty(); 26 | $.get(url, function(projects) { 27 | for (i in projects) { 28 | $select.append( 29 | $("
4 |
5 | 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 38 | -------------------------------------------------------------------------------- /softwarecollections/static/index-header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |
5 |
6 |
7 | 8 | Log in 9 | 10 | 11 |
12 |
13 |
14 |
15 |
16 |
17 | 31 |
32 |
33 |
34 |
35 | 43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 | 54 | 55 | 56 |
57 |
58 | 63 |