Merge branch 'master' of https://github.com/dae/anki
@ -1,7 +1,11 @@
|
||||
Please see the README file for basic requirements.
|
||||
|
||||
You also need to have the python-pyqt development packages installed
|
||||
(specifically, you need the binary pyuic4).
|
||||
In addition to the basic requirements, you also need the PyQt development
|
||||
tools (specifically pyrcc4 and pyuic4). These are often contained in a
|
||||
separate package on Linux, such as 'pyqt4-dev-tools' on Debian/Ubuntu. On a Mac
|
||||
they are part of the PyQt source install.
|
||||
|
||||
Windows users, please see the note at the bottom of this file before proceeding.
|
||||
|
||||
To use the development version:
|
||||
|
||||
@ -9,7 +13,11 @@ $ git clone https://github.com/dae/anki.git
|
||||
$ cd anki
|
||||
$ ./tools/build_ui.sh
|
||||
|
||||
Make sure you rebuild the UI every time you update the sources.
|
||||
If you get any errors, you will not be able to proceed, so please return to
|
||||
the top and check the requirements again.
|
||||
|
||||
ALL USERS: Make sure you rebuild the UI every time you git pull, otherwise you
|
||||
will get errors down the road.
|
||||
|
||||
The translations are stored in a bazaar repo for integration with Launchpad's
|
||||
translation services. If you want to use a language other than English:
|
||||
@ -31,3 +39,20 @@ Before contributing code, please read the LICENSE file.
|
||||
|
||||
If you'd like to contribute translations, please see the translations section
|
||||
of http://ankisrs.net/docs/manual.html#_contributing
|
||||
|
||||
WINDOWS USERS:
|
||||
|
||||
I have not tested the build scripts on Windows, so you'll need to solve any
|
||||
problems you encounter on your own. The easiest way is to use a source
|
||||
tarball instead of git, as that way you don't need to build the UI yourself.
|
||||
If you do want to use git, a user contributed the following, which should get
|
||||
you most of the way there:
|
||||
|
||||
1) Install "git bash".
|
||||
2) In the tools directory, modify build_ui.sh. Locate the line that reads
|
||||
"pyuic4 $i -o $py" and alter it to be of the following form:
|
||||
"<python-path-string>" "<pyuic-path-string>" $i -o $py
|
||||
These two paths must point to your python executable, and to pyuic.py, on your
|
||||
system. Typical paths would be:
|
||||
<python-path> = C:\\Python27\\python.exe
|
||||
<pyuic-path-string> = C:\\Python27\\Lib\\site-packages\\PyQt4\\uic\\pyuic.py
|
||||
|
@ -30,6 +30,6 @@ if arch[1] == "ELF":
|
||||
sys.path.insert(0, os.path.join(ext, "py2.%d-%s" % (
|
||||
sys.version_info[1], arch[0][0:2])))
|
||||
|
||||
version="2.0.19" # build scripts grep this line, so preserve formatting
|
||||
version="2.0.26" # build scripts grep this line, so preserve formatting
|
||||
from anki.storage import Collection
|
||||
__all__ = ["Collection"]
|
||||
|
@ -1,4 +1,142 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFFjCCA/6gAwIBAgIQRi3682cfg0pV4BcKiOriGTANBgkqhkiG9w0BAQUFADBy
|
||||
MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD
|
||||
VQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEYMBYGA1UE
|
||||
AxMPRXNzZW50aWFsU1NMIENBMB4XDTE0MDQxMDAwMDAwMFoXDTE3MDQwOTIzNTk1
|
||||
OVowWzEhMB8GA1UECxMYRG9tYWluIENvbnRyb2wgVmFsaWRhdGVkMR4wHAYDVQQL
|
||||
ExVFc3NlbnRpYWxTU0wgV2lsZGNhcmQxFjAUBgNVBAMUDSouYW5raXdlYi5uZXQw
|
||||
ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC+A1hdxZ8NNorbzRF9qutY
|
||||
7DNuKQdXRjMuRszAeAHI8Ln6+zfvMvLPvCHrZ9aDNTpKhAQBuE7NrFkR9oR0xHT8
|
||||
JtgVQKJ5zvVFfZmNoHyWYW0+dj8ay1Y/74V/I4Xhb43Fk4Q7UBgAl0kwm5vDWC6U
|
||||
HsA9/KPfEbWCOLSVoA1sU60viggnfbIl0XmNkkt355KvUdJgT5LisOfi8KvH58RN
|
||||
X8RHDsLI+Kv3rc11hLxwJ171NC0bPvjOIvQ5NKlNeDFZASx00kaGE3H26r1it2Gr
|
||||
hrR8IlTSV967JDpYi1FO+Aom54/OZ6ozE/JNsbKlcwmdH2CO2aMizFZfoQmYEsyr
|
||||
AgMBAAGjggG9MIIBuTAfBgNVHSMEGDAWgBTay+qtWwhdzP/8JlTOSeVVxjj0+DAd
|
||||
BgNVHQ4EFgQU8P8DDEM/4k6bCQr8Z9BeoGw5ANgwDgYDVR0PAQH/BAQDAgWgMAwG
|
||||
A1UdEwEB/wQCMAAwNAYDVR0lBC0wKwYIKwYBBQUHAwEGCCsGAQUFBwMCBgorBgEE
|
||||
AYI3CgMDBglghkgBhvhCBAEwTwYDVR0gBEgwRjA6BgsrBgEEAbIxAQICBzArMCkG
|
||||
CCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21vZG8uY29tL0NQUzAIBgZngQwB
|
||||
AgEwOwYDVR0fBDQwMjAwoC6gLIYqaHR0cDovL2NybC5jb21vZG9jYS5jb20vRXNz
|
||||
ZW50aWFsU1NMQ0EuY3JsMG4GCCsGAQUFBwEBBGIwYDA4BggrBgEFBQcwAoYsaHR0
|
||||
cDovL2NydC5jb21vZG9jYS5jb20vRXNzZW50aWFsU1NMQ0FfMi5jcnQwJAYIKwYB
|
||||
BQUHMAGGGGh0dHA6Ly9vY3NwLmNvbW9kb2NhLmNvbTAlBgNVHREEHjAcgg0qLmFu
|
||||
a2l3ZWIubmV0ggthbmtpd2ViLm5ldDANBgkqhkiG9w0BAQUFAAOCAQEAR2JPdym8
|
||||
MsSqmi2EyB4zR/UZH7XQUdIwx/1NhKf1XTN9akTNsS2y6Vp+TvaRVknEm7Z1i8CU
|
||||
xiSZicsUOUr8MCzDVtTl3KUuYNUdsv0yXwTvGc01xJ26ix+KTmmQVKBq86gYGzXI
|
||||
pLG0mfG1UAdZc6MxhcPXDROppvGXWk2vb6xYryVQiqV45SCiX2a4PtIf8zqO62eD
|
||||
Xqazh3fpJ685rBnvWvpi8teYlYbJpJoaFEHa5ime2VixEYPVnfY2DBbiLQVImCgF
|
||||
4NLwbCh79uD90CiGXHZbVUCq8fNf2gYzhK08erbDWhK39DzkBcE1XlbsPYzN9fUL
|
||||
E1Re6AD7oBwALQ==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFAzCCA+ugAwIBAgIQGLLLuqME8aAPwfLzJkYqSjANBgkqhkiG9w0BAQUFADCB
|
||||
gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV
|
||||
BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw
|
||||
MDBaFw0xOTEyMzEyMzU5NTlaMHIxCzAJBgNVBAYTAkdCMRswGQYDVQQIExJHcmVh
|
||||
dGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoTEUNPTU9E
|
||||
TyBDQSBMaW1pdGVkMRgwFgYDVQQDEw9Fc3NlbnRpYWxTU0wgQ0EwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt8AiwcsargxIxF3CJhakgEtSYau2A1NHf
|
||||
5I5ZLdOWIY120j8YC0YZYwvHIPPlC92AGvFaoL0dds23Izp0XmEbdaqb1IX04XiR
|
||||
0y3hr/yYLgbSeT1awB8hLRyuIVPGOqchfr7tZ291HRqfalsGs2rjsQuqag7nbWzD
|
||||
ypWMN84hHzWQfdvaGlyoiBSyD8gSIF/F03/o4Tjg27z5H6Gq1huQByH6RSRQXScq
|
||||
oChBRVt9vKCiL6qbfltTxfEFFld+Edc7tNkBdtzffRDPUanlOPJ7FAB1WfnwWdsX
|
||||
Pvev5gItpHnBXaIcw5rIp6gLSApqLn8tl2X2xQScRMiZln5+pN0vAgMBAAGjggGD
|
||||
MIIBfzAfBgNVHSMEGDAWgBQLWOWLxkwVN6RAqTCpIb5HNlpW/zAdBgNVHQ4EFgQU
|
||||
2svqrVsIXcz//CZUzknlVcY49PgwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQI
|
||||
MAYBAf8CAQAwIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMD4GA1Ud
|
||||
IAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwczovL3NlY3VyZS5jb21v
|
||||
ZG8uY29tL0NQUzBJBgNVHR8EQjBAMD6gPKA6hjhodHRwOi8vY3JsLmNvbW9kb2Nh
|
||||
LmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9uQXV0aG9yaXR5LmNybDBsBggrBgEFBQcB
|
||||
AQRgMF4wNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuY29tb2RvY2EuY29tL0NvbW9k
|
||||
b1VUTlNHQ0NBLmNydDAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2Eu
|
||||
Y29tMA0GCSqGSIb3DQEBBQUAA4IBAQAtlzR6QDLqcJcvgTtLeRJ3rvuq1xqo2l/z
|
||||
odueTZbLN3qo6u6bldudu+Ennv1F7Q5Slqz0J790qpL0pcRDAB8OtXj5isWMcL2a
|
||||
ejGjKdBZa0wztSz4iw+SY1dWrCRnilsvKcKxudokxeRiDn55w/65g+onO7wdQ7Vu
|
||||
F6r7yJiIatnyfKH2cboZT7g440LX8NqxwCPf3dfxp+0Jj1agq8MLy6SSgIGSH6lv
|
||||
+Wwz3D5XxqfyH8wqfOQsTEZf6/Nh9yvENZ+NWPU6g0QO2JOsTGvMd/QDzczc4BxL
|
||||
XSXaPV7Od4rhPsbXlM1wSTz/Dr0ISKvlUhQVnQ6cGodWaK2cCQBk
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEqzCCA5OgAwIBAgIQLnmDLpCIh+qLjvMabuZ6RDANBgkqhkiG9w0BAQUFADCB
|
||||
kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
|
||||
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
|
||||
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw
|
||||
IFNHQzAeFw0wNjEyMDEwMDAwMDBaFw0yMDA1MzAxMDQ4MzhaMIGBMQswCQYDVQQG
|
||||
EwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxm
|
||||
b3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RP
|
||||
IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
|
||||
MIIBCgKCAQEA0ECLi3LjkRv3UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZ
|
||||
rts7oVewdFFxze1CkU1B/qnI2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAh
|
||||
TaHYujl8HJ6jJJ3ygxaYqhZ8Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23Iw
|
||||
ambV4EajcNxo2f8ESIl33rXp+2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVD
|
||||
iOEjPqXSJDlqR6sA1KGzqSX+DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ
|
||||
0o7KBWFxB3NH5YoZEr0ETc5OnKVIrLsm9wIDAQABo4IBCTCCAQUwHwYDVR0jBBgw
|
||||
FoAUUzLRs89/+uDxoF2FTpLSnkUdtE8wHQYDVR0OBBYEFAtY5YvGTBU3pECpMKkh
|
||||
vkc2Wlb/MA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MCAGA1UdJQQZ
|
||||
MBcGCisGAQQBgjcKAwMGCWCGSAGG+EIEATARBgNVHSAECjAIMAYGBFUdIAAwbQYD
|
||||
VR0fBGYwZDAxoC+gLYYraHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLURBVEFD
|
||||
b3JwU0dDLmNybDAvoC2gK4YpaHR0cDovL2NybC5jb21vZG8ubmV0L1VUTi1EQVRB
|
||||
Q29ycFNHQy5jcmwwDQYJKoZIhvcNAQEFBQADggEBANheksSuFNxDrcKkw2dFBx35
|
||||
N6IZxxw3NZETHAfEfUKmDvCGXENrDkTPviRhOkKpzp1Mr3k5cN0OBCBOlZw83rdg
|
||||
umNDQO1qD4FJRrsek8BL8/jhNkkbb7YMDfKQV4r8bZPyKMf6hgoosxcOWYoutr/N
|
||||
4axMZmzyVZFWtzK/seR9teg6ti/bspzaUJOOTsWsmn5cnhI8O03GUHCzZSuO92uh
|
||||
uyXAALv17BZlgQ771KMhlneaqHS8U6rCOVD/CwIJYcyVt9eIavZcxWjTFJUaR1/Z
|
||||
+y3kL48ThqsxE0ATrG7ttRAwixtQqc7ujMrrfLW5Fj3U+m+SbR6ivfsCSsVwvvE=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEpjCCA46gAwIBAgIQRurwlgVMxeP6Zepun0LGZDANBgkqhkiG9w0BAQUFADBv
|
||||
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
|
||||
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
|
||||
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
|
||||
gZMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
|
||||
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
|
||||
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMRswGQYDVQQDExJVVE4gLSBEQVRBQ29y
|
||||
cCBTR0MwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDf7lgQoituVcSO
|
||||
vy5GCefgCA8uK3oTlBu99raAjmUFkwAevK/iD44ZDRJH7Kyto/oucPjebvtWQhWe
|
||||
LlzvI94huQV2JxkPT9bDnLS+lBlj8qYRCutTSJy+8ik7FugaoEymyfQYWWjAcPJT
|
||||
AMBeUIKlVm82+UrgRIagTU7WR25JSstn16bEBbmOHvT8/83nNuCcBWyyMyIV0LTg
|
||||
zBfAssD0/jI/KSqVe9jyp04PVHyhDYCzCQPB/1zdXpo+vK68R4pqrnHKH7EquF9C
|
||||
BQvsRjDRcgvK6VZt9e/feL5hurKlrgRMvKisaRWXve/rtIy/NfjUw9EoDlw6n3AY
|
||||
MyB3xKKvAgMBAAGjggEXMIIBEzAfBgNVHSMEGDAWgBStvZh6NLQm9/rEJlTvA73g
|
||||
JMtUGjAdBgNVHQ4EFgQUUzLRs89/+uDxoF2FTpLSnkUdtE8wDgYDVR0PAQH/BAQD
|
||||
AgEGMA8GA1UdEwEB/wQFMAMBAf8wIAYDVR0lBBkwFwYKKwYBBAGCNwoDAwYJYIZI
|
||||
AYb4QgQBMBEGA1UdIAQKMAgwBgYEVR0gADB7BgNVHR8EdDByMDigNqA0hjJodHRw
|
||||
Oi8vY3JsLmNvbW9kb2NhLmNvbS9BZGRUcnVzdEV4dGVybmFsQ0FSb290LmNybDA2
|
||||
oDSgMoYwaHR0cDovL2NybC5jb21vZG8ubmV0L0FkZFRydXN0RXh0ZXJuYWxDQVJv
|
||||
b3QuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQBjhpIQsRP6N76OKrYbikP1XK4OFN/3
|
||||
aUB/vxpxAAnYv9QkSr/gk/8B2AvGD+x+R5ywXfd8FJ38wDOShFvSg/RS4iJYdPxD
|
||||
Gz+no1jaA/288Drk7cwSu8m5rnsEoARyv+neLdKnUWYAc9K9fqqeU5Z9abIYPo6t
|
||||
VlB+99Ww/zliZYKMllfDj/dg9sKNNIf8T0Pl278cqvaGzebfET+NB/dtgxPAOIg5
|
||||
YKF+MOHjiD6ku2NvLOmKaCzulmmsBGHhT04OnXJM9nk4yMdIaW+UD3S0vMjPV025
|
||||
dXGWDYoGC+vd0PA8fcYumEZqOMcCtci4smV13tqQCLZ3uFMAJctHynNf
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU
|
||||
MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs
|
||||
IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290
|
||||
MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux
|
||||
FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h
|
||||
bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v
|
||||
dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt
|
||||
H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9
|
||||
uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX
|
||||
mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX
|
||||
a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN
|
||||
E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0
|
||||
WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD
|
||||
VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0
|
||||
Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU
|
||||
cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx
|
||||
IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN
|
||||
AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH
|
||||
YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5
|
||||
6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC
|
||||
Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
|
||||
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
||||
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFFzCCA/+gAwIBAgIRAP+ceCiXnKf8x8mBIBmTckswDQYJKoZIhvcNAQEFBQAw
|
||||
cTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G
|
||||
A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ29tb2RvIENBIExpbWl0ZWQxFzAVBgNV
|
||||
@ -53,3 +191,59 @@ Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX
|
||||
c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a
|
||||
mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ=
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEhjCCA26gAwIBAgIQUkIGSk83/kNpSHqWZ/9dJzANBgkqhkiG9w0BAQUFADBv
|
||||
MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk
|
||||
ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF
|
||||
eHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEwNDgzOFow
|
||||
gZcxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2FsdCBMYWtl
|
||||
IENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8GA1UECxMY
|
||||
aHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR8wHQYDVQQDExZVVE4tVVNFUkZpcnN0
|
||||
LUhhcmR3YXJlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsffDOD+0
|
||||
qH/POYJRZ9Btn9L/WPPnnyvsDYlUmbk4mRb34CF5SMK7YXQSlh08anLVPBBnOjnt
|
||||
KxPNZuuVCTOkbJex6MbswXV5nEZejavQav25KlUXEFSzGfCa9vGxXbanbfvgcRdr
|
||||
ooj7AN/+GjF3DJoBerEy4ysBBzhuw6VeI7xFm3tQwckwj9vlK3rTW/szQB6g1ZgX
|
||||
vIuHw4nTXaCOsqqq9o5piAbF+okh8widaS4JM5spDUYPjMxJNLBpUb35Bs1orWZM
|
||||
vD6sYb0KiA7I3z3ufARMnQpea5HW7sftKI2rTYeJc9BupNAeFosU4XZEA39jrOTN
|
||||
SZzFkvSrMqFIWwIDAQABo4H0MIHxMB8GA1UdIwQYMBaAFK29mHo0tCb3+sQmVO8D
|
||||
veAky1QaMB0GA1UdDgQWBBShcl8mGyiYQ5VdBzfVhZadS9LDRTAOBgNVHQ8BAf8E
|
||||
BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zARBgNVHSAECjAIMAYGBFUdIAAwewYDVR0f
|
||||
BHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20vQWRkVHJ1c3RFeHRl
|
||||
cm5hbENBUm9vdC5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9BZGRU
|
||||
cnVzdEV4dGVybmFsQ0FSb290LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAYGQ5WaJD
|
||||
ZS79+R/WrjO76FMTxIjuIxpszthkWVNTkOg239T88055L9XmjwzvKkFtcb2beDgj
|
||||
03BLhgz9EqciYhLYzOBR7y3lzQxFoura7X7s9zKa5wU1Xm7CLGhonf+M8cpVh8Qv
|
||||
sUAG3IQiXG2zzdGbGgozKGYWDL0zwvYH8eOheZTg+NDQ099Shj+p4ckdPoaEsdtf
|
||||
7uRJQ8E5fc8vlqd1XX5nZ4TlWSBAvzcivwdDtDDhQ4rNA11tuSnZhKf1YmOEhtY3
|
||||
vm9nu/9iVzmdDE2yKmE9HZzvmncgoC/uGnKdsJ2/eBMnBwpgEZP1Dy7J72skg/6b
|
||||
kLRLaIHQwvrgPw==
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFAzCCA+ugAwIBAgIQTM1KmltFEyGMz5AviytRcTANBgkqhkiG9w0BAQUFADCB
|
||||
lzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug
|
||||
Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho
|
||||
dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xHzAdBgNVBAMTFlVUTi1VU0VSRmlyc3Qt
|
||||
SGFyZHdhcmUwHhcNMDYwOTE4MDAwMDAwWhcNMjAwNTMwMTA0ODM4WjBxMQswCQYD
|
||||
VQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdT
|
||||
YWxmb3JkMRowGAYDVQQKExFDb21vZG8gQ0EgTGltaXRlZDEXMBUGA1UEAxMOUG9z
|
||||
aXRpdmVTU0wgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9T3lY
|
||||
IpPJKD5SEQAvwKkgitctVR4Q57h/4oYqpOxe6eSSWJZUDfMXukGeFZFV78LuACAY
|
||||
RYMm3yDMPbOhEzEKIVx5g3mrJBVcVvC0lZih2tIb6ha1y7ewwVP5pEba8C4kuGKe
|
||||
joteK1qWoOpQ6Yj7KCpNmpxIT4O2h65Pxci12f2+P9GnncYsEw3AAcezcPOPabuw
|
||||
PBDf6wkAhD9u7/zjLbTHXRHM9/Lx9uLjAH4SDt6NfQDKOj32cuh5JaYIFveriP9W
|
||||
XgkXwFqCBWI0KyhIMpfQhAysExjbnmbHqhSLEWlN8QnTul2piDdi2L8Dm53X5gV+
|
||||
wmpSqo0HgOqODvMdAgMBAAGjggFuMIIBajAfBgNVHSMEGDAWgBShcl8mGyiYQ5Vd
|
||||
BzfVhZadS9LDRTAdBgNVHQ4EFgQUuMoR6QYxedvDlMboGSq8uzUWMaQwDgYDVR0P
|
||||
AQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwewYDVR0fBHQwcjA4oDagNIYy
|
||||
aHR0cDovL2NybC5jb21vZG9jYS5jb20vVVROLVVTRVJGaXJzdC1IYXJkd2FyZS5j
|
||||
cmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29tb2RvLm5ldC9VVE4tVVNFUkZpcnN0LUhh
|
||||
cmR3YXJlLmNybDCBhgYIKwYBBQUHAQEEejB4MDsGCCsGAQUFBzAChi9odHRwOi8v
|
||||
Y3J0LmNvbW9kb2NhLmNvbS9VVE5BZGRUcnVzdFNlcnZlckNBLmNydDA5BggrBgEF
|
||||
BQcwAoYtaHR0cDovL2NydC5jb21vZG8ubmV0L1VUTkFkZFRydXN0U2VydmVyQ0Eu
|
||||
Y3J0MA0GCSqGSIb3DQEBBQUAA4IBAQAdtOf5GEhd7fpawx3jt++GFclsE0kWDTGM
|
||||
MVzn2odkjq8SFqRaLZIaOz4hZaoXw5V+QBz9FGkGGM2sMexq8RaeiSY9WyGN6Oj5
|
||||
qz2qPMuZ8oZfiFMVBRflqNKFp05Jfdbdx4/OiL9lBeAUtTF37r0qhujop2ot2mUZ
|
||||
jGfibfZKhWaDtjJNn0IjF9dFQWp2BNStuY9u3MI+6VHyntjzf/tQKvCL/W8NIjYu
|
||||
zg5G8t6P2jt9HpOs/PQyKw+rAR+lQI/jJJkfXbKqDLnioeeSDJBLU30fKO5WPa8Y
|
||||
Z0nf1R7CqJgrTEeDgUwuRMLvyGPui3tbMfYmYb95HLCpTqnJUHvi
|
||||
-----END CERTIFICATE-----
|
||||
|
@ -73,9 +73,8 @@ class _Collection(object):
|
||||
self.crt = int(time.mktime(d.timetuple()))
|
||||
self.sched = Scheduler(self)
|
||||
if not self.conf.get("newBury", False):
|
||||
mod = self.db.mod
|
||||
self.sched.unburyCards()
|
||||
self.db.mod = mod
|
||||
self.conf['newBury'] = True
|
||||
self.setMod()
|
||||
|
||||
def name(self):
|
||||
n = os.path.splitext(os.path.basename(self.path))[0]
|
||||
@ -148,10 +147,6 @@ crt=?, mod=?, scm=?, dty=?, usn=?, ls=?, conf=?""",
|
||||
def close(self, save=True):
|
||||
"Disconnect from DB."
|
||||
if self.db:
|
||||
if not self.conf.get("newBury", False):
|
||||
mod = self.db.mod
|
||||
self.sched.unburyCards()
|
||||
self.db.mod = mod
|
||||
if save:
|
||||
self.save()
|
||||
else:
|
||||
@ -515,13 +510,11 @@ where c.nid = n.id and c.id in %s group by nid""" % ids2str(cids)):
|
||||
afmt = afmt or template['afmt']
|
||||
for (type, format) in (("q", qfmt), ("a", afmt)):
|
||||
if type == "q":
|
||||
format = format.replace("{{cloze:", "{{cq:%d:" % (
|
||||
data[4]+1))
|
||||
format = re.sub("{{(?!type:)(.*?)cloze:", r"{{\1cq-%d:" % (data[4]+1), format)
|
||||
format = format.replace("<%cloze:", "<%%cq:%d:" % (
|
||||
data[4]+1))
|
||||
else:
|
||||
format = format.replace("{{cloze:", "{{ca:%d:" % (
|
||||
data[4]+1))
|
||||
format = re.sub("{{(.*?)cloze:", r"{{\1ca-%d:" % (data[4]+1), format)
|
||||
format = format.replace("<%cloze:", "<%%ca:%d:" % (
|
||||
data[4]+1))
|
||||
fields['FrontSide'] = stripSounds(d['q'])
|
||||
@ -613,13 +606,19 @@ where c.nid == f.id
|
||||
if self._undo[0] == 1:
|
||||
old = self._undo[2]
|
||||
self.clearUndo()
|
||||
self._undo = [1, _("Review"), old + [copy.copy(card)]]
|
||||
wasLeech = card.note().hasTag("leech") or False
|
||||
self._undo = [1, _("Review"), old + [copy.copy(card)], wasLeech]
|
||||
|
||||
def _undoReview(self):
|
||||
data = self._undo[2]
|
||||
wasLeech = self._undo[3]
|
||||
c = data.pop()
|
||||
if not data:
|
||||
self.clearUndo()
|
||||
# remove leech tag if it didn't have it before
|
||||
if not wasLeech and c.note().hasTag("leech"):
|
||||
c.note().delTag("leech")
|
||||
c.note().flush()
|
||||
# write old data
|
||||
c.flush()
|
||||
# and delete revlog entry
|
||||
@ -696,6 +695,10 @@ select id from notes where mid not in """ + ids2str(self.models.ids()))
|
||||
self.remNotes(ids)
|
||||
# for each model
|
||||
for m in self.models.all():
|
||||
for t in m['tmpls']:
|
||||
if t['did'] == "None":
|
||||
t['did'] = None
|
||||
problems.append(_("Fixed AnkiDroid deck override bug."))
|
||||
if m['type'] == MODEL_STD:
|
||||
# model with missing req specification
|
||||
if 'req' not in m:
|
||||
@ -753,6 +756,17 @@ select id from cards where odue > 0 and (type=1 or queue=2) and not odid""")
|
||||
"Fixed %d cards with invalid properties.", cnt) % cnt)
|
||||
self.db.execute("update cards set odue=0 where id in "+
|
||||
ids2str(ids))
|
||||
# cards with odid set when not in a dyn deck
|
||||
dids = [id for id in self.decks.allIds() if not self.decks.isDyn(id)]
|
||||
ids = self.db.list("""
|
||||
select id from cards where odid > 0 and did in %s""" % ids2str(dids))
|
||||
if ids:
|
||||
cnt = len(ids)
|
||||
problems.append(
|
||||
ngettext("Fixed %d card with invalid properties.",
|
||||
"Fixed %d cards with invalid properties.", cnt) % cnt)
|
||||
self.db.execute("update cards set odid=0, odue=0 where id in "+
|
||||
ids2str(ids))
|
||||
# tags
|
||||
self.tags.registerNotes()
|
||||
# field cache
|
||||
|
@ -38,6 +38,8 @@ DYN_DUE = 6
|
||||
DYN_REVADDED = 7
|
||||
DYN_DUEPRIORITY = 8
|
||||
|
||||
DYN_MAX_SIZE = 99999
|
||||
|
||||
# model types
|
||||
MODEL_STD = 0
|
||||
MODEL_CLOZE = 1
|
||||
|
@ -197,6 +197,12 @@ class DeckManager(object):
|
||||
deck['collapsed'] = not deck['collapsed']
|
||||
self.save(deck)
|
||||
|
||||
def collapseBrowser(self, did):
|
||||
deck = self.get(did)
|
||||
collapsed = deck.get('browserCollapsed', False)
|
||||
deck['browserCollapsed'] = not collapsed
|
||||
self.save(deck)
|
||||
|
||||
def count(self):
|
||||
return len(self.decks)
|
||||
|
||||
|
@ -20,10 +20,12 @@ class Exporter(object):
|
||||
file.close()
|
||||
|
||||
def escapeText(self, text):
|
||||
"Escape newlines, tabs and CSS."
|
||||
"Escape newlines, tabs, CSS and quotechar."
|
||||
text = text.replace("\n", "<br>")
|
||||
text = text.replace("\t", " " * 8)
|
||||
text = re.sub("(?i)<style>.*?</style>", "", text)
|
||||
if "\"" in text:
|
||||
text = "\"" + text.replace("\"", "\"\"") + "\""
|
||||
return text
|
||||
|
||||
def cardIds(self):
|
||||
@ -134,8 +136,14 @@ class AnkiExporter(Exporter):
|
||||
data)
|
||||
# notes
|
||||
strnids = ids2str(nids.keys())
|
||||
notedata = self.src.db.all("select * from notes where id in "+
|
||||
strnids)
|
||||
notedata = []
|
||||
for row in self.src.db.all(
|
||||
"select * from notes where id in "+strnids):
|
||||
# remove system tags if not exporting scheduling info
|
||||
if not self.includeSched:
|
||||
row = list(row)
|
||||
row[5] = self.removeSystemTags(row[5])
|
||||
notedata.append(row)
|
||||
self.dst.db.executemany(
|
||||
"insert into notes values (?,?,?,?,?,?,?,?,?,?,?)",
|
||||
notedata)
|
||||
@ -206,6 +214,9 @@ class AnkiExporter(Exporter):
|
||||
# overwrite to apply customizations to the deck before it's closed,
|
||||
# such as update the deck description
|
||||
pass
|
||||
|
||||
def removeSystemTags(self, tags):
|
||||
return self.src.tags.remFromStr("marked leech", tags)
|
||||
|
||||
# Packaged Anki decks
|
||||
######################################################################
|
||||
|
@ -18,7 +18,7 @@ class MnemosyneImporter(NoteImporter):
|
||||
db = DB(self.file)
|
||||
ver = db.scalar(
|
||||
"select value from global_variables where key='version'")
|
||||
assert ver.startswith('Mnemosyne SQL 1')
|
||||
assert ver.startswith('Mnemosyne SQL 1') or ver == "2"
|
||||
# gather facts into temp objects
|
||||
curid = None
|
||||
notes = {}
|
||||
|
@ -140,65 +140,29 @@ class SupermemoXmlImporter(NoteImporter):
|
||||
#s = re.sub(u'>',u'>',s)
|
||||
#s = re.sub(u'<',u'<',s)
|
||||
|
||||
return unicode(btflsoup(s,convertEntities=btflsoup.HTML_ENTITIES ))
|
||||
return unicode(btflsoup(s, selfClosingTags=['br','hr','img','wbr'], convertEntities=btflsoup.HTML_ENTITIES))
|
||||
|
||||
def _afactor2efactor(self, af):
|
||||
# Adapted from <http://www.supermemo.com/beta/xml/xml-core.htm>
|
||||
|
||||
def _unescape(self,s,initilize):
|
||||
"""Note: This method is not used, BeautifulSoup does better job.
|
||||
"""
|
||||
# Ranges for A-factors and E-factors
|
||||
af_min = 1.2
|
||||
af_max = 6.9
|
||||
ef_min = 1.3
|
||||
ef_max = 3.3
|
||||
|
||||
if self._unescape_trtable == None:
|
||||
self._unescape_trtable = (
|
||||
('€',u'€'), (' ',u' '), ('!',u'!'), ('"',u'"'), ('#',u'#'), ('$',u'$'), ('%',u'%'), ('&',u'&'), (''',u"'"),
|
||||
('(',u'('), (')',u')'), ('*',u'*'), ('+',u'+'), (',',u','), ('-',u'-'), ('.',u'.'), ('/',u'/'), ('0',u'0'),
|
||||
('1',u'1'), ('2',u'2'), ('3',u'3'), ('4',u'4'), ('5',u'5'), ('6',u'6'), ('7',u'7'), ('8',u'8'), ('9',u'9'),
|
||||
(':',u':'), (';',u';'), ('<',u'<'), ('=',u'='), ('>',u'>'), ('?',u'?'), ('@',u'@'), ('A',u'A'), ('B',u'B'),
|
||||
('C',u'C'), ('D',u'D'), ('E',u'E'), ('F',u'F'), ('G',u'G'), ('H',u'H'), ('I',u'I'), ('J',u'J'), ('K',u'K'),
|
||||
('L',u'L'), ('M',u'M'), ('N',u'N'), ('O',u'O'), ('P',u'P'), ('Q',u'Q'), ('R',u'R'), ('S',u'S'), ('T',u'T'),
|
||||
('U',u'U'), ('V',u'V'), ('W',u'W'), ('X',u'X'), ('Y',u'Y'), ('Z',u'Z'), ('[',u'['), ('\',u'\\'), (']',u']'),
|
||||
('^',u'^'), ('_',u'_'), ('`',u'`'), ('a',u'a'), ('b',u'b'), ('c',u'c'), ('d',u'd'), ('e',u'e'), ('f',u'f'),
|
||||
('g',u'g'), ('h',u'h'), ('i',u'i'), ('j',u'j'), ('k',u'k'), ('l',u'l'), ('m',u'm'), ('n',u'n'),
|
||||
('o',u'o'), ('p',u'p'), ('q',u'q'), ('r',u'r'), ('s',u's'), ('t',u't'), ('u',u'u'), ('v',u'v'),
|
||||
('w',u'w'), ('x',u'x'), ('y',u'y'), ('z',u'z'), ('{',u'{'), ('|',u'|'), ('}',u'}'), ('~',u'~'),
|
||||
(' ',u' '), ('¡',u'¡'), ('¢',u'¢'), ('£',u'£'), ('¤',u'¤'), ('¥',u'¥'), ('¦',u'¦'), ('§',u'§'),
|
||||
('¨',u'¨'), ('©',u'©'), ('ª',u'ª'), ('«',u'«'), ('¬',u'¬'), ('­',u''), ('®',u'®'), ('¯',u'¯'),
|
||||
('°',u'°'), ('±',u'±'), ('²',u'²'), ('³',u'³'), ('´',u'´'), ('µ',u'µ'), ('¶',u'¶'), ('·',u'·'),
|
||||
('¸',u'¸'), ('¹',u'¹'), ('º',u'º'), ('»',u'»'), ('¼',u'¼'), ('½',u'½'), ('¾',u'¾'), ('¿',u'¿'),
|
||||
('À',u'À'), ('Á',u'Á'), ('Â',u'Â'), ('Ã',u'Ã'), ('Ä',u'Ä'), ('Å',u'Å'), ('Å',u'Å'), ('Æ',u'Æ'),
|
||||
('Ç',u'Ç'), ('È',u'È'), ('É',u'É'), ('Ê',u'Ê'), ('Ë',u'Ë'), ('Ì',u'Ì'), ('Í',u'Í'), ('Î',u'Î'),
|
||||
('Ï',u'Ï'), ('Ð',u'Ð'), ('Ñ',u'Ñ'), ('Ò',u'Ò'), ('Ó',u'Ó'), ('Ô',u'Ô'), ('Õ',u'Õ'), ('Ö',u'Ö'),
|
||||
('×',u'×'), ('Ø',u'Ø'), ('Ù',u'Ù'), ('Ú',u'Ú'), ('Û',u'Û'), ('Ü',u'Ü'), ('Ý',u'Ý'), ('Þ',u'Þ'),
|
||||
('ß',u'ß'), ('à',u'à'), ('á',u'á'), ('â',u'â'), ('ã',u'ã'), ('ä',u'ä'), ('å',u'å'), ('æ',u'æ'),
|
||||
('ç',u'ç'), ('è',u'è'), ('é',u'é'), ('ê',u'ê'), ('ë',u'ë'), ('ì',u'ì'), ('í',u'í'), ('í',u'í'),
|
||||
('î',u'î'), ('ï',u'ï'), ('ð',u'ð'), ('ñ',u'ñ'), ('ò',u'ò'), ('ó',u'ó'), ('ô',u'ô'), ('õ',u'õ'),
|
||||
('ö',u'ö'), ('÷',u'÷'), ('ø',u'ø'), ('ù',u'ù'), ('ú',u'ú'), ('û',u'û'), ('ü',u'ü'), ('ý',u'ý'),
|
||||
('þ',u'þ'), ('ÿ',u'ÿ'), ('Ā',u'Ā'), ('ā',u'ā'), ('Ă',u'Ă'), ('ă',u'ă'), ('Ą',u'Ą'), ('ą',u'ą'),
|
||||
('Ć',u'Ć'), ('ć',u'ć'), ('Ĉ',u'Ĉ'), ('ĉ',u'ĉ'), ('Ċ',u'Ċ'), ('ċ',u'ċ'), ('Č',u'Č'), ('č',u'č'),
|
||||
('Ď',u'Ď'), ('ď',u'ď'), ('Đ',u'Đ'), ('đ',u'đ'), ('Ē',u'Ē'), ('ē',u'ē'), ('Ĕ',u'Ĕ'), ('ĕ',u'ĕ'),
|
||||
('Ė',u'Ė'), ('ė',u'ė'), ('Ę',u'Ę'), ('ę',u'ę'), ('Ě',u'Ě'), ('ě',u'ě'), ('Ĝ',u'Ĝ'), ('ĝ',u'ĝ'),
|
||||
('Ğ',u'Ğ'), ('ğ',u'ğ'), ('Ġ',u'Ġ'), ('ġ',u'ġ'), ('Ģ',u'Ģ'), ('ģ',u'ģ'), ('Ĥ',u'Ĥ'), ('ĥ',u'ĥ'),
|
||||
('Ħ',u'Ħ'), ('ħ',u'ħ'), ('Ĩ',u'Ĩ'), ('ĩ',u'ĩ'), ('Ī',u'Ī'), ('ī',u'ī'), ('Ĭ',u'Ĭ'), ('ĭ',u'ĭ'),
|
||||
('Į',u'Į'), ('į',u'į'), ('İ',u'İ'), ('ı',u'ı'), ('IJ',u'IJ'), ('ij',u'ij'), ('Ĵ',u'Ĵ'), ('ĵ',u'ĵ'),
|
||||
('Ķ',u'Ķ'), ('ķ',u'ķ'), ('ĸ',u'ĸ'), ('Ĺ',u'Ĺ'), ('ĺ',u'ĺ'), ('Ļ',u'Ļ'), ('ļ',u'ļ'), ('Ľ',u'Ľ'),
|
||||
('ľ',u'ľ'), ('Ŀ',u'Ŀ'), ('ŀ',u'ŀ'), ('Ł',u'Ł'), ('ł',u'ł'), ('Ń',u'Ń'), ('ń',u'ń'), ('Ņ',u'Ņ'),
|
||||
('ņ',u'ņ'), ('Ň',u'Ň'), ('ň',u'ň'), ('ʼn',u'ʼn'), ('Ŋ',u'Ŋ'), ('ŋ',u'ŋ'), ('Ō',u'Ō'), ('ō',u'ō'),
|
||||
('Ŏ',u'Ŏ'), ('ŏ',u'ŏ'), ('Ő',u'Ő'), ('ő',u'ő'), ('Œ',u'Œ'), ('œ',u'œ'), ('Ŕ',u'Ŕ'), ('ŕ',u'ŕ'),
|
||||
('Ŗ',u'Ŗ'), ('ŗ',u'ŗ'), ('Ř',u'Ř'), ('ř',u'ř'), ('Ś',u'Ś'), ('ś',u'ś'), ('Ŝ',u'Ŝ'), ('ŝ',u'ŝ'),
|
||||
('Ş',u'Ş'), ('ş',u'ş'), ('Š',u'Š'), ('š',u'š'), ('Ţ',u'Ţ'), ('ţ',u'ţ'), ('Ť',u'Ť'), ('ť',u'ť'),
|
||||
('Ŧ',u'Ŧ'), ('ŧ',u'ŧ'), ('Ũ',u'Ũ'), ('ũ',u'ũ'), ('Ū',u'Ū'), ('ū',u'ū'), ('Ŭ',u'Ŭ'), ('ŭ',u'ŭ'),
|
||||
('Ů',u'Ů'), ('ů',u'ů'), ('Ű',u'Ű'), ('ű',u'ű'), ('Ų',u'Ų'), ('ų',u'ų'), ('Ŵ',u'Ŵ'), ('ŵ',u'ŵ'),
|
||||
('Ŷ',u'Ŷ'), ('ŷ',u'ŷ'), ('Ÿ',u'Ÿ'), ('Ź',u'Ź'), ('ź',u'ź'), ('Ż',u'Ż'), ('ż',u'ż'), ('Ž',u'Ž'),
|
||||
('ž',u'ž'), ('ſ',u'ſ'), ('Ŕ',u'Ŕ'), ('ŕ',u'ŕ'), ('Ŗ',u'Ŗ'), ('ŗ',u'ŗ'), ('Ř',u'Ř'), ('ř',u'ř'),
|
||||
('Ś',u'Ś'), ('ś',u'ś'), ('Ŝ',u'Ŝ'), ('ŝ',u'ŝ'), ('Ş',u'Ş'), ('ş',u'ş'), ('Š',u'Š'), ('š',u'š'),
|
||||
('Ţ',u'Ţ'), ('ţ',u'ţ'), ('Ť',u'Ť'), ('Ɂ',u'ť'), ('Ŧ',u'Ŧ'), ('ŧ',u'ŧ'), ('Ũ',u'Ũ'), ('ũ',u'ũ'),
|
||||
('Ū',u'Ū'), ('ū',u'ū'), ('Ŭ',u'Ŭ'), ('ŭ',u'ŭ'), ('Ů',u'Ů'), ('ů',u'ů'), ('Ű',u'Ű'), ('ű',u'ű'),
|
||||
('Ų',u'Ų'), ('ų',u'ų'), ('Ŵ',u'Ŵ'), ('ŵ',u'ŵ'), ('Ŷ',u'Ŷ'), ('ŷ',u'ŷ'), ('Ÿ',u'Ÿ'), ('Ź',u'Ź'),
|
||||
('ź',u'ź'), ('Ż',u'Ż'), ('ż',u'ż'), ('Ž',u'Ž'), ('ž',u'ž'), ('ſ',u'ſ'),
|
||||
)
|
||||
# Sanity checks for the A-factor
|
||||
if af < af_min:
|
||||
af = af_min
|
||||
elif af > af_max:
|
||||
af = af_max
|
||||
|
||||
# Scale af to the range 0..1
|
||||
af_scaled = (af - af_min) / (af_max - af_min)
|
||||
# Rescale to the interval ef_min..ef_max
|
||||
ef = ef_min + af_scaled * (ef_max - ef_min)
|
||||
|
||||
#m = re.match()
|
||||
#s = s.replace(code[0], code[1])
|
||||
return ef
|
||||
|
||||
## DEFAULT IMPORTER METHODS
|
||||
|
||||
@ -249,7 +213,7 @@ class SupermemoXmlImporter(NoteImporter):
|
||||
nextDue = tLastrep + (float(item.Interval) * 86400.0)
|
||||
remDays = int((nextDue - time.time())/86400)
|
||||
card.due = self.col.sched.today+remDays
|
||||
card.factor = int(float(item.AFactor.replace(',','.'))*1000)
|
||||
card.factor = int(self._afactor2efactor(float(item.AFactor.replace(',','.')))*1000)
|
||||
note.cards[0] = card
|
||||
|
||||
# categories & tags
|
||||
|
@ -7,8 +7,13 @@ from anki.utils import checksum, call, namedtmp, tmpdir, isMac, stripHTML
|
||||
from anki.hooks import addHook
|
||||
from anki.lang import _
|
||||
|
||||
latexCmd = ["latex", "-interaction=nonstopmode"]
|
||||
latexDviPngCmd = ["dvipng", "-D", "200", "-T", "tight"]
|
||||
# if you modify these in an add-on, you must make sure to take tmp.tex as the
|
||||
# input, and output tmp.png as the output file
|
||||
latexCmds = [
|
||||
["latex", "-interaction=nonstopmode", "tmp.tex"],
|
||||
["dvipng", "-D", "200", "-T", "tight", "tmp.dvi", "-o", "tmp.png"]
|
||||
]
|
||||
|
||||
build = True # if off, use existing media but don't create new
|
||||
regexps = {
|
||||
"standard": re.compile(r"\[latex\](.+?)\[/latex\]", re.DOTALL | re.IGNORECASE),
|
||||
@ -89,14 +94,11 @@ package in the LaTeX header instead.""") % bad
|
||||
oldcwd = os.getcwd()
|
||||
png = namedtmp("tmp.png")
|
||||
try:
|
||||
# generate dvi
|
||||
# generate png
|
||||
os.chdir(tmpdir())
|
||||
if call(latexCmd + ["tmp.tex"], stdout=log, stderr=log):
|
||||
return _errMsg("latex", texpath)
|
||||
# and png
|
||||
if call(latexDviPngCmd + ["tmp.dvi", "-o", "tmp.png"],
|
||||
stdout=log, stderr=log):
|
||||
return _errMsg("dvipng", texpath)
|
||||
for latexCmd in latexCmds:
|
||||
if call(latexCmd, stdout=log, stderr=log):
|
||||
return _errMsg(latexCmd[0], texpath)
|
||||
# add to media
|
||||
shutil.copyfile(png, os.path.join(mdir, fname))
|
||||
return
|
||||
|
@ -217,6 +217,7 @@ class MediaManager(object):
|
||||
files = os.listdir(mdir)
|
||||
else:
|
||||
files = local
|
||||
renamedFiles = False
|
||||
for file in files:
|
||||
if not local:
|
||||
if not os.path.isfile(file):
|
||||
@ -236,14 +237,20 @@ class MediaManager(object):
|
||||
# delete if we already have the NFC form, otherwise rename
|
||||
if os.path.exists(nfcFile):
|
||||
os.unlink(file)
|
||||
renamedFiles = True
|
||||
else:
|
||||
os.rename(file, nfcFile)
|
||||
renamedFiles = True
|
||||
file = nfcFile
|
||||
# compare
|
||||
if nfcFile not in allRefs:
|
||||
unused.append(file)
|
||||
else:
|
||||
allRefs.discard(nfcFile)
|
||||
# if we renamed any files to nfc format, we must rerun the check
|
||||
# to make sure the renamed files are not marked as unused
|
||||
if renamedFiles:
|
||||
return self.check(local=local)
|
||||
nohave = [x for x in allRefs if not x.startswith("_")]
|
||||
return (nohave, unused, invalid)
|
||||
|
||||
@ -333,7 +340,7 @@ class MediaManager(object):
|
||||
# Illegal characters
|
||||
##########################################################################
|
||||
|
||||
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0]')
|
||||
_illegalCharReg = re.compile(r'[][><:"/?*^\\|\0\r\n]')
|
||||
|
||||
def stripIllegal(self, str):
|
||||
return re.sub(self._illegalCharReg, "", str)
|
||||
|
@ -569,7 +569,7 @@ select id from notes where mid = ?)""" % " ".join(map),
|
||||
sflds = splitFields(flds)
|
||||
map = self.fieldMap(m)
|
||||
ords = set()
|
||||
matches = re.findall("{{cloze:(.+?)}}", m['tmpls'][0]['qfmt'])
|
||||
matches = re.findall("{{[^}]*?cloze:(?:[^}]?:)*(.+?)}}", m['tmpls'][0]['qfmt'])
|
||||
matches += re.findall("<%cloze:(.+?)%>", m['tmpls'][0]['qfmt'])
|
||||
for fname in matches:
|
||||
if fname not in map:
|
||||
|
@ -954,7 +954,9 @@ select id from cards where did in %s and queue = 2 and due <= ? limit ?)"""
|
||||
def _fillDyn(self, deck):
|
||||
search, limit, order = deck['terms'][0]
|
||||
orderlimit = self._dynOrder(order, limit)
|
||||
search += " -is:suspended -is:buried -deck:filtered"
|
||||
if search.strip():
|
||||
search = "(%s)" % search
|
||||
search = "%s -is:suspended -is:buried -deck:filtered" % search
|
||||
try:
|
||||
ids = self.col.findCards(search, order=orderlimit)
|
||||
except:
|
||||
@ -997,7 +999,7 @@ due = odue, odue = 0, odid = 0, usn = ?, mod = ? where %s""" % lim,
|
||||
elif o == DYN_DUE:
|
||||
t = "c.due"
|
||||
elif o == DYN_DUEPRIORITY:
|
||||
t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 10000+due end)" % (
|
||||
t = "(case when queue=2 and due <= %d then (ivl / cast(%d-due+0.001 as real)) else 100000+due end)" % (
|
||||
self.today, self.today)
|
||||
else:
|
||||
# if we don't understand the term, default to due order
|
||||
|
@ -204,12 +204,12 @@ addHook("unloadProfile", stopMplayer)
|
||||
##########################################################################
|
||||
|
||||
try:
|
||||
|
||||
import pyaudio
|
||||
import wave
|
||||
|
||||
PYAU_FORMAT = pyaudio.paInt16
|
||||
PYAU_CHANNELS = 1
|
||||
PYAU_RATE = 44100
|
||||
PYAU_INPUT_INDEX = None
|
||||
except:
|
||||
pass
|
||||
@ -244,12 +244,16 @@ class PyAudioThreadedRecorder(threading.Thread):
|
||||
except NameError:
|
||||
raise Exception(
|
||||
"Pyaudio not installed (recording not supported on OSX10.3)")
|
||||
|
||||
rate = int(p.get_default_input_device_info()['defaultSampleRate'])
|
||||
|
||||
stream = p.open(format=PYAU_FORMAT,
|
||||
channels=PYAU_CHANNELS,
|
||||
rate=PYAU_RATE,
|
||||
rate=rate,
|
||||
input=True,
|
||||
input_device_index=PYAU_INPUT_INDEX,
|
||||
frames_per_buffer=chunk)
|
||||
|
||||
all = []
|
||||
while not self.finish:
|
||||
try:
|
||||
@ -267,7 +271,7 @@ class PyAudioThreadedRecorder(threading.Thread):
|
||||
wf = wave.open(processingSrc, 'wb')
|
||||
wf.setnchannels(PYAU_CHANNELS)
|
||||
wf.setsampwidth(p.get_sample_size(PYAU_FORMAT))
|
||||
wf.setframerate(PYAU_RATE)
|
||||
wf.setframerate(rate)
|
||||
wf.writeframes(data)
|
||||
wf.close()
|
||||
|
||||
|
@ -353,7 +353,7 @@ group by day order by day""" % (self._limit(), lim),
|
||||
tot, period, unit))
|
||||
if total and tot:
|
||||
perMin = total / float(tot)
|
||||
perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % perMin
|
||||
perMin = ngettext("%d card/minute", "%d cards/minute", perMin) % round(perMin)
|
||||
self._line(
|
||||
i, _("Average answer time"),
|
||||
_("%(a)0.1fs (%(b)s)") % dict(a=(tot*60)/total, b=perMin))
|
||||
@ -705,7 +705,7 @@ select
|
||||
sum(case when queue=2 and ivl >= 21 then 1 else 0 end), -- mtr
|
||||
sum(case when queue in (1,3) or (queue=2 and ivl < 21) then 1 else 0 end), -- yng/lrn
|
||||
sum(case when queue=0 then 1 else 0 end), -- new
|
||||
sum(case when queue=-1 then 1 else 0 end) -- susp
|
||||
sum(case when queue<0 then 1 else 0 end) -- susp
|
||||
from cards where did in %s""" % self._limit())
|
||||
|
||||
# Footer
|
||||
|
@ -48,10 +48,11 @@ def addForwardOptionalReverse(col):
|
||||
mm = col.models
|
||||
m = addBasicModel(col)
|
||||
m['name'] = _("Basic (optional reversed card)")
|
||||
fm = mm.newField(_("Add Reverse"))
|
||||
av = _("Add Reverse")
|
||||
fm = mm.newField(av)
|
||||
mm.addField(m, fm)
|
||||
t = mm.newTemplate(_("Card 2"))
|
||||
t['qfmt'] = "{{#Add Reverse}}{{"+_("Back")+"}}{{/Add Reverse}}"
|
||||
t['qfmt'] = "{{#%s}}{{%s}}{{/%s}}" % (av, _("Back"), av)
|
||||
t['afmt'] = "{{FrontSide}}\n\n<hr id=answer>\n\n"+"{{"+_("Front")+"}}"
|
||||
mm.addTemplate(m, t)
|
||||
return m
|
||||
|
@ -757,6 +757,7 @@ class MediaSyncer(object):
|
||||
# if the sanity check failed, force a resync
|
||||
self.col.media.forceResync()
|
||||
return "sanityCheckFailed"
|
||||
return "success"
|
||||
|
||||
def removed(self):
|
||||
return self.col.media.removed()
|
||||
|
@ -4,7 +4,7 @@ from anki.hooks import runFilter
|
||||
from anki.template import furigana; furigana.install()
|
||||
from anki.template import hint; hint.install()
|
||||
|
||||
clozeReg = r"\{\{c%s::(.*?)(::(.*?))?\}\}"
|
||||
clozeReg = r"(?s)\{\{c%s::(.*?)(::(.*?))?\}\}"
|
||||
|
||||
modifiers = {}
|
||||
def modifier(symbol):
|
||||
@ -158,40 +158,45 @@ class Template(object):
|
||||
return txt
|
||||
|
||||
# field modifiers
|
||||
parts = tag_name.split(':',2)
|
||||
parts = tag_name.split(':')
|
||||
extra = None
|
||||
if len(parts) == 1 or parts[0] == '':
|
||||
return '{unknown field %s}' % tag_name
|
||||
elif len(parts) == 2:
|
||||
(mod, tag) = parts
|
||||
elif len(parts) == 3:
|
||||
(mod, extra, tag) = parts
|
||||
else:
|
||||
mods, tag = parts[:-1], parts[-1] #py3k has *mods, tag = parts
|
||||
|
||||
txt = get_or_attr(context, tag)
|
||||
|
||||
#Since 'text:' and other mods can affect html on which Anki relies to
|
||||
#process clozes, we need to make sure clozes are always
|
||||
#treated after all the other mods, regardless of how they're specified
|
||||
#in the template, so that {{cloze:text: == {{text:cloze:
|
||||
#For type:, we return directly since no other mod than cloze (or other
|
||||
#pre-defined mods) can be present and those are treated separately
|
||||
mods.reverse()
|
||||
mods.sort(key=lambda s: not s=="type")
|
||||
|
||||
# built-in modifiers
|
||||
if mod == 'text':
|
||||
# strip html
|
||||
if txt:
|
||||
return stripHTML(txt)
|
||||
return ""
|
||||
elif mod == 'type':
|
||||
# type answer field; convert it to [[type:...]] for the gui code
|
||||
# to process
|
||||
return "[[%s]]" % tag_name
|
||||
elif mod == 'cq' or mod == 'ca':
|
||||
# cloze deletion
|
||||
if txt and extra:
|
||||
return self.clozeText(txt, extra, mod[1])
|
||||
for mod in mods:
|
||||
# built-in modifiers
|
||||
if mod == 'text':
|
||||
# strip html
|
||||
txt = stripHTML(txt) if txt else ""
|
||||
elif mod == 'type':
|
||||
# type answer field; convert it to [[type:...]] for the gui code
|
||||
# to process
|
||||
return "[[%s]]" % tag_name
|
||||
elif mod.startswith('cq-') or mod.startswith('ca-'):
|
||||
# cloze deletion
|
||||
mod, extra = mod.split("-")
|
||||
txt = self.clozeText(txt, extra, mod[1]) if txt and extra else ""
|
||||
else:
|
||||
return ""
|
||||
else:
|
||||
# hook-based field modifier
|
||||
txt = runFilter('fmod_' + mod, txt or '', extra, context,
|
||||
tag, tag_name);
|
||||
if txt is None:
|
||||
return '{unknown field %s}' % tag_name
|
||||
return txt
|
||||
# hook-based field modifier
|
||||
mod, extra = re.search("^(.*?)(?:\((.*)\))?$", mod).groups()
|
||||
txt = runFilter('fmod_' + mod, txt or '', extra or '', context,
|
||||
tag, tag_name);
|
||||
if txt is None:
|
||||
return '{unknown field %s}' % tag_name
|
||||
return txt
|
||||
|
||||
def clozeText(self, txt, ord, type):
|
||||
reg = clozeReg
|
||||
|
@ -17,6 +17,7 @@ import sys
|
||||
import locale
|
||||
from hashlib import sha1
|
||||
import platform
|
||||
import traceback
|
||||
|
||||
from anki.lang import _, ngettext
|
||||
|
||||
@ -360,6 +361,8 @@ def invalidFilename(str, dirsep=True):
|
||||
return "/"
|
||||
elif (dirsep or not isWin) and "\\" in str:
|
||||
return "\\"
|
||||
elif str.strip().startswith("."):
|
||||
return "."
|
||||
|
||||
def platDesc():
|
||||
# we may get an interrupted system call, so try this in a loop
|
||||
@ -382,3 +385,15 @@ def platDesc():
|
||||
except:
|
||||
continue
|
||||
return theos
|
||||
|
||||
# Debugging
|
||||
##############################################################################
|
||||
|
||||
class TimedLog(object):
|
||||
def __init__(self):
|
||||
self._last = time.time()
|
||||
def log(self, s):
|
||||
path, num, fn, y = traceback.extract_stack(limit=2)[0]
|
||||
sys.stderr.write("%5dms: %s(): %s\n" % ((time.time() - self._last)*1000, fn, s))
|
||||
self._last = time.time()
|
||||
|
||||
|
@ -31,8 +31,8 @@ system. It's free and open source.")
|
||||
Alex Fraser, Andreas Klauer, Andrew Wright, Bernhard Ibertsberger, Charlene Barina,
|
||||
Christian Krause, Christian Rusche, David Smith, Dave Druelinger, Dotan Cohen,
|
||||
Emilio Wuerges, Emmanuel Jarri, Frank Harper, Gregor Skumavc, H. Mijail,
|
||||
Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7,
|
||||
Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Kieran Clancy, LaC, Laurent Steffan,
|
||||
Houssam Salem, Ian Lewis, Immanuel Asmus, Iroiro, Jarvik7,
|
||||
Jin Eun-Deok, Jo Nakashima, Johanna Lindh, Julien Baley, Kieran Clancy, LaC, Laurent Steffan,
|
||||
Luca Ban, Luciano Esposito, Marco Giancotti, Marcus Rubeus, Mari Egami, Michael Jürges, Mark Wilbur,
|
||||
Matthew Duggan, Matthew Holtz, Meelis Vasser, Michael Keppler, Michael
|
||||
Montague, Michael Penkov, Michal Čadil, Morteza Salehi, Nathanael Law, Nick Cook, Niklas
|
||||
|
145
aqt/browser.py
@ -96,10 +96,14 @@ class DataModel(QAbstractTableModel):
|
||||
return
|
||||
elif role == Qt.DisplayRole and section < len(self.activeCols):
|
||||
type = self.columnType(section)
|
||||
txt = None
|
||||
for stype, name in self.browser.columns:
|
||||
if type == stype:
|
||||
txt = name
|
||||
break
|
||||
# handle case where extension has set an invalid column type
|
||||
if not txt:
|
||||
txt = self.browser.columns[0][1]
|
||||
return txt
|
||||
else:
|
||||
return
|
||||
@ -500,15 +504,18 @@ class Browser(QMainWindow):
|
||||
|
||||
def setupSearch(self):
|
||||
self.filterTimer = None
|
||||
self.form.searchEdit.setLineEdit(FavouritesLineEdit(self.mw, self))
|
||||
self.connect(self.form.searchButton,
|
||||
SIGNAL("clicked()"),
|
||||
self.onSearch)
|
||||
self.connect(self.form.searchEdit.lineEdit(),
|
||||
SIGNAL("returnPressed()"),
|
||||
self.onSearch)
|
||||
self.setTabOrder(self.form.searchEdit, self.form.tableView)
|
||||
self.form.searchEdit.setCompleter(None)
|
||||
self.form.searchEdit.addItems(self.mw.pm.profile['searchHistory'])
|
||||
self.connect(self.form.searchEdit.lineEdit(),
|
||||
SIGNAL("returnPressed()"),
|
||||
self.onSearch)
|
||||
|
||||
def onSearch(self, reset=True):
|
||||
"Careful: if reset is true, the current note is saved."
|
||||
@ -712,11 +719,9 @@ by clicking on one on the left."""))
|
||||
|
||||
def setColumnSizes(self):
|
||||
hh = self.form.tableView.horizontalHeader()
|
||||
for i in range(len(self.model.activeCols)):
|
||||
if hh.visualIndex(i) == len(self.model.activeCols) - 1:
|
||||
hh.setResizeMode(i, QHeaderView.Stretch)
|
||||
else:
|
||||
hh.setResizeMode(i, QHeaderView.Interactive)
|
||||
hh.setResizeMode(QHeaderView.Interactive)
|
||||
hh.setResizeMode(hh.logicalIndex(len(self.model.activeCols)-1),
|
||||
QHeaderView.Stretch)
|
||||
# this must be set post-resize or it doesn't work
|
||||
hh.setCascadingSectionResizes(False)
|
||||
|
||||
@ -727,9 +732,10 @@ by clicking on one on the left."""))
|
||||
######################################################################
|
||||
|
||||
class CallbackItem(QTreeWidgetItem):
|
||||
def __init__(self, root, name, onclick):
|
||||
def __init__(self, root, name, onclick, oncollapse=None):
|
||||
QTreeWidgetItem.__init__(self, root, [name])
|
||||
self.onclick = onclick
|
||||
self.oncollapse = oncollapse
|
||||
|
||||
def setupTree(self):
|
||||
self.connect(
|
||||
@ -739,22 +745,31 @@ by clicking on one on the left."""))
|
||||
p.setColor(QPalette.Base, QColor("#d6dde0"))
|
||||
self.form.tree.setPalette(p)
|
||||
self.buildTree()
|
||||
self.connect(
|
||||
self.form.tree, SIGNAL("itemExpanded(QTreeWidgetItem*)"),
|
||||
lambda item: self.onTreeCollapse(item))
|
||||
self.connect(
|
||||
self.form.tree, SIGNAL("itemCollapsed(QTreeWidgetItem*)"),
|
||||
lambda item: self.onTreeCollapse(item))
|
||||
|
||||
def buildTree(self):
|
||||
self.form.tree.clear()
|
||||
root = self.form.tree
|
||||
self._systemTagTree(root)
|
||||
self._favTree(root)
|
||||
self._decksTree(root)
|
||||
self._modelTree(root)
|
||||
self._userTagTree(root)
|
||||
self.form.tree.expandAll()
|
||||
self.form.tree.setItemsExpandable(False)
|
||||
self.form.tree.setIndentation(15)
|
||||
|
||||
def onTreeClick(self, item, col):
|
||||
if getattr(item, 'onclick', None):
|
||||
item.onclick()
|
||||
|
||||
def onTreeCollapse(self, item):
|
||||
if getattr(item, 'oncollapse', None):
|
||||
item.oncollapse()
|
||||
|
||||
def setFilter(self, *args):
|
||||
if len(args) == 1:
|
||||
txt = args[0]
|
||||
@ -804,6 +819,18 @@ by clicking on one on the left."""))
|
||||
item.setIcon(0, QIcon(":/icons/" + icon))
|
||||
return root
|
||||
|
||||
def _favTree(self, root):
|
||||
saved = self.col.conf.get('savedFilters', [])
|
||||
if not saved:
|
||||
# Don't add favourites to tree if none saved
|
||||
return
|
||||
root = self.CallbackItem(root, _("My Searches"), None)
|
||||
root.setExpanded(True)
|
||||
root.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
|
||||
for name, filt in saved.items():
|
||||
item = self.CallbackItem(root, name, lambda s=filt: self.setFilter(s))
|
||||
item.setIcon(0, QIcon(":/icons/emblem-favorite-dark.png"))
|
||||
|
||||
def _userTagTree(self, root):
|
||||
for t in sorted(self.col.tags.all()):
|
||||
if t.lower() == "marked" or t.lower() == "leech":
|
||||
@ -817,10 +844,13 @@ by clicking on one on the left."""))
|
||||
def fillGroups(root, grps, head=""):
|
||||
for g in grps:
|
||||
item = self.CallbackItem(
|
||||
root, g[0], lambda g=g: self.setFilter(
|
||||
"deck", head+g[0]))
|
||||
root, g[0],
|
||||
lambda g=g: self.setFilter("deck", head+g[0]),
|
||||
lambda g=g: self.mw.col.decks.collapseBrowser(g[1]))
|
||||
item.setIcon(0, QIcon(":/icons/deck16.png"))
|
||||
newhead = head + g[0]+"::"
|
||||
collapsed = self.mw.col.decks.get(g[1]).get('browserCollapsed', False)
|
||||
item.setExpanded(not collapsed)
|
||||
fillGroups(item, g[5], newhead)
|
||||
fillGroups(root, grps)
|
||||
|
||||
@ -847,7 +877,7 @@ by clicking on one on the left."""))
|
||||
d = QDialog(self)
|
||||
l = QVBoxLayout()
|
||||
l.setMargin(0)
|
||||
w = AnkiWebView()
|
||||
w = AnkiWebView(canCopy=True)
|
||||
l.addWidget(w)
|
||||
w.stdHtml(info + "<p>" + reps)
|
||||
bb = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
@ -982,8 +1012,7 @@ where id in %s""" % ids2str(sf))
|
||||
c(self._previewWindow, SIGNAL("finished(int)"), self._onPreviewFinished)
|
||||
vbox = QVBoxLayout()
|
||||
vbox.setMargin(0)
|
||||
self._previewWeb = AnkiWebView()
|
||||
self._previewWeb.setFocusPolicy(Qt.NoFocus)
|
||||
self._previewWeb = AnkiWebView(True)
|
||||
vbox.addWidget(self._previewWeb)
|
||||
bbox = QDialogButtonBox()
|
||||
self._previewPrev = bbox.addButton("<", QDialogButtonBox.ActionRole)
|
||||
@ -1193,7 +1222,9 @@ update cards set usn=?, mod=?, did=? where id in """ + scids,
|
||||
frm = aqt.forms.reposition.Ui_Dialog()
|
||||
frm.setupUi(d)
|
||||
(pmin, pmax) = self.col.db.first(
|
||||
"select min(due), max(due) from cards where type=0")
|
||||
"select min(due), max(due) from cards where type=0 and odid=0")
|
||||
pmin = pmin or 0
|
||||
pmax = pmax or 0
|
||||
txt = _("Queue top: %d") % pmin
|
||||
txt += "\n" + _("Queue bottom: %d") % pmax
|
||||
frm.label.setText(txt)
|
||||
@ -1708,3 +1739,87 @@ a { margin-right: 1em; }
|
||||
self.browser.addTags()
|
||||
elif l == "deletetag":
|
||||
self.browser.deleteTags()
|
||||
|
||||
|
||||
# Favourites button
|
||||
######################################################################
|
||||
class FavouritesLineEdit(QLineEdit):
|
||||
buttonClicked = pyqtSignal(bool)
|
||||
|
||||
def __init__(self, mw, browser, parent=None):
|
||||
super(FavouritesLineEdit, self).__init__(parent)
|
||||
self.mw = mw
|
||||
self.browser = browser
|
||||
# add conf if missing
|
||||
if not self.mw.col.conf.has_key('savedFilters'):
|
||||
self.mw.col.conf['savedFilters'] = {}
|
||||
self.button = QToolButton(self)
|
||||
self.button.setStyleSheet('border: 0px;')
|
||||
self.button.setCursor(Qt.ArrowCursor)
|
||||
self.button.clicked.connect(self.buttonClicked.emit)
|
||||
self.setIcon(':/icons/emblem-favorite-off.png')
|
||||
# flag to raise save or delete dialog on button click
|
||||
self.doSave = True
|
||||
# name of current saved filter (if query matches)
|
||||
self.name = None
|
||||
self.buttonClicked.connect(self.onClicked)
|
||||
self.connect(self, SIGNAL("textChanged(QString)"), self.updateButton)
|
||||
|
||||
def resizeEvent(self, event):
|
||||
buttonSize = self.button.sizeHint()
|
||||
frameWidth = self.style().pixelMetric(QStyle.PM_DefaultFrameWidth)
|
||||
self.button.move(self.rect().right() - frameWidth - buttonSize.width(),
|
||||
(self.rect().bottom() - buttonSize.height() + 1) / 2)
|
||||
super(FavouritesLineEdit, self).resizeEvent(event)
|
||||
|
||||
def setIcon(self, path):
|
||||
self.button.setIcon(QIcon(path))
|
||||
|
||||
def setText(self, txt):
|
||||
super(FavouritesLineEdit, self).setText(txt)
|
||||
self.updateButton()
|
||||
|
||||
def updateButton(self, reset=True):
|
||||
# If search text is a saved query, switch to the delete button.
|
||||
# Otherwise show save button.
|
||||
txt = unicode(self.text()).strip()
|
||||
for key, value in self.mw.col.conf['savedFilters'].items():
|
||||
if txt == value:
|
||||
self.doSave = False
|
||||
self.name = key
|
||||
self.setIcon(QIcon(":/icons/emblem-favorite.png"))
|
||||
return
|
||||
self.doSave = True
|
||||
self.setIcon(QIcon(":/icons/emblem-favorite-off.png"))
|
||||
|
||||
def onClicked(self):
|
||||
if self.doSave:
|
||||
self.saveClicked()
|
||||
else:
|
||||
self.deleteClicked()
|
||||
|
||||
def saveClicked(self):
|
||||
txt = unicode(self.text()).strip()
|
||||
dlg = QInputDialog(self)
|
||||
dlg.setInputMode(QInputDialog.TextInput)
|
||||
dlg.setLabelText(_("The current search terms will be added as a new "
|
||||
"item in the sidebar.\n"
|
||||
"Search name:"))
|
||||
dlg.setWindowTitle(_("Save search"))
|
||||
ok = dlg.exec_()
|
||||
name = dlg.textValue()
|
||||
if ok:
|
||||
self.mw.col.conf['savedFilters'][name] = txt
|
||||
|
||||
self.updateButton()
|
||||
self.browser.setupTree()
|
||||
|
||||
def deleteClicked(self):
|
||||
msg = _('Remove "%s" from your saved searches?') % self.name
|
||||
ok = QMessageBox.question(self, _('Remove search'),
|
||||
msg, QMessageBox.Yes, QMessageBox.No)
|
||||
|
||||
if ok == QMessageBox.Yes:
|
||||
self.mw.col.conf['savedFilters'].pop(self.name, None)
|
||||
self.updateButton()
|
||||
self.browser.setupTree()
|
||||
|
@ -120,9 +120,9 @@ class CardLayout(QDialog):
|
||||
self.model, joinFields(self.note.fields)))
|
||||
for g in pform.groupBox, pform.groupBox_2:
|
||||
g.setTitle(g.title() + _(" (1 of %d)") % max(cnt, 1))
|
||||
pform.frontWeb = AnkiWebView()
|
||||
pform.frontWeb = AnkiWebView(True)
|
||||
pform.frontPrevBox.addWidget(pform.frontWeb)
|
||||
pform.backWeb = AnkiWebView()
|
||||
pform.backWeb = AnkiWebView(True)
|
||||
pform.backPrevBox.addWidget(pform.backWeb)
|
||||
for wig in pform.frontWeb, pform.backWeb:
|
||||
wig.page().setLinkDelegationPolicy(
|
||||
|
@ -41,7 +41,7 @@ class CustomStudy(QDialog):
|
||||
|
||||
def onRadioChange(self, idx):
|
||||
f = self.form; sp = f.spin
|
||||
smin = 1; smax = 9999; sval = 1
|
||||
smin = 1; smax = DYN_MAX_SIZE; sval = 1
|
||||
post = _("cards")
|
||||
tit = ""
|
||||
spShow = True
|
||||
@ -127,15 +127,15 @@ class CustomStudy(QDialog):
|
||||
# and then set various options
|
||||
if i == RADIO_FORGOT:
|
||||
dyn['delays'] = [1]
|
||||
dyn['terms'][0] = ['rated:%d:1' % spin, 9999, DYN_RANDOM]
|
||||
dyn['terms'][0] = ['rated:%d:1' % spin, DYN_MAX_SIZE, DYN_RANDOM]
|
||||
dyn['resched'] = False
|
||||
elif i == RADIO_AHEAD:
|
||||
dyn['delays'] = None
|
||||
dyn['terms'][0] = ['prop:due<=%d' % spin, 9999, DYN_DUE]
|
||||
dyn['terms'][0] = ['prop:due<=%d' % spin, DYN_MAX_SIZE, DYN_DUE]
|
||||
dyn['resched'] = True
|
||||
elif i == RADIO_PREVIEW:
|
||||
dyn['delays'] = None
|
||||
dyn['terms'][0] = ['is:new added:%s'%spin, 9999, DYN_OLDEST]
|
||||
dyn['terms'][0] = ['is:new added:%s'%spin, DYN_MAX_SIZE, DYN_OLDEST]
|
||||
dyn['resched'] = False
|
||||
elif i == RADIO_CRAM:
|
||||
dyn['delays'] = None
|
||||
|
@ -17,6 +17,7 @@ class DeckBrowser(object):
|
||||
self.mw = mw
|
||||
self.web = mw.web
|
||||
self.bottom = aqt.toolbar.BottomBar(mw, mw.bottomWeb)
|
||||
self.scrollPos = QPoint(0, 0)
|
||||
|
||||
def show(self):
|
||||
clearAudioQueue()
|
||||
@ -65,6 +66,7 @@ class DeckBrowser(object):
|
||||
key = unicode(evt.text())
|
||||
|
||||
def _selDeck(self, did):
|
||||
self.scrollPos = self.web.page().mainFrame().scrollPosition()
|
||||
self.mw.col.decks.select(did)
|
||||
self.mw.onOverview()
|
||||
|
||||
@ -152,7 +154,7 @@ body { margin: 1em; -webkit-user-select: none; }
|
||||
if self.web.key == "deckBrowser":
|
||||
return self.web.page().mainFrame().scrollPosition()
|
||||
else:
|
||||
return QPoint(0,0)
|
||||
return self.scrollPos
|
||||
|
||||
def _renderStats(self):
|
||||
cards, thetime = self.mw.col.db.first("""
|
||||
|
@ -21,7 +21,7 @@ import anki.js
|
||||
from BeautifulSoup import BeautifulSoup
|
||||
|
||||
pics = ("jpg", "jpeg", "png", "tif", "tiff", "gif", "svg")
|
||||
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv")
|
||||
audio = ("wav", "mp3", "ogg", "flac", "mp4", "swf", "mov", "mpeg", "mkv", "m4a")
|
||||
|
||||
_html = """
|
||||
<html><head>%s<style>
|
||||
@ -690,7 +690,7 @@ class Editor(object):
|
||||
|
||||
def onCloze(self):
|
||||
# check that the model is set up for cloze deletion
|
||||
if '{{cloze:' not in self.note.model()['tmpls'][0]['qfmt']:
|
||||
if not re.search('{{(.*:)*cloze:',self.note.model()['tmpls'][0]['qfmt']):
|
||||
if self.addMode:
|
||||
tooltip(_("Warning, cloze deletions will not work until "
|
||||
"you switch the type at the top to Cloze."))
|
||||
@ -988,7 +988,7 @@ to a cloze type first, via Edit>Change Note Type."""))
|
||||
class EditorWebView(AnkiWebView):
|
||||
|
||||
def __init__(self, parent, editor):
|
||||
AnkiWebView.__init__(self)
|
||||
AnkiWebView.__init__(self, canFocus=True)
|
||||
self.editor = editor
|
||||
self.strip = self.editor.mw.pm.profile['stripHTML']
|
||||
|
||||
@ -1117,13 +1117,18 @@ class EditorWebView(AnkiWebView):
|
||||
url = mime.urls()[0].toString()
|
||||
# chrome likes to give us the URL twice with a \n
|
||||
url = url.splitlines()[0]
|
||||
mime = QMimeData()
|
||||
newmime = QMimeData()
|
||||
link = self.editor.urlToLink(url)
|
||||
if link:
|
||||
mime.setHtml(link)
|
||||
newmime.setHtml(link)
|
||||
elif mime.hasImage():
|
||||
# if we couldn't convert the url to a link and there's an
|
||||
# image on the clipboard (such as copy&paste from
|
||||
# google images in safari), use that instead
|
||||
return self._processImage(mime)
|
||||
else:
|
||||
mime.setText(url)
|
||||
return mime
|
||||
newmime.setText(url)
|
||||
return newmime
|
||||
|
||||
# if the user has used 'copy link location' in the browser, the clipboard
|
||||
# will contain the URL as text, and no URLs or HTML. the URL will already
|
||||
|
@ -75,7 +75,7 @@ into a bug report:""")
|
||||
pluginText = _("""\
|
||||
An error occurred in an add-on.<br>
|
||||
Please post on the add-on forum:<br>%s<br>""")
|
||||
pluginText %= "https://groups.google.com/forum/#!forum/anki-addons"
|
||||
pluginText %= "https://anki.tenderapp.com/discussions/add-ons"
|
||||
if "addon" in error:
|
||||
txt = pluginText
|
||||
else:
|
||||
|
@ -17,7 +17,6 @@ import aqt.forms
|
||||
import aqt.modelchooser
|
||||
import aqt.deckchooser
|
||||
|
||||
|
||||
class ChangeMap(QDialog):
|
||||
def __init__(self, mw, model, current):
|
||||
QDialog.__init__(self, mw, Qt.Window)
|
||||
@ -80,6 +79,9 @@ class ImportDialog(QDialog):
|
||||
self.updateDelimiterButtonText()
|
||||
self.frm.allowHTML.setChecked(self.mw.pm.profile.get('allowHTML', True))
|
||||
self.frm.importMode.setCurrentIndex(self.mw.pm.profile.get('importMode', 1))
|
||||
# import button
|
||||
b = QPushButton(_("Import"))
|
||||
self.frm.buttonBox.addButton(b, QDialogButtonBox.AcceptRole)
|
||||
self.exec_()
|
||||
|
||||
def setupOptions(self):
|
||||
@ -88,8 +90,6 @@ class ImportDialog(QDialog):
|
||||
self.mw, self.frm.modelArea, label=False)
|
||||
self.deck = aqt.deckchooser.DeckChooser(
|
||||
self.mw, self.frm.deckArea, label=False)
|
||||
self.connect(self.frm.importButton, SIGNAL("clicked()"),
|
||||
self.doImport)
|
||||
|
||||
def modelChanged(self):
|
||||
self.importer.model = self.mw.col.models.current()
|
||||
@ -139,8 +139,8 @@ you can enter it here. Use \\t to represent tab."""),
|
||||
d = `d`
|
||||
txt = _("Fields separated by: %s") % d
|
||||
self.frm.autoDetect.setText(txt)
|
||||
|
||||
def doImport(self, update=False):
|
||||
|
||||
def accept(self):
|
||||
self.importer.mapping = self.mapping
|
||||
if not self.importer.mappingOk():
|
||||
showWarning(
|
||||
@ -250,7 +250,7 @@ you can enter it here. Use \\t to represent tab."""),
|
||||
QDialog.reject(self)
|
||||
|
||||
def helpRequested(self):
|
||||
openHelp("FileImport")
|
||||
openHelp("importing")
|
||||
|
||||
|
||||
def showUnicodeWarning():
|
||||
@ -338,6 +338,8 @@ with a different browser.""")
|
||||
msg = _("""\
|
||||
Invalid file. Please restore from backup.""")
|
||||
showWarning(msg)
|
||||
elif "invalidTempFolder" in err:
|
||||
showWarning(mw.errorHandler.tempFolderMsg())
|
||||
elif "readonly" in err:
|
||||
showWarning(_("""\
|
||||
Unable to import from a read-only file."""))
|
||||
|
22
aqt/main.py
@ -4,7 +4,7 @@
|
||||
|
||||
import re
|
||||
import signal
|
||||
import zipfile
|
||||
import zipfile
|
||||
|
||||
from send2trash import send2trash
|
||||
from aqt.qt import *
|
||||
@ -63,6 +63,8 @@ class AnkiQt(QMainWindow):
|
||||
self.onAppMsg(unicode(args[0], sys.getfilesystemencoding(), "ignore"))
|
||||
# Load profile in a timer so we can let the window finish init and not
|
||||
# close on profile load error.
|
||||
if isMac and qtmajor >= 5:
|
||||
self.show()
|
||||
self.progress.timer(10, self.setupProfile, False)
|
||||
|
||||
def setupUI(self):
|
||||
@ -264,15 +266,19 @@ To import into a password protected profile, please open the profile before atte
|
||||
|
||||
def loadCollection(self):
|
||||
self.hideSchemaMsg = True
|
||||
cpath = self.pm.collectionPath()
|
||||
try:
|
||||
self.col = Collection(self.pm.collectionPath(), log=True)
|
||||
self.col = Collection(cpath, log=True)
|
||||
except anki.db.Error:
|
||||
# move back to profile manager
|
||||
# warn user
|
||||
showWarning("""\
|
||||
Your collection is corrupt. Please see the manual for \
|
||||
how to restore from a backup.""")
|
||||
self.unloadProfile()
|
||||
raise
|
||||
# move it out of the way so the profile can be used again
|
||||
newpath = cpath+str(intTime())
|
||||
os.rename(cpath, newpath)
|
||||
# then close
|
||||
sys.exit(1)
|
||||
except Exception, e:
|
||||
# the custom exception handler won't catch this if we immediately
|
||||
# unload, so we have to manually handle it
|
||||
@ -377,7 +383,9 @@ the manual for information on how to restore from an automatic backup."))
|
||||
if cleanup:
|
||||
cleanup(state)
|
||||
self.state = state
|
||||
runHook('beforeStateChange', state, oldState, *args)
|
||||
getattr(self, "_"+state+"State")(oldState, *args)
|
||||
runHook('afterStateChange', state, oldState, *args)
|
||||
|
||||
def _deckBrowserState(self, oldState):
|
||||
self.deckBrowser.show()
|
||||
@ -405,10 +413,12 @@ the manual for information on how to restore from an automatic backup."))
|
||||
|
||||
def _reviewState(self, oldState):
|
||||
self.reviewer.show()
|
||||
self.web.setCanFocus(True)
|
||||
|
||||
def _reviewCleanup(self, newState):
|
||||
if newState != "resetRequired" and newState != "review":
|
||||
self.reviewer.cleanup()
|
||||
self.web.setCanFocus(False)
|
||||
|
||||
def noteChanged(self, nid):
|
||||
"Called when a card or note is edited (but not deleted)."
|
||||
@ -504,7 +514,7 @@ title="%s">%s</button>''' % (
|
||||
self.toolbar = aqt.toolbar.Toolbar(self, tweb)
|
||||
self.toolbar.draw()
|
||||
# main area
|
||||
self.web = aqt.webview.AnkiWebView()
|
||||
self.web = aqt.webview.AnkiWebView(canFocus=True)
|
||||
self.web.setObjectName("mainText")
|
||||
self.web.setFocusPolicy(Qt.WheelFocus)
|
||||
self.web.setMinimumWidth(400)
|
||||
|
@ -123,7 +123,7 @@ to their original deck.""")
|
||||
counts = list(self.mw.col.sched.counts())
|
||||
finished = not sum(counts)
|
||||
for n in range(len(counts)):
|
||||
if counts[n] == 1000:
|
||||
if counts[n] >= 1000:
|
||||
counts[n] = "1000+"
|
||||
but = self.mw.button
|
||||
if finished:
|
||||
|
@ -18,6 +18,8 @@ class Preferences(QDialog):
|
||||
self.prof = self.mw.pm.profile
|
||||
self.form = aqt.forms.preferences.Ui_Preferences()
|
||||
self.form.setupUi(self)
|
||||
self.form.buttonBox.button(QDialogButtonBox.Help).setAutoDefault(False)
|
||||
self.form.buttonBox.button(QDialogButtonBox.Close).setAutoDefault(False)
|
||||
self.connect(self.form.buttonBox, SIGNAL("helpRequested()"),
|
||||
lambda: openHelp("profileprefs"))
|
||||
self.setupCollection()
|
||||
|
@ -179,7 +179,7 @@ documentation for information on using a flash drive.""")
|
||||
|
||||
def _defaultBase(self):
|
||||
if isWin:
|
||||
if qtmajor >= 5:
|
||||
if False: #qtmajor >= 5:
|
||||
loc = QStandardPaths.writeableLocation(QStandardPaths.DocumentsLocation)
|
||||
else:
|
||||
loc = QDesktopServices.storageLocation(QDesktopServices.DocumentsLocation)
|
||||
@ -284,7 +284,15 @@ please see:
|
||||
|
||||
def _onLangSelected(self):
|
||||
f = self.langForm
|
||||
code = langs[f.lang.currentRow()][1]
|
||||
obj = langs[f.lang.currentRow()]
|
||||
code = obj[1]
|
||||
name = obj[0]
|
||||
en = "Are you sure you wish to display Anki's interface in %s?"
|
||||
r = QMessageBox.question(
|
||||
None, "Anki", en%name, QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No)
|
||||
if r != QMessageBox.Yes:
|
||||
return self._setDefaultLang()
|
||||
self.meta['defaultLang'] = code
|
||||
sql = "update profiles set data = ? where name = ?"
|
||||
self.db.execute(sql, cPickle.dumps(self.meta), "_global")
|
||||
|
15
aqt/qt.py
@ -2,9 +2,13 @@
|
||||
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
# imports are all in this file to make moving to pyside easier in the future
|
||||
# fixme: make sure not to optimize imports on this file
|
||||
|
||||
import sip
|
||||
import os
|
||||
|
||||
import sip, os
|
||||
from anki.utils import isWin, isMac
|
||||
|
||||
sip.setapi('QString', 2)
|
||||
sip.setapi('QVariant', 2)
|
||||
sip.setapi('QUrl', 2)
|
||||
@ -18,14 +22,16 @@ from PyQt4.QtGui import *
|
||||
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
|
||||
from PyQt4.QtNetwork import QLocalServer, QLocalSocket
|
||||
|
||||
|
||||
def debug():
|
||||
from PyQt4.QtCore import pyqtRemoveInputHook
|
||||
from pdb import set_trace
|
||||
pyqtRemoveInputHook()
|
||||
set_trace()
|
||||
|
||||
import sys, traceback
|
||||
|
||||
if os.environ.get("DEBUG"):
|
||||
import sys, traceback
|
||||
def info(type, value, tb):
|
||||
from PyQt4.QtCore import pyqtRemoveInputHook
|
||||
for line in traceback.format_exception(type, value, tb):
|
||||
@ -43,8 +49,3 @@ if qtmajor <= 4 and qtminor <= 6:
|
||||
import anki.template.furigana
|
||||
anki.template.furigana.ruby = r'<span style="display: inline-block; text-align: center; line-height: 1; white-space: nowrap; vertical-align: baseline; margin: 0; padding: 0"><span style="display: block; text-decoration: none; line-height: 1.2; font-weight: normal; font-size: 0.64em">\2</span>\1</span>'
|
||||
|
||||
if isWin or isMac:
|
||||
# we no longer use this, but want it included in the mac+win builds
|
||||
# so we don't break add-ons that use it. any new add-ons should use
|
||||
# the above variables instead
|
||||
from PyQt4 import pyqtconfig
|
||||
|
@ -134,7 +134,8 @@ function _updateQA (q, answerMode, klass) {
|
||||
typeans.focus();
|
||||
}
|
||||
if (answerMode) {
|
||||
window.location = "#answer";
|
||||
var e = $("#answer");
|
||||
if (e[0]) { e[0].scrollIntoView(); }
|
||||
} else {
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
@ -433,10 +434,11 @@ Please run Tools>Empty Cards""")
|
||||
return txt.split("::")[0]
|
||||
return txt
|
||||
matches = [noHint(txt) for txt in matches]
|
||||
if len(matches) > 1:
|
||||
txt = ", ".join(matches)
|
||||
else:
|
||||
uniqMatches = set(matches)
|
||||
if len(uniqMatches) == 1:
|
||||
txt = matches[0]
|
||||
else:
|
||||
txt = ", ".join(matches)
|
||||
return txt
|
||||
|
||||
def tokenizeComparison(self, given, correct):
|
||||
|
@ -170,6 +170,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
|
||||
elif "10061" in err or "10013" in err or "10053" in err:
|
||||
return _(
|
||||
"Antivirus or firewall software is preventing Anki from connecting to the internet.")
|
||||
elif "10054" in err or "Broken pipe" in err:
|
||||
return _("Connection timed out. Either your internet connection is experiencing problems, or you have a very large file in your media folder.")
|
||||
elif "Unable to find the server" in err:
|
||||
return _(
|
||||
"Server not found. Either your connection is down, or antivirus/firewall "
|
||||
@ -178,6 +180,8 @@ AnkiWeb is too busy at the moment. Please try again in a few minutes.""")
|
||||
return _("Proxy authentication required.")
|
||||
elif "code: 413" in err:
|
||||
return _("Your collection or a media file is too large to sync.")
|
||||
elif "EOF occurred in violation of protocol" in err:
|
||||
return _("Error establishing a secure connection. This is usually caused by filtering software, or problems with your ISP.")
|
||||
return err
|
||||
|
||||
def _getUserPass(self):
|
||||
|
@ -240,7 +240,7 @@ def getFile(parent, title, cb, filter="*.*", dir=None, key=None):
|
||||
ret = []
|
||||
def accept():
|
||||
# work around an osx crash
|
||||
aqt.mw.app.processEvents()
|
||||
#aqt.mw.app.processEvents()
|
||||
file = unicode(list(d.selectedFiles())[0])
|
||||
if dirkey:
|
||||
dir = os.path.dirname(file)
|
||||
@ -394,6 +394,7 @@ def tooltip(msg, period=3000, parent=None):
|
||||
lab.setWindowFlags(Qt.ToolTip)
|
||||
p = QPalette()
|
||||
p.setColor(QPalette.Window, QColor("#feffc4"))
|
||||
p.setColor(QPalette.WindowText, QColor("#000000"))
|
||||
lab.setPalette(p)
|
||||
lab.move(
|
||||
aw.mapToGlobal(QPoint(0, -100 + aw.height())))
|
||||
|
@ -40,7 +40,8 @@ class AnkiWebPage(QWebPage):
|
||||
|
||||
class AnkiWebView(QWebView):
|
||||
|
||||
def __init__(self):
|
||||
# canFocus implies canCopy
|
||||
def __init__(self, canFocus=False, canCopy=False):
|
||||
QWebView.__init__(self)
|
||||
self.setRenderHints(
|
||||
QPainter.TextAntialiasing |
|
||||
@ -59,6 +60,8 @@ class AnkiWebView(QWebView):
|
||||
self.allowDrops = False
|
||||
# reset each time new html is set; used to detect if still in same state
|
||||
self.key = None
|
||||
self.setCanFocus(canFocus)
|
||||
self._canCopy = canCopy or canFocus
|
||||
|
||||
def keyPressEvent(self, evt):
|
||||
if evt.matches(QKeySequence.Copy):
|
||||
@ -78,9 +81,7 @@ class AnkiWebView(QWebView):
|
||||
QWebView.keyReleaseEvent(self, evt)
|
||||
|
||||
def contextMenuEvent(self, evt):
|
||||
# lazy: only run in reviewer
|
||||
import aqt
|
||||
if aqt.mw.state != "review":
|
||||
if not self._canCopy:
|
||||
return
|
||||
m = QMenu(self)
|
||||
a = m.addAction(_("Copy"))
|
||||
@ -131,6 +132,13 @@ button {
|
||||
def setBridge(self, bridge):
|
||||
self._bridge.setBridge(bridge)
|
||||
|
||||
def setCanFocus(self, canFocus=False):
|
||||
self._canFocus = canFocus
|
||||
if self._canFocus:
|
||||
self.setFocusPolicy(Qt.WheelFocus)
|
||||
else:
|
||||
self.setFocusPolicy(Qt.NoFocus)
|
||||
|
||||
def eval(self, js):
|
||||
self.page().mainFrame().evaluateJavaScript(js)
|
||||
|
||||
|
@ -46,7 +46,7 @@
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
<number>99999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -35,6 +35,8 @@
|
||||
<file>icons/editclear.png</file>
|
||||
<file>icons/view-statistics.png</file>
|
||||
<file>icons/emblem-favorite.png</file>
|
||||
<file>icons/emblem-favorite-dark.png</file>
|
||||
<file>icons/emblem-favorite-off.png</file>
|
||||
<file>icons/view-pim-calendar.png</file>
|
||||
<file>icons/anki-tag.png</file>
|
||||
<file>icons/edit-redo.png</file>
|
||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 545 B |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 786 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 440 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 459 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 537 B |
BIN
designer/icons/emblem-favorite-dark.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
designer/icons/emblem-favorite-off.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 802 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 502 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 482 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 483 B |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 475 B |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 517 B |
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 457 B |
@ -94,20 +94,6 @@
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="1">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="importButton">
|
||||
<property name="text">
|
||||
<string>&Import</string>
|
||||
</property>
|
||||
<property name="default">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QScrollArea" name="mappingArea">
|
||||
<property name="sizePolicy">
|
||||
@ -133,8 +119,8 @@
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>402</width>
|
||||
<height>206</height>
|
||||
<width>529</width>
|
||||
<height>251</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
@ -158,7 +144,6 @@
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>importButton</tabstop>
|
||||
<tabstop>buttonBox</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
|
@ -9,8 +9,27 @@ def assertException(exception, func):
|
||||
found = True
|
||||
assert found
|
||||
|
||||
def getEmptyDeck(**kwargs):
|
||||
|
||||
# Creating new decks is expensive. Just do it once, and then spin off
|
||||
# copies from the master.
|
||||
def getEmptyCol():
|
||||
if len(getEmptyCol.master) == 0:
|
||||
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
|
||||
os.close(fd)
|
||||
os.unlink(nam)
|
||||
col = aopen(nam)
|
||||
col.db.close()
|
||||
getEmptyCol.master = nam
|
||||
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
|
||||
shutil.copy(getEmptyCol.master, nam)
|
||||
return aopen(nam)
|
||||
|
||||
getEmptyCol.master = ""
|
||||
|
||||
# Fallback for when the DB needs options passed in.
|
||||
def getEmptyDeckWith(**kwargs):
|
||||
(fd, nam) = tempfile.mkstemp(suffix=".anki2")
|
||||
os.close(fd)
|
||||
os.unlink(nam)
|
||||
return aopen(nam, **kwargs)
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
# coding: utf-8
|
||||
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
def test_previewCards():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u'1'
|
||||
f['Back'] = u'2'
|
||||
@ -23,7 +23,7 @@ def test_previewCards():
|
||||
assert deck.cardCount() == 1
|
||||
|
||||
def test_delete():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u'1'
|
||||
f['Back'] = u'2'
|
||||
@ -39,7 +39,7 @@ def test_delete():
|
||||
assert deck.db.scalar("select count() from graves") == 2
|
||||
|
||||
def test_misc():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u'1'
|
||||
f['Back'] = u'2'
|
||||
@ -49,7 +49,7 @@ def test_misc():
|
||||
assert c.template()['ord'] == 0
|
||||
|
||||
def test_genrem():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u'1'
|
||||
f['Back'] = u''
|
||||
@ -76,7 +76,7 @@ def test_genrem():
|
||||
assert len(f.cards()) == 2
|
||||
|
||||
def test_gendeck():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
cloze = d.models.byName("Cloze")
|
||||
d.models.setCurrent(cloze)
|
||||
f = d.newNote()
|
||||
|
@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
from tests.shared import assertException, getEmptyDeck
|
||||
import os, tempfile
|
||||
from tests.shared import assertException, getEmptyCol
|
||||
from anki.stdmodels import addBasicModel
|
||||
|
||||
from anki import Collection as aopen
|
||||
@ -11,8 +11,9 @@ newMod = None
|
||||
|
||||
def test_create():
|
||||
global newPath, newMod
|
||||
path = "/tmp/test_attachNew.anki2"
|
||||
(fd, path) = tempfile.mkstemp(suffix=".anki2", prefix="test_attachNew")
|
||||
try:
|
||||
os.close(fd)
|
||||
os.unlink(path)
|
||||
except OSError:
|
||||
pass
|
||||
@ -40,7 +41,7 @@ def test_openReadOnly():
|
||||
os.unlink(newPath)
|
||||
|
||||
def test_noteAddDelete():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
# add a note
|
||||
f = deck.newNote()
|
||||
f['Front'] = u"one"; f['Back'] = u"two"
|
||||
@ -79,7 +80,7 @@ def test_noteAddDelete():
|
||||
assert f2.dupeOrEmpty()
|
||||
|
||||
def test_fieldChecksum():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u"new"; f['Back'] = u"new2"
|
||||
deck.addNote(f)
|
||||
@ -92,7 +93,7 @@ def test_fieldChecksum():
|
||||
"select csum from notes") == int("302811ae", 16)
|
||||
|
||||
def test_addDelTags():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u"1"
|
||||
deck.addNote(f)
|
||||
@ -111,14 +112,14 @@ def test_addDelTags():
|
||||
assert len(f.tags) == 2
|
||||
|
||||
def test_timestamps():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
assert len(deck.models.models) == 4
|
||||
for i in range(100):
|
||||
addBasicModel(deck)
|
||||
assert len(deck.models.models) == 104
|
||||
|
||||
def test_furigana():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
mm = deck.models
|
||||
m = mm.current()
|
||||
# filter should work
|
||||
|
@ -1,9 +1,9 @@
|
||||
# coding: utf-8
|
||||
|
||||
from tests.shared import assertException, getEmptyDeck
|
||||
from tests.shared import assertException, getEmptyCol
|
||||
|
||||
def test_basic():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
# we start with a standard deck
|
||||
assert len(deck.decks.decks) == 1
|
||||
# it should have an id of 1
|
||||
@ -42,7 +42,7 @@ def test_basic():
|
||||
deck.sched.deckDueList()
|
||||
|
||||
def test_remove():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
# create a new deck, and add a note/card to it
|
||||
g1 = deck.decks.id("g1")
|
||||
f = deck.newNote()
|
||||
@ -68,7 +68,7 @@ def test_remove():
|
||||
assert deck.noteCount() == 0
|
||||
|
||||
def test_rename():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
id = d.decks.id("hello::world")
|
||||
# should be able to rename into a completely different branch, creating
|
||||
# parents as necessary
|
||||
@ -89,7 +89,7 @@ def test_rename():
|
||||
assert n in d.decks.allNames()
|
||||
|
||||
def test_renameForDragAndDrop():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
|
||||
def deckNames():
|
||||
return [ name for name in sorted(d.decks.allNames()) if name <> u'Default' ]
|
||||
|
@ -4,7 +4,7 @@ import nose, os, tempfile
|
||||
from anki import Collection as aopen
|
||||
from anki.exporting import *
|
||||
from anki.importing import Anki2Importer
|
||||
from shared import getEmptyDeck
|
||||
from shared import getEmptyCol
|
||||
|
||||
deck = None
|
||||
ds = None
|
||||
@ -12,7 +12,7 @@ testDir = os.path.dirname(__file__)
|
||||
|
||||
def setup1():
|
||||
global deck
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = ["tag", "tag2"]
|
||||
deck.addNote(f)
|
||||
@ -36,7 +36,9 @@ def test_export_anki():
|
||||
deck.decks.setConf(dobj, confId)
|
||||
# export
|
||||
e = AnkiExporter(deck)
|
||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
|
||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||
newname = unicode(newname)
|
||||
os.close(fd)
|
||||
os.unlink(newname)
|
||||
e.exportInto(newname)
|
||||
# exporting should not have changed conf for original deck
|
||||
@ -54,7 +56,9 @@ def test_export_anki():
|
||||
# conf should be 1
|
||||
assert dobj['conf'] == 1
|
||||
# try again, limited to a deck
|
||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
|
||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||
newname = unicode(newname)
|
||||
os.close(fd)
|
||||
os.unlink(newname)
|
||||
e.did = 1
|
||||
e.exportInto(newname)
|
||||
@ -69,13 +73,15 @@ def test_export_ankipkg():
|
||||
n['Front'] = u'[sound:今日.mp3]'
|
||||
deck.addNote(n)
|
||||
e = AnkiPackageExporter(deck)
|
||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".apkg")[1])
|
||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".apkg")
|
||||
newname = unicode(newname)
|
||||
os.close(fd)
|
||||
os.unlink(newname)
|
||||
e.exportInto(newname)
|
||||
|
||||
@nose.with_setup(setup1)
|
||||
def test_export_anki_due():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u"foo"
|
||||
deck.addNote(f)
|
||||
@ -92,11 +98,13 @@ def test_export_anki_due():
|
||||
# export
|
||||
e = AnkiExporter(deck)
|
||||
e.includeSched = True
|
||||
newname = unicode(tempfile.mkstemp(prefix="ankitest", suffix=".anki2")[1])
|
||||
fd, newname = tempfile.mkstemp(prefix="ankitest", suffix=".anki2")
|
||||
newname = unicode(newname)
|
||||
os.close(fd)
|
||||
os.unlink(newname)
|
||||
e.exportInto(newname)
|
||||
# importing into a new deck, the due date should be equivalent
|
||||
deck2 = getEmptyDeck()
|
||||
deck2 = getEmptyCol()
|
||||
imp = Anki2Importer(deck2, newname)
|
||||
imp.run()
|
||||
c = deck2.getCard(c.id)
|
||||
@ -115,7 +123,9 @@ def test_export_anki_due():
|
||||
@nose.with_setup(setup1)
|
||||
def test_export_textnote():
|
||||
e = TextNoteExporter(deck)
|
||||
f = unicode(tempfile.mkstemp(prefix="ankitest")[1])
|
||||
fd, f = tempfile.mkstemp(prefix="ankitest")
|
||||
f = unicode(f)
|
||||
os.close(fd)
|
||||
os.unlink(f)
|
||||
e.exportInto(f)
|
||||
e.includeTags = True
|
||||
|
@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
|
||||
from anki.find import Finder
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
def test_parse():
|
||||
f = Finder(None)
|
||||
@ -20,7 +20,7 @@ def test_parse():
|
||||
assert f._tokenize("deck:'two words'") == ["deck:two words"]
|
||||
|
||||
def test_findCards():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u'dog'
|
||||
f['Back'] = u'cat'
|
||||
@ -216,7 +216,7 @@ def test_findCards():
|
||||
assert len(deck.findCards("added:2")) == deck.cardCount()
|
||||
|
||||
def test_findReplace():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u'foo'
|
||||
f['Back'] = u'bar'
|
||||
@ -243,7 +243,7 @@ def test_findReplace():
|
||||
f.load(); assert f['Back'] == "reg"
|
||||
|
||||
def test_findDupes():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u'foo'
|
||||
f['Back'] = u'bar'
|
||||
|
@ -1,7 +1,7 @@
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
from tests.shared import getUpgradeDeckPath, getEmptyDeck
|
||||
from tests.shared import getUpgradeDeckPath, getEmptyCol
|
||||
from anki.upgrade import Upgrader
|
||||
from anki.utils import ids2str
|
||||
from anki.importing import Anki1Importer, Anki2Importer, TextImporter, \
|
||||
@ -27,7 +27,7 @@ def test_anki2():
|
||||
open(os.path.join(src.media.dir(), "_foo.jpg"), "w").write("foo")
|
||||
src.close()
|
||||
# create a new empty deck
|
||||
dst = getEmptyDeck()
|
||||
dst = getEmptyCol()
|
||||
# import src into dst
|
||||
imp = Anki2Importer(dst, srcpath)
|
||||
imp.run()
|
||||
@ -50,7 +50,7 @@ def test_anki2():
|
||||
assert len(os.listdir(dst.media.dir())) == 1
|
||||
|
||||
def test_anki2_mediadupes():
|
||||
tmp = getEmptyDeck()
|
||||
tmp = getEmptyCol()
|
||||
# add a note that references a sound
|
||||
n = tmp.newNote()
|
||||
n['Front'] = "[sound:foo.mp3]"
|
||||
@ -60,7 +60,7 @@ def test_anki2_mediadupes():
|
||||
open(os.path.join(tmp.media.dir(), "foo.mp3"), "w").write("foo")
|
||||
tmp.close()
|
||||
# it should be imported correctly into an empty deck
|
||||
empty = getEmptyDeck()
|
||||
empty = getEmptyCol()
|
||||
imp = Anki2Importer(empty, tmp.path)
|
||||
imp.run()
|
||||
assert os.listdir(empty.media.dir()) == ["foo.mp3"]
|
||||
@ -95,7 +95,7 @@ def test_anki2_mediadupes():
|
||||
assert "_" in n.fields[0]
|
||||
|
||||
def test_apkg():
|
||||
tmp = getEmptyDeck()
|
||||
tmp = getEmptyCol()
|
||||
apkg = unicode(os.path.join(testDir, "support/media.apkg"))
|
||||
imp = AnkiPackageImporter(tmp, apkg)
|
||||
assert os.listdir(tmp.media.dir()) == []
|
||||
@ -122,7 +122,7 @@ def test_anki1():
|
||||
os.mkdir(mdir)
|
||||
open(os.path.join(mdir, "_foo.jpg"), "w").write("foo")
|
||||
# create a new empty deck
|
||||
dst = getEmptyDeck()
|
||||
dst = getEmptyCol()
|
||||
# import src into dst
|
||||
imp = Anki1Importer(dst, tmp)
|
||||
imp.run()
|
||||
@ -138,7 +138,7 @@ def test_anki1():
|
||||
|
||||
def test_anki1_diffmodels():
|
||||
# create a new empty deck
|
||||
dst = getEmptyDeck()
|
||||
dst = getEmptyCol()
|
||||
# import the 1 card version of the model
|
||||
tmp = getUpgradeDeckPath("diffmodels1.anki")
|
||||
imp = Anki1Importer(dst, tmp)
|
||||
@ -165,7 +165,7 @@ def test_anki1_diffmodels():
|
||||
|
||||
def test_suspended():
|
||||
# create a new empty deck
|
||||
dst = getEmptyDeck()
|
||||
dst = getEmptyCol()
|
||||
# import the 1 card version of the model
|
||||
tmp = getUpgradeDeckPath("suspended12.anki")
|
||||
imp = Anki1Importer(dst, tmp)
|
||||
@ -174,7 +174,7 @@ def test_suspended():
|
||||
|
||||
def test_anki2_diffmodels():
|
||||
# create a new empty deck
|
||||
dst = getEmptyDeck()
|
||||
dst = getEmptyCol()
|
||||
# import the 1 card version of the model
|
||||
tmp = getUpgradeDeckPath("diffmodels2-1.apkg")
|
||||
imp = AnkiPackageImporter(dst, tmp)
|
||||
@ -206,7 +206,7 @@ def test_anki2_diffmodels():
|
||||
|
||||
def test_anki2_updates():
|
||||
# create a new empty deck
|
||||
dst = getEmptyDeck()
|
||||
dst = getEmptyCol()
|
||||
tmp = getUpgradeDeckPath("update1.apkg")
|
||||
imp = AnkiPackageImporter(dst, tmp)
|
||||
imp.run()
|
||||
@ -232,7 +232,7 @@ def test_anki2_updates():
|
||||
assert dst.db.scalar("select flds from notes").startswith("goodbye")
|
||||
|
||||
def test_csv():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
file = unicode(os.path.join(testDir, "support/text-2fields.txt"))
|
||||
i = TextImporter(deck, file)
|
||||
i.initMapping()
|
||||
@ -266,7 +266,7 @@ def test_csv():
|
||||
deck.close()
|
||||
|
||||
def test_csv2():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
mm = deck.models
|
||||
m = mm.current()
|
||||
f = mm.newField("Three")
|
||||
@ -289,7 +289,7 @@ def test_csv2():
|
||||
deck.close()
|
||||
|
||||
def test_supermemo_xml_01_unicode():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
file = unicode(os.path.join(testDir, "support/supermemo1.xml"))
|
||||
i = SupermemoXmlImporter(deck, file)
|
||||
#i.META.logToStdOutput = True
|
||||
@ -297,12 +297,13 @@ def test_supermemo_xml_01_unicode():
|
||||
assert i.total == 1
|
||||
cid = deck.db.scalar("select id from cards")
|
||||
c = deck.getCard(cid)
|
||||
assert c.factor == 5701
|
||||
# Applies A Factor-to-E Factor conversion
|
||||
assert c.factor == 2879
|
||||
assert c.reps == 7
|
||||
deck.close()
|
||||
|
||||
def test_mnemo():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
file = unicode(os.path.join(testDir, "support/mnemo.db"))
|
||||
i = MnemosyneImporter(deck, file)
|
||||
i.run()
|
||||
|
@ -1,14 +1,14 @@
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol
|
||||
from anki.utils import stripHTML
|
||||
|
||||
def test_latex():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# change latex cmd to simulate broken build
|
||||
import anki.latex
|
||||
anki.latex.latexCmd[0] = "nolatex"
|
||||
anki.latex.latexCmds[0][0] = "nolatex"
|
||||
# add a note with latex
|
||||
f = d.newNote()
|
||||
f['Front'] = u"[latex]hello[/latex]"
|
||||
@ -17,7 +17,7 @@ def test_latex():
|
||||
assert len(os.listdir(d.media.dir())) == 0
|
||||
# check the error message
|
||||
msg = f.cards()[0].q()
|
||||
assert "executing latex" in msg
|
||||
assert "executing nolatex" in msg
|
||||
assert "installed" in msg
|
||||
# check if we have latex installed, and abort test if we don't
|
||||
for cmd in ("latex", "dvipng"):
|
||||
@ -26,7 +26,7 @@ def test_latex():
|
||||
print "aborting test; %s is not installed" % cmd
|
||||
return
|
||||
# fix path
|
||||
anki.latex.latexCmd[0] = "latex"
|
||||
anki.latex.latexCmds[0][0] = "latex"
|
||||
# check media db should cause latex to be generated
|
||||
d.media.check()
|
||||
assert len(os.listdir(d.media.dir())) == 1
|
||||
|
@ -4,12 +4,12 @@ import tempfile
|
||||
import os
|
||||
import time
|
||||
|
||||
from shared import getEmptyDeck, testDir
|
||||
from shared import getEmptyCol, testDir
|
||||
|
||||
|
||||
# copying files to media folder
|
||||
def test_add():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
dir = tempfile.mkdtemp(prefix="anki")
|
||||
path = os.path.join(dir, u"foo.jpg")
|
||||
open(path, "w").write("hello")
|
||||
@ -22,7 +22,7 @@ def test_add():
|
||||
assert d.media.addFile(path) == "foo (1).jpg"
|
||||
|
||||
def test_strings():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
mf = d.media.filesInStr
|
||||
mid = d.models.models.keys()[0]
|
||||
assert mf(mid, "aoeu") == []
|
||||
@ -46,7 +46,7 @@ def test_strings():
|
||||
assert es('<img src="foo bar.jpg">') == '<img src="foo%20bar.jpg">'
|
||||
|
||||
def test_deckIntegration():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# create a media dir
|
||||
d.media.dir()
|
||||
# put a file into it
|
||||
@ -68,7 +68,7 @@ def test_deckIntegration():
|
||||
assert ret[1] == ["foo.jpg"]
|
||||
|
||||
def test_changes():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
assert d.media._changed()
|
||||
def added():
|
||||
return d.media.db.execute("select fname from log where type = 0")
|
||||
@ -103,7 +103,7 @@ def test_changes():
|
||||
assert len(list(d.media.removed())) == 1
|
||||
|
||||
def test_illegal():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
aString = u"a:b|cd\\e/f\0g*h"
|
||||
good = u"abcdefgh"
|
||||
assert d.media.stripIllegal(aString) == good
|
||||
|
@ -1,10 +1,10 @@
|
||||
# coding: utf-8
|
||||
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol
|
||||
from anki.utils import stripHTML, joinFields
|
||||
|
||||
def test_modelDelete():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
f = deck.newNote()
|
||||
f['Front'] = u'1'
|
||||
f['Back'] = u'2'
|
||||
@ -14,7 +14,7 @@ def test_modelDelete():
|
||||
assert deck.cardCount() == 0
|
||||
|
||||
def test_modelCopy():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
m = deck.models.current()
|
||||
m2 = deck.models.copy(m)
|
||||
assert m2['name'] == "Basic copy"
|
||||
@ -27,7 +27,7 @@ def test_modelCopy():
|
||||
assert deck.models.scmhash(m) == deck.models.scmhash(m2)
|
||||
|
||||
def test_fields():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u'1'
|
||||
f['Back'] = u'2'
|
||||
@ -74,7 +74,7 @@ def test_fields():
|
||||
assert d.getNote(d.models.nids(m)[0]).fields == ["", "2", "1"]
|
||||
|
||||
def test_templates():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
m = d.models.current(); mm = d.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
@ -107,8 +107,31 @@ def test_templates():
|
||||
mm.addTemplate(m, t)
|
||||
assert not d.models.remTemplate(m, m['tmpls'][0])
|
||||
|
||||
def test_cloze_ordinals():
|
||||
d = getEmptyCol()
|
||||
d.models.setCurrent(d.models.byName("Cloze"))
|
||||
m = d.models.current(); mm = d.models
|
||||
|
||||
#We replace the default Cloze template
|
||||
t = mm.newTemplate("ChainedCloze")
|
||||
t['qfmt'] = "{{text:cloze:Text}}"
|
||||
t['afmt'] = "{{text:cloze:Text}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
d.models.remTemplate(m, m['tmpls'][0])
|
||||
|
||||
f = d.newNote()
|
||||
f['Text'] = u'{{c1::firstQ::firstA}}{{c2::secondQ::secondA}}'
|
||||
d.addNote(f)
|
||||
assert d.cardCount() == 2
|
||||
(c, c2) = f.cards()
|
||||
# first card should have first ord
|
||||
assert c.ord == 0
|
||||
assert c2.ord == 1
|
||||
|
||||
|
||||
def test_text():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
m = d.models.current()
|
||||
m['tmpls'][0]['qfmt'] = "{{text:Front}}"
|
||||
d.models.save(m)
|
||||
@ -118,7 +141,7 @@ def test_text():
|
||||
assert "helloworld" in f.cards()[0].q()
|
||||
|
||||
def test_cloze():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
d.models.setCurrent(d.models.byName("Cloze"))
|
||||
f = d.newNote()
|
||||
assert f.model()['name'] == "Cloze"
|
||||
@ -163,8 +186,31 @@ def test_cloze():
|
||||
f.flush()
|
||||
assert len(f.cards()) == 2
|
||||
|
||||
def test_chained_mods():
|
||||
d = getEmptyCol()
|
||||
d.models.setCurrent(d.models.byName("Cloze"))
|
||||
m = d.models.current(); mm = d.models
|
||||
|
||||
#We replace the default Cloze template
|
||||
t = mm.newTemplate("ChainedCloze")
|
||||
t['qfmt'] = "{{cloze:text:Text}}"
|
||||
t['afmt'] = "{{cloze:text:Text}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
d.models.remTemplate(m, m['tmpls'][0])
|
||||
|
||||
f = d.newNote()
|
||||
q1 = '<span style=\"color:red\">phrase</span>'
|
||||
a1 = '<b>sentence</b>'
|
||||
q2 = '<span style=\"color:red\">en chaine</span>'
|
||||
a2 = '<i>chained</i>'
|
||||
f['Text'] = "This {{c1::%s::%s}} demonstrates {{c1::%s::%s}} clozes." % (q1,a1,q2,a2)
|
||||
assert d.addNote(f) == 1
|
||||
assert "This <span class=cloze>[sentence]</span> demonstrates <span class=cloze>[chained]</span> clozes." in f.cards()[0].q()
|
||||
assert "This <span class=cloze>phrase</span> demonstrates <span class=cloze>en chaine</span> clozes." in f.cards()[0].a()
|
||||
|
||||
def test_modelChange():
|
||||
deck = getEmptyDeck()
|
||||
deck = getEmptyCol()
|
||||
basic = deck.models.byName("Basic")
|
||||
cloze = deck.models.byName("Cloze")
|
||||
# enable second template and add a note
|
||||
@ -238,7 +284,7 @@ def test_modelChange():
|
||||
assert deck.db.scalar("select count() from cards where nid = ?", f.id) == 1
|
||||
|
||||
def test_availOrds():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
m = d.models.current(); mm = d.models
|
||||
t = m['tmpls'][0]
|
||||
f = d.newNote()
|
||||
|
@ -1,171 +0,0 @@
|
||||
# coding: utf-8
|
||||
|
||||
import nose, os, time
|
||||
from tests.shared import assertException
|
||||
|
||||
from anki.sync import Syncer, FullSyncer, RemoteServer, \
|
||||
MediaSyncer, RemoteMediaServer, httpCon
|
||||
from anki import Collection as aopen
|
||||
|
||||
deck1=None
|
||||
deck2=None
|
||||
client=None
|
||||
server=None
|
||||
server2=None
|
||||
|
||||
# Remote tests
|
||||
##########################################################################
|
||||
|
||||
import tests.test_sync as ts
|
||||
from tests.test_sync import setup_basic
|
||||
import anki.sync
|
||||
anki.sync.SYNC_URL = "http://localhost:5000/sync/"
|
||||
TEST_USER = "synctest@ichi2.net"
|
||||
TEST_PASS = "abc123"
|
||||
TEST_HKEY = "WqYF0m7fOHCNPI4a"
|
||||
TEST_REMOTE = True
|
||||
|
||||
def setup_remote():
|
||||
setup_basic()
|
||||
# mark deck1 as changed
|
||||
ts.deck1.save()
|
||||
ts.server = RemoteServer(TEST_HKEY)
|
||||
ts.client.server = ts.server
|
||||
|
||||
@nose.with_setup(setup_remote)
|
||||
def test_meta():
|
||||
global TEST_REMOTE
|
||||
try:
|
||||
# if the key is wrong, meta returns nothing
|
||||
ts.server.hkey = "abc"
|
||||
assert not ts.server.meta()
|
||||
except Exception, e:
|
||||
if e.errno == 61:
|
||||
TEST_REMOTE = False
|
||||
print "aborting; server offline"
|
||||
return
|
||||
ts.server.hkey = TEST_HKEY
|
||||
meta = ts.server.meta()
|
||||
assert meta['mod']
|
||||
assert meta['scm']
|
||||
assert meta['mod'] != ts.client.col.mod
|
||||
assert abs(meta['ts'] - time.time()) < 3
|
||||
|
||||
@nose.with_setup(setup_remote)
|
||||
def test_hkey():
|
||||
if not TEST_REMOTE:
|
||||
return
|
||||
assert not ts.server.hostKey(TEST_USER, "wrongpass")
|
||||
ts.server.hkey = "willchange"
|
||||
k = ts.server.hostKey(TEST_USER, TEST_PASS)
|
||||
assert k == ts.server.hkey == TEST_HKEY
|
||||
|
||||
@nose.with_setup(setup_remote)
|
||||
def test_download():
|
||||
if not TEST_REMOTE:
|
||||
return
|
||||
f = FullSyncer(ts.client.col, "abc", ts.server.con)
|
||||
assertException(Exception, f.download)
|
||||
f.hkey = TEST_HKEY
|
||||
f.download()
|
||||
|
||||
@nose.with_setup(setup_remote)
|
||||
def test_remoteSync():
|
||||
if not TEST_REMOTE:
|
||||
return
|
||||
# not yet associated, so will require a full sync
|
||||
assert ts.client.sync() == "fullSync"
|
||||
# upload
|
||||
f = FullSyncer(ts.client.col, TEST_HKEY, ts.server.con)
|
||||
assert f.upload()
|
||||
ts.client.col.reopen()
|
||||
# should report no changes
|
||||
assert ts.client.sync() == "noChanges"
|
||||
# bump local col
|
||||
ts.client.col.setMod()
|
||||
ts.client.col.save()
|
||||
assert ts.client.sync() == "success"
|
||||
# again, no changes
|
||||
assert ts.client.sync() == "noChanges"
|
||||
|
||||
# Remote media tests
|
||||
##########################################################################
|
||||
# We can't run useful tests for local media, because the desktop code assumes
|
||||
# the current directory is the media folder.
|
||||
|
||||
def setup_remoteMedia():
|
||||
setup_basic()
|
||||
con = httpCon()
|
||||
ts.server = RemoteMediaServer(TEST_HKEY, con)
|
||||
ts.server2 = RemoteServer(TEST_HKEY)
|
||||
ts.client = MediaSyncer(ts.deck1, ts.server)
|
||||
|
||||
@nose.with_setup(setup_remoteMedia)
|
||||
def test_media():
|
||||
if not TEST_REMOTE:
|
||||
return
|
||||
print "media test disabled"
|
||||
return
|
||||
ts.server.mediatest("reset")
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 0
|
||||
assert ts.server.mediatest("count") == 0
|
||||
# initially, nothing to do
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
||||
# add a file
|
||||
time.sleep(1)
|
||||
os.chdir(ts.deck1.media.dir())
|
||||
p = os.path.join(ts.deck1.media.dir(), "foo.jpg")
|
||||
open(p, "wb").write("foo")
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
||||
assert ts.server.mediatest("count") == 0
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
||||
time.sleep(1)
|
||||
# should have been synced
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
||||
assert ts.server.mediatest("count") == 1
|
||||
# if we remove the file, should be removed
|
||||
os.unlink(p)
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 0
|
||||
assert ts.server.mediatest("count") == 0
|
||||
# we should be able to add it again
|
||||
time.sleep(1)
|
||||
open(p, "wb").write("foo")
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
||||
assert ts.server.mediatest("count") == 1
|
||||
# if we modify it, it should get sent too. also we set the zip size very
|
||||
# low here, so that we can test splitting into multiple zips
|
||||
import anki.media; anki.media.SYNC_ZIP_SIZE = 1
|
||||
time.sleep(1)
|
||||
open(p, "wb").write("bar")
|
||||
open(p+"2", "wb").write("baz")
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
||||
assert ts.server.mediatest("count") == 2
|
||||
# if we lose our media db, we should be able to bring it back in sync
|
||||
time.sleep(1)
|
||||
ts.deck1.media.close()
|
||||
os.unlink(ts.deck1.media.dir()+".db")
|
||||
ts.deck1.media.connect()
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
||||
assert ts.server.mediatest("count") == 2
|
||||
# if we send an unchanged file, the server should cope
|
||||
time.sleep(1)
|
||||
ts.deck1.media.db.execute("insert into log values ('foo.jpg', 0)")
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "noChanges"
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 2
|
||||
assert ts.server.mediatest("count") == 2
|
||||
# if we remove foo.jpg on the ts.server, the removal should be synced
|
||||
assert ts.server.mediatest("removefoo") == "OK"
|
||||
assert ts.client.sync(ts.server2.meta()[4]) == "success"
|
||||
assert len(os.listdir(ts.deck1.media.dir())) == 1
|
||||
assert ts.server.mediatest("count") == 1
|
@ -3,13 +3,13 @@
|
||||
import time
|
||||
import copy
|
||||
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol
|
||||
from anki.utils import intTime
|
||||
from anki.hooks import addHook
|
||||
|
||||
|
||||
def test_clock():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
if (d.sched.dayCutoff - intTime()) < 10*60:
|
||||
raise Exception("Unit tests will fail around the day rollover.")
|
||||
|
||||
@ -18,12 +18,12 @@ def checkRevIvl(d, c, targetIvl):
|
||||
return min <= c.ivl <= max
|
||||
|
||||
def test_basics():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
d.reset()
|
||||
assert not d.sched.getCard()
|
||||
|
||||
def test_new():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
d.reset()
|
||||
assert d.sched.newCount == 0
|
||||
# add a note
|
||||
@ -43,29 +43,31 @@ def test_new():
|
||||
assert c.queue == 1
|
||||
assert c.type == 1
|
||||
assert c.due >= t
|
||||
# the default order should ensure siblings are not seen together, and
|
||||
# should show all cards
|
||||
m = d.models.current(); mm = d.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
t['qfmt'] = "{{Back}}"
|
||||
t['afmt'] = "{{Front}}"
|
||||
mm.addTemplate(m, t)
|
||||
mm.save(m)
|
||||
f = d.newNote()
|
||||
f['Front'] = u"2"; f['Back'] = u"2"
|
||||
d.addNote(f)
|
||||
f = d.newNote()
|
||||
f['Front'] = u"3"; f['Back'] = u"3"
|
||||
d.addNote(f)
|
||||
d.reset()
|
||||
qs = ("2", "3", "2", "3")
|
||||
for n in range(4):
|
||||
c = d.sched.getCard()
|
||||
assert qs[n] in c.q()
|
||||
d.sched.answerCard(c, 2)
|
||||
|
||||
# disabled for now, as the learn fudging makes this randomly fail
|
||||
# # the default order should ensure siblings are not seen together, and
|
||||
# # should show all cards
|
||||
# m = d.models.current(); mm = d.models
|
||||
# t = mm.newTemplate("Reverse")
|
||||
# t['qfmt'] = "{{Back}}"
|
||||
# t['afmt'] = "{{Front}}"
|
||||
# mm.addTemplate(m, t)
|
||||
# mm.save(m)
|
||||
# f = d.newNote()
|
||||
# f['Front'] = u"2"; f['Back'] = u"2"
|
||||
# d.addNote(f)
|
||||
# f = d.newNote()
|
||||
# f['Front'] = u"3"; f['Back'] = u"3"
|
||||
# d.addNote(f)
|
||||
# d.reset()
|
||||
# qs = ("2", "3", "2", "3")
|
||||
# for n in range(4):
|
||||
# c = d.sched.getCard()
|
||||
# assert qs[n] in c.q()
|
||||
# d.sched.answerCard(c, 2)
|
||||
|
||||
def test_newLimits():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add some notes
|
||||
g2 = d.decks.id("Default::foo")
|
||||
for i in range(30):
|
||||
@ -95,7 +97,7 @@ def test_newLimits():
|
||||
assert d.sched.newCount == 9
|
||||
|
||||
def test_newBoxes():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -108,7 +110,7 @@ def test_newBoxes():
|
||||
d.sched.answerCard(c, 2)
|
||||
|
||||
def test_learn():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"; f['Back'] = u"two"
|
||||
@ -176,15 +178,13 @@ def test_learn():
|
||||
c.queue = 1
|
||||
c.odue = 321
|
||||
c.flush()
|
||||
print "----begin"
|
||||
d.sched.removeLrn()
|
||||
c.load()
|
||||
print c.__dict__
|
||||
assert c.queue == 2
|
||||
assert c.due == 321
|
||||
|
||||
def test_learn_collapsed():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add 2 notes
|
||||
f = d.newNote()
|
||||
f['Front'] = u"1"
|
||||
@ -210,7 +210,7 @@ def test_learn_collapsed():
|
||||
assert not c.q().endswith("2")
|
||||
|
||||
def test_learn_day():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
@ -268,7 +268,7 @@ def test_learn_day():
|
||||
assert d.sched.counts() == (0, 0, 0)
|
||||
|
||||
def test_reviews():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"; f['Back'] = u"two"
|
||||
@ -360,7 +360,7 @@ def test_reviews():
|
||||
assert c.queue == -1
|
||||
|
||||
def test_button_spacing():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -382,7 +382,7 @@ def test_button_spacing():
|
||||
def test_overdue_lapse():
|
||||
# disabled in commit 3069729776990980f34c25be66410e947e9d51a2
|
||||
return
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
@ -415,7 +415,7 @@ def test_overdue_lapse():
|
||||
assert d.sched.counts() == (0, 0, 1)
|
||||
|
||||
def test_finished():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# nothing due
|
||||
assert "Congratulations" in d.sched.finishedMsg()
|
||||
assert "limit" not in d.sched.finishedMsg()
|
||||
@ -434,7 +434,7 @@ def test_finished():
|
||||
assert "limit" not in d.sched.finishedMsg()
|
||||
|
||||
def test_nextIvl():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"; f['Back'] = u"two"
|
||||
d.addNote(f)
|
||||
@ -490,7 +490,7 @@ def test_nextIvl():
|
||||
assert d.sched.nextIvlStr(c, 4) == "10.8 months"
|
||||
|
||||
def test_misc():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -504,7 +504,7 @@ def test_misc():
|
||||
assert d.sched.getCard()
|
||||
|
||||
def test_suspend():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -547,7 +547,7 @@ def test_suspend():
|
||||
assert c.did == 1
|
||||
|
||||
def test_cram():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -655,7 +655,7 @@ def test_cram():
|
||||
assert c.did == 1
|
||||
|
||||
def test_cram_rem():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -676,7 +676,7 @@ def test_cram_rem():
|
||||
|
||||
def test_cram_resched():
|
||||
# add card
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -780,7 +780,7 @@ def test_cram_resched():
|
||||
# print c.__dict__
|
||||
|
||||
def test_ordcycle():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add two more templates and set second active
|
||||
m = d.models.current(); mm = d.models
|
||||
t = mm.newTemplate("Reverse")
|
||||
@ -804,7 +804,7 @@ def test_ordcycle():
|
||||
assert d.sched.getCard().ord == 2
|
||||
|
||||
def test_counts_idx():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"; f['Back'] = u"two"
|
||||
d.addNote(f)
|
||||
@ -826,7 +826,7 @@ def test_counts_idx():
|
||||
assert d.sched.counts() == (0, 2, 0)
|
||||
|
||||
def test_repCounts():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -878,7 +878,7 @@ def test_repCounts():
|
||||
assert d.sched.counts() == (0, 1, 0)
|
||||
|
||||
def test_timing():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a few review cards, due today
|
||||
for i in range(5):
|
||||
f = d.newNote()
|
||||
@ -904,7 +904,7 @@ def test_timing():
|
||||
assert c.queue == 1
|
||||
|
||||
def test_collapse():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
@ -918,7 +918,7 @@ def test_collapse():
|
||||
assert not d.sched.getCard()
|
||||
|
||||
def test_deckDue():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
@ -968,7 +968,7 @@ def test_deckDue():
|
||||
d.sched.deckDueTree()
|
||||
|
||||
def test_deckTree():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
d.decks.id("new::b::c")
|
||||
d.decks.id("new2")
|
||||
# new should not appear twice in tree
|
||||
@ -977,7 +977,7 @@ def test_deckTree():
|
||||
assert "new" not in names
|
||||
|
||||
def test_deckFlow():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
@ -1001,7 +1001,7 @@ def test_deckFlow():
|
||||
d.sched.answerCard(c, 2)
|
||||
|
||||
def test_reorder():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note with default deck
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
@ -1039,7 +1039,7 @@ def test_reorder():
|
||||
assert f4.cards()[0].due == 2
|
||||
|
||||
def test_forget():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -1053,7 +1053,7 @@ def test_forget():
|
||||
assert d.sched.counts() == (1, 0, 0)
|
||||
|
||||
def test_resched():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
d.addNote(f)
|
||||
@ -1069,7 +1069,7 @@ def test_resched():
|
||||
assert c.ivl == +1
|
||||
|
||||
def test_norelearn():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# add a note
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
@ -1090,7 +1090,7 @@ def test_norelearn():
|
||||
d.sched.answerCard(c, 1)
|
||||
|
||||
def test_failmult():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"; f['Back'] = u"two"
|
||||
d.addNote(f)
|
||||
|
@ -1,10 +1,10 @@
|
||||
# coding: utf-8
|
||||
|
||||
import os
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol
|
||||
|
||||
def test_stats():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
f = d.newNote()
|
||||
f['Front'] = "foo"
|
||||
d.addNote(f)
|
||||
@ -18,7 +18,7 @@ def test_stats():
|
||||
assert d.cardStats(c)
|
||||
|
||||
def test_graphs_empty():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
assert d.stats().report()
|
||||
|
||||
def test_graphs():
|
||||
|
@ -5,7 +5,7 @@ import nose, os, shutil, time
|
||||
from anki import Collection as aopen, Collection
|
||||
from anki.utils import intTime
|
||||
from anki.sync import Syncer, LocalServer
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol, getEmptyDeckWith
|
||||
|
||||
# Local tests
|
||||
##########################################################################
|
||||
@ -18,7 +18,7 @@ server2=None
|
||||
|
||||
def setup_basic():
|
||||
global deck1, deck2, client, server
|
||||
deck1 = getEmptyDeck()
|
||||
deck1 = getEmptyCol()
|
||||
# add a note to deck 1
|
||||
f = deck1.newNote()
|
||||
f['Front'] = u"foo"; f['Back'] = u"bar"; f.tags = [u"foo"]
|
||||
@ -26,7 +26,7 @@ def setup_basic():
|
||||
# answer it
|
||||
deck1.reset(); deck1.sched.answerCard(deck1.sched.getCard(), 4)
|
||||
# repeat for deck2
|
||||
deck2 = getEmptyDeck(server=True)
|
||||
deck2 = getEmptyDeckWith(server=True)
|
||||
f = deck2.newNote()
|
||||
f['Front'] = u"bar"; f['Back'] = u"bar"; f.tags = [u"bar"]
|
||||
deck2.addNote(f)
|
||||
@ -247,7 +247,7 @@ def test_threeway2():
|
||||
anki.notes.intTime = lambda x=1: intTime(1000)
|
||||
def setup():
|
||||
# create collection 1 with a single note
|
||||
c1 = getEmptyDeck()
|
||||
c1 = getEmptyCol()
|
||||
f = c1.newNote()
|
||||
f['Front'] = u"startingpoint"
|
||||
nid = f.id
|
||||
@ -325,7 +325,7 @@ def _test_speed():
|
||||
deck1.tags.tags[tx] = -1
|
||||
deck1._usn = -1
|
||||
deck1.save()
|
||||
deck2 = getEmptyDeck(server=True)
|
||||
deck2 = getEmptyDeckWith(server=True)
|
||||
deck1.scm = deck2.scm = 0
|
||||
server = LocalServer(deck2)
|
||||
client = Syncer(deck1, server)
|
||||
|
@ -1,11 +1,11 @@
|
||||
# coding: utf-8
|
||||
|
||||
import time
|
||||
from tests.shared import getEmptyDeck
|
||||
from tests.shared import getEmptyCol
|
||||
from anki.consts import *
|
||||
|
||||
def test_op():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
# should have no undo by default
|
||||
assert not d.undoName()
|
||||
# let's adjust a study option
|
||||
@ -36,7 +36,7 @@ def test_op():
|
||||
assert d.undoName() == "Review"
|
||||
|
||||
def test_review():
|
||||
d = getEmptyDeck()
|
||||
d = getEmptyCol()
|
||||
d.conf['counts'] = COUNT_REMAINING
|
||||
f = d.newNote()
|
||||
f['Front'] = u"one"
|
||||
|
@ -1,6 +1,6 @@
|
||||
# coding: utf-8
|
||||
|
||||
import datetime, shutil
|
||||
import datetime, shutil, tempfile
|
||||
from anki import Collection
|
||||
from anki.consts import *
|
||||
from shared import getUpgradeDeckPath, testDir
|
||||
@ -63,8 +63,9 @@ def test_invalid_ords():
|
||||
assert deck.db.scalar("select count() from cards where ord = 1") == 1
|
||||
|
||||
def test_upgrade2():
|
||||
p = "/tmp/alpha-upgrade.anki2"
|
||||
fd, p = tempfile.mkstemp(suffix=".anki2", prefix="alpha-upgrade")
|
||||
if os.path.exists(p):
|
||||
os.close(fd)
|
||||
os.unlink(p)
|
||||
shutil.copy2(os.path.join(testDir, "support/anki2-alpha.anki2"), p)
|
||||
col = Collection(p)
|
||||
|
11
thirdparty/pyaudio.py
vendored
@ -92,16 +92,7 @@ __author__ = "Hubert Pham"
|
||||
__version__ = "0.2.4"
|
||||
__docformat__ = "restructuredtext en"
|
||||
|
||||
import sys
|
||||
|
||||
# attempt to import PortAudio
|
||||
try:
|
||||
import _portaudio as pa
|
||||
except ImportError:
|
||||
print "Please build and install the PortAudio Python " +\
|
||||
"bindings first."
|
||||
sys.exit(-1)
|
||||
|
||||
import _portaudio as pa
|
||||
|
||||
# Try to use Python 2.4's built in `set'
|
||||
try:
|
||||
|
@ -29,7 +29,7 @@ do
|
||||
echo " * "$py
|
||||
pyuic4 $i -o $py
|
||||
# munge the output to use gettext
|
||||
perl -pi.bak -e 's/QtGui.QApplication.translate\(".*?", /_(/; s/, None, QtGui.*/))/' $py
|
||||
perl -pi.bak -e 's/(QtGui\.QApplication\.)?_?translate\(".*?", /_(/; s/, None.*/))/' $py
|
||||
rm $py.bak
|
||||
fi
|
||||
done
|
||||
|